mirror of
https://github.com/TeamNewPipe/NewPipe
synced 2026-01-13 18:22:41 +00:00
Compare commits
1925 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3c306a0971 | ||
|
|
c0d6c8aeb3 | ||
|
|
b27b49e4f3 | ||
|
|
7ed0dbcf1a | ||
|
|
8a23de6b20 | ||
|
|
6cc3089204 | ||
|
|
093e95c078 | ||
|
|
7c8ac04e35 | ||
|
|
dc88f8b172 | ||
|
|
c94f0ded27 | ||
|
|
b553aa2159 | ||
|
|
a7bd2666f0 | ||
|
|
fe2fc60581 | ||
|
|
ce59c05d5b | ||
|
|
a4858bc702 | ||
|
|
a2bb58a991 | ||
|
|
f7b41227d2 | ||
|
|
5b1a6831d5 | ||
|
|
42b1bbe414 | ||
|
|
db9f20a22f | ||
|
|
cf67b592da | ||
|
|
e867bfbc82 | ||
|
|
9a671851df | ||
|
|
4b92f78cc8 | ||
|
|
c585982557 | ||
|
|
6bf22e7ad0 | ||
|
|
2f8dccf7f6 | ||
|
|
027768d97d | ||
|
|
085f63b8c5 | ||
|
|
6f7c337e00 | ||
|
|
16a968f3bb | ||
|
|
d7e0167fed | ||
|
|
41c4f515cf | ||
|
|
d9a8218372 | ||
|
|
dd9bd4da8b | ||
|
|
cf98500b7f | ||
|
|
2ce8facc05 | ||
|
|
d1b117d07c | ||
|
|
c0377c7ebf | ||
|
|
a2490a5730 | ||
|
|
177334ba62 | ||
|
|
7bce588767 | ||
|
|
4bb67c634f | ||
|
|
3653afbcc4 | ||
|
|
1f4a4ea09f | ||
|
|
3d38add4b4 | ||
|
|
124b7eefb5 | ||
|
|
b52924048c | ||
|
|
93393f5dff | ||
|
|
275a75ebaa | ||
|
|
3e4a7a19cc | ||
|
|
734af457f3 | ||
|
|
55bdb1f47a | ||
|
|
adff0d199d | ||
|
|
f95b3262a0 | ||
|
|
794a14e76c | ||
|
|
ba857b5ef7 | ||
|
|
2aed04a8c2 | ||
|
|
5f9e6b51da | ||
|
|
e7b5c99ed6 | ||
|
|
9c0b3d35be | ||
|
|
a54bc96eab | ||
|
|
a2a8e4b965 | ||
|
|
81ad2c61d9 | ||
|
|
32616493b3 | ||
|
|
05183ffd0f | ||
|
|
e72ddc9439 | ||
|
|
32e3caecac | ||
|
|
df43389183 | ||
|
|
19b77809ec | ||
|
|
be05b827f3 | ||
|
|
5dfc6f822d | ||
|
|
c3e004da03 | ||
|
|
8bae73b6ea | ||
|
|
d1e19d3b63 | ||
|
|
ffca897ddf | ||
|
|
4277b6e262 | ||
|
|
506c4ce701 | ||
|
|
d251e58984 | ||
|
|
4a1213c081 | ||
|
|
8b7609255c | ||
|
|
ef78fe0653 | ||
|
|
70b3ccb422 | ||
|
|
81d6b367fe | ||
|
|
0a78ae60be | ||
|
|
a61830a860 | ||
|
|
86bae9ddc9 | ||
|
|
033780862a | ||
|
|
6094d8a74e | ||
|
|
356ca3d177 | ||
|
|
d69806faa9 | ||
|
|
ab67635dcb | ||
|
|
cee3d49458 | ||
|
|
5b53a7aef7 | ||
|
|
9b29665cc0 | ||
|
|
f447c87b45 | ||
|
|
e3eea45d86 | ||
|
|
f61a06ce0a | ||
|
|
539842aa99 | ||
|
|
5925f1d2aa | ||
|
|
61eb150825 | ||
|
|
cf95de4d27 | ||
|
|
fdad7ec1ba | ||
|
|
850efb4237 | ||
|
|
853cb3887f | ||
|
|
412f2c1664 | ||
|
|
2810a69bd4 | ||
|
|
5347f95f50 | ||
|
|
6b469f0621 | ||
|
|
0021562c93 | ||
|
|
f2bd2b0a59 | ||
|
|
647eb8bbf5 | ||
|
|
816d13ae3f | ||
|
|
578fea4a9c | ||
|
|
1a660d9a4a | ||
|
|
227ac6d9e3 | ||
|
|
bb57407733 | ||
|
|
13ddcce0a2 | ||
|
|
53767a78d1 | ||
|
|
5600e8a2ad | ||
|
|
c6ed52c592 | ||
|
|
3ad14e4adf | ||
|
|
8a22bdea5d | ||
|
|
6135a3c3e2 | ||
|
|
1e3c979303 | ||
|
|
d0228406b6 | ||
|
|
507a2237b7 | ||
|
|
c15c597d99 | ||
|
|
7c26cd3270 | ||
|
|
938af73059 | ||
|
|
1c047366d2 | ||
|
|
cb20f0cbb0 | ||
|
|
468251c84e | ||
|
|
ca86ae0c9a | ||
|
|
59221b0b4e | ||
|
|
d3e0640400 | ||
|
|
bcb72321f5 | ||
|
|
4060af715d | ||
|
|
2ec0237e83 | ||
|
|
c5593880f2 | ||
|
|
3673cbce4f | ||
|
|
1f6f7be4b2 | ||
|
|
580cce3506 | ||
|
|
36ba546fc6 | ||
|
|
7f37799cbe | ||
|
|
5570eeeff9 | ||
|
|
2b186ce6e0 | ||
|
|
72938fed69 | ||
|
|
d54c806e03 | ||
|
|
7eb3551485 | ||
|
|
57abe27895 | ||
|
|
a628a36082 | ||
|
|
0d3e04ff25 | ||
|
|
0c78a3f7b0 | ||
|
|
fb1f574c26 | ||
|
|
7f15c18fca | ||
|
|
e274650956 | ||
|
|
2a1db4a338 | ||
|
|
0b6ea9ec61 | ||
|
|
257a826d45 | ||
|
|
e9f48f5134 | ||
|
|
fcf04624d4 | ||
|
|
16218d6dc5 | ||
|
|
071f33e3cd | ||
|
|
6d15389da8 | ||
|
|
2e8530ec00 | ||
|
|
4edd1c5497 | ||
|
|
8e693b8b42 | ||
|
|
29376066e8 | ||
|
|
a44f3071bf | ||
|
|
b66047e084 | ||
|
|
f0ca916432 | ||
|
|
c88b4032ef | ||
|
|
6f5e99be6f | ||
|
|
fd4c37e9b3 | ||
|
|
7a8dab2d58 | ||
|
|
6f3dfad550 | ||
|
|
18fb0a13d7 | ||
|
|
e88f9ae03b | ||
|
|
30c010ad3f | ||
|
|
e84e70bdc6 | ||
|
|
1e6b6165ae | ||
|
|
c6d149d091 | ||
|
|
d55d8d78de | ||
|
|
eff59f7b5e | ||
|
|
a7a5437245 | ||
|
|
6671b9e55b | ||
|
|
2dde1cc589 | ||
|
|
17866c29ae | ||
|
|
8dc4e6dc2a | ||
|
|
1197f44262 | ||
|
|
a86ed1f801 | ||
|
|
e98d3423e4 | ||
|
|
95333d37c8 | ||
|
|
8bcf0c6498 | ||
|
|
340b92e32b | ||
|
|
6e68ab19f9 | ||
|
|
15fed32d92 | ||
|
|
897c754dd4 | ||
|
|
ec1e746a22 | ||
|
|
001f078ba9 | ||
|
|
b5321152fd | ||
|
|
66d15ea635 | ||
|
|
72177033d2 | ||
|
|
06b7072240 | ||
|
|
4700f35739 | ||
|
|
bbfa280e86 | ||
|
|
2669ba944d | ||
|
|
c8788dbfbe | ||
|
|
5e7c3b53f8 | ||
|
|
99348c2300 | ||
|
|
ab2b9797fd | ||
|
|
637653ea11 | ||
|
|
04cb6ba3d0 | ||
|
|
eb1cddd85a | ||
|
|
723b230093 | ||
|
|
7185fae491 | ||
|
|
0274cd6beb | ||
|
|
ad2ea0b807 | ||
|
|
c24999075d | ||
|
|
773bde14ab | ||
|
|
00b08318a5 | ||
|
|
39e5d8ccc2 | ||
|
|
e25622df4b | ||
|
|
ea5939c1b7 | ||
|
|
4734d04d4f | ||
|
|
493e47f7e6 | ||
|
|
ef2b32eb05 | ||
|
|
1da91d44e1 | ||
|
|
ebd7ab3e46 | ||
|
|
99bddfdf0a | ||
|
|
144f48c9a6 | ||
|
|
8d0d2ba07b | ||
|
|
a6ad334dc0 | ||
|
|
468ca30070 | ||
|
|
7b2d2d9338 | ||
|
|
0e08819cf3 | ||
|
|
1d3f7b49dc | ||
|
|
3566ec7012 | ||
|
|
f37a36efa4 | ||
|
|
2ea069cd8c | ||
|
|
08e111f6dc | ||
|
|
6339881684 | ||
|
|
e222538575 | ||
|
|
c4a67ce420 | ||
|
|
77e348ba62 | ||
|
|
bde39d8c37 | ||
|
|
ebb906c273 | ||
|
|
46b91bf8b0 | ||
|
|
7a432b38e9 | ||
|
|
9b6a201bbb | ||
|
|
a79d7c8417 | ||
|
|
7a6e0d651f | ||
|
|
7476498823 | ||
|
|
4c7b5d44a0 | ||
|
|
620bb54881 | ||
|
|
73be747cbe | ||
|
|
4d874451c9 | ||
|
|
3245d620c3 | ||
|
|
a59f80589a | ||
|
|
6b269c7559 | ||
|
|
53cadeab61 | ||
|
|
e90d388fdb | ||
|
|
219f059834 | ||
|
|
dc2dac66a3 | ||
|
|
e04ee666b8 | ||
|
|
7d27003bb2 | ||
|
|
b866c9dd08 | ||
|
|
d17236fe45 | ||
|
|
d2580ec87c | ||
|
|
7c10f414dc | ||
|
|
2a4717cb7f | ||
|
|
be9cb8a4da | ||
|
|
e887363910 | ||
|
|
a274159726 | ||
|
|
83384e0de4 | ||
|
|
748904b8ad | ||
|
|
3e72df8b1e | ||
|
|
2921563e9c | ||
|
|
01c1346696 | ||
|
|
796e0456ef | ||
|
|
eeb68497fe | ||
|
|
7eadb6acad | ||
|
|
9d588aa7e7 | ||
|
|
204b5f7f09 | ||
|
|
e0f53b63ce | ||
|
|
83f4dbe40e | ||
|
|
8d8ba68838 | ||
|
|
3f25940dec | ||
|
|
745773b207 | ||
|
|
35f5575595 | ||
|
|
e4746f8b32 | ||
|
|
7e0552efde | ||
|
|
6075b98634 | ||
|
|
ebe9f518d0 | ||
|
|
37ceddd11b | ||
|
|
d1d8b911b9 | ||
|
|
796e656328 | ||
|
|
8b869915e7 | ||
|
|
9b05243d61 | ||
|
|
81c24510a8 | ||
|
|
9e7fb4d21a | ||
|
|
7805f8a9b1 | ||
|
|
ae7f04578d | ||
|
|
ce814cffd1 | ||
|
|
703b310ef0 | ||
|
|
da6c4ad36a | ||
|
|
a8c849d38a | ||
|
|
d8d5e04a51 | ||
|
|
fa348cb98f | ||
|
|
f4ec2d8107 | ||
|
|
de39d828de | ||
|
|
25d3d0d0ba | ||
|
|
6f3b1000a7 | ||
|
|
1ebb8d8d14 | ||
|
|
4e4acdaecc | ||
|
|
f7fb03bf56 | ||
|
|
429aafc7ba | ||
|
|
acdfede2a8 | ||
|
|
8366c4c165 | ||
|
|
4c7260b043 | ||
|
|
c878f7dc25 | ||
|
|
aca21f6ef2 | ||
|
|
10c582bafb | ||
|
|
7b3bd26631 | ||
|
|
731f88da84 | ||
|
|
b7fb9a65b6 | ||
|
|
843c24b17a | ||
|
|
18dbbfc95a | ||
|
|
5b6e187b49 | ||
|
|
9025a9b88c | ||
|
|
07b2891671 | ||
|
|
c4a739bef6 | ||
|
|
1008c74cd7 | ||
|
|
60dc9d27bc | ||
|
|
9eb0f48a7a | ||
|
|
7b1fccdd06 | ||
|
|
64ae07b03b | ||
|
|
6ecbbd1f79 | ||
|
|
a1fb268764 | ||
|
|
868661edf0 | ||
|
|
9899e63d53 | ||
|
|
0b37b8b059 | ||
|
|
1b34ca822f | ||
|
|
a4e3a874ad | ||
|
|
8ec3df552a | ||
|
|
b4b1c9256b | ||
|
|
acee20d897 | ||
|
|
6ea3ebb72d | ||
|
|
55f23e9304 | ||
|
|
ad223a04f8 | ||
|
|
0b150ea475 | ||
|
|
167e9fbc6d | ||
|
|
77ea160cd9 | ||
|
|
f9204450f1 | ||
|
|
21ef76816f | ||
|
|
f166cfbac8 | ||
|
|
e5f64710f4 | ||
|
|
32a5062081 | ||
|
|
e6bc29281e | ||
|
|
617ee0afc0 | ||
|
|
1b47a1a994 | ||
|
|
5a87cfc25d | ||
|
|
00a178f7d3 | ||
|
|
2a2c82e73b | ||
|
|
bb882ada2c | ||
|
|
1d42e45d78 | ||
|
|
15c4a5c9ea | ||
|
|
f4435f9031 | ||
|
|
5a423c89a3 | ||
|
|
8b02154f5a | ||
|
|
97c454ea77 | ||
|
|
f07a6d03b5 | ||
|
|
1f18fb5446 | ||
|
|
92ee5b66ab | ||
|
|
fcc92c3e27 | ||
|
|
3e91b5a793 | ||
|
|
42e5cc3bef | ||
|
|
16c61a1919 | ||
|
|
c2b4b0490b | ||
|
|
a310a06e3c | ||
|
|
9228511527 | ||
|
|
bbc4174501 | ||
|
|
5f3196b74c | ||
|
|
725bd8029f | ||
|
|
479ab5df0e | ||
|
|
18c45ad30b | ||
|
|
6f32f098eb | ||
|
|
e8289d3912 | ||
|
|
472d9322ce | ||
|
|
0233ffafb6 | ||
|
|
468ee4756f | ||
|
|
abf9365bbe | ||
|
|
1e0789162f | ||
|
|
31f407f4e8 | ||
|
|
77330ffc50 | ||
|
|
6f132f3fed | ||
|
|
c193b4f07c | ||
|
|
c745b845c5 | ||
|
|
3b69e0dd25 | ||
|
|
8ec55ef394 | ||
|
|
ef5084036c | ||
|
|
7dd317e530 | ||
|
|
e5db3ed9b7 | ||
|
|
3a00dc5b5f | ||
|
|
2d848020fc | ||
|
|
70123d19fe | ||
|
|
ea3770260a | ||
|
|
eea1a80de6 | ||
|
|
4889775ae6 | ||
|
|
355effd93d | ||
|
|
f1583b6e0c | ||
|
|
347566c311 | ||
|
|
1f73572dd3 | ||
|
|
2bfb83c4cd | ||
|
|
ceed1c4962 | ||
|
|
2b5b9d3599 | ||
|
|
96e3709b7b | ||
|
|
239fc2f6f8 | ||
|
|
4c77e5cdd2 | ||
|
|
974f8f692c | ||
|
|
e97d0b9a69 | ||
|
|
b0b0a75c87 | ||
|
|
abcacf8c74 | ||
|
|
290428b981 | ||
|
|
37d1541d6b | ||
|
|
1500ce7490 | ||
|
|
a48529872d | ||
|
|
31cffa68c5 | ||
|
|
6909d1e527 | ||
|
|
972235bfba | ||
|
|
6db560fd2c | ||
|
|
1a64d8aec9 | ||
|
|
1e1fb32558 | ||
|
|
008eb5ba4a | ||
|
|
f46e0acc89 | ||
|
|
b615ef5810 | ||
|
|
d773279de8 | ||
|
|
56d721651a | ||
|
|
ee17abff92 | ||
|
|
952bb1a2eb | ||
|
|
c287813e00 | ||
|
|
eab4fd80d7 | ||
|
|
5c8f8869d4 | ||
|
|
4954dfe107 | ||
|
|
4eb8094fb8 | ||
|
|
1dd2423a0b | ||
|
|
6fcf989c62 | ||
|
|
67d1a4f643 | ||
|
|
3df9433baf | ||
|
|
b12d568147 | ||
|
|
8691e035a0 | ||
|
|
2683043762 | ||
|
|
be5aa59f61 | ||
|
|
9398dfb7cf | ||
|
|
ff6d2b30e4 | ||
|
|
29bb999a32 | ||
|
|
9320507e26 | ||
|
|
181fc4fa0a | ||
|
|
4f3dd4b662 | ||
|
|
7b09de99ea | ||
|
|
f9f0da18e1 | ||
|
|
ba0fdb9478 | ||
|
|
8c684bca22 | ||
|
|
1266a75549 | ||
|
|
3871d5aed7 | ||
|
|
25d555126d | ||
|
|
f529d15d7a | ||
|
|
9eda30fc71 | ||
|
|
dfd6424d9c | ||
|
|
fdc961f2de | ||
|
|
a6d4000d24 | ||
|
|
e1024e59c3 | ||
|
|
990164802d | ||
|
|
34f18fbdb3 | ||
|
|
17e24bb038 | ||
|
|
df7e2b7734 | ||
|
|
1ddef06bd2 | ||
|
|
18bd910bf0 | ||
|
|
0db44f6e33 | ||
|
|
7aac3d38f0 | ||
|
|
e406b6f780 | ||
|
|
2dad9666a9 | ||
|
|
063abf1688 | ||
|
|
a86f8e9a22 | ||
|
|
0ce6d4fe92 | ||
|
|
886f6c721c | ||
|
|
9d7d089279 | ||
|
|
0bd624dfa9 | ||
|
|
8c29760d93 | ||
|
|
9b893d841d | ||
|
|
e8c0163153 | ||
|
|
256568d966 | ||
|
|
34bed47a52 | ||
|
|
9b0996fade | ||
|
|
7c1028df5d | ||
|
|
eaea60e0cb | ||
|
|
efab05dcfc | ||
|
|
d79f77f7e0 | ||
|
|
f0d459d490 | ||
|
|
e269c073ac | ||
|
|
85d5609144 | ||
|
|
671c593db1 | ||
|
|
8b14c7a2cb | ||
|
|
23862419eb | ||
|
|
43d54db4dd | ||
|
|
b8b0060440 | ||
|
|
64d79ceb30 | ||
|
|
25f7b44d48 | ||
|
|
ada7e628da | ||
|
|
76f2338c3d | ||
|
|
a0ed8036c0 | ||
|
|
863ce65b10 | ||
|
|
85190b16cb | ||
|
|
abc6fd8b2a | ||
|
|
b6f603154e | ||
|
|
90cb9d3de1 | ||
|
|
d47c9a2e29 | ||
|
|
b7aea96ca0 | ||
|
|
a5879a4407 | ||
|
|
0452c69771 | ||
|
|
4eb9dff45e | ||
|
|
b7c1e88b59 | ||
|
|
23814330d9 | ||
|
|
8bc75aacea | ||
|
|
17576d223a | ||
|
|
90e8f0ca63 | ||
|
|
adc4a811b7 | ||
|
|
7c80233f26 | ||
|
|
f05ae2de35 | ||
|
|
cf9da556a8 | ||
|
|
32a142bf79 | ||
|
|
2680d41a3d | ||
|
|
1550fc4398 | ||
|
|
f2a85f3b7e | ||
|
|
49c2b4c196 | ||
|
|
a8281e174e | ||
|
|
d7f5c8bd55 | ||
|
|
b3337df88b | ||
|
|
d760616e55 | ||
|
|
914a4d32b4 | ||
|
|
148f53e21e | ||
|
|
5e7fa0f964 | ||
|
|
0973ceb9d2 | ||
|
|
5214bfe8cb | ||
|
|
187aaafddc | ||
|
|
208cb405ca | ||
|
|
9b9d267cd4 | ||
|
|
6f90a27f9f | ||
|
|
4e1dddc06d | ||
|
|
1f504d6f23 | ||
|
|
2db1fd813f | ||
|
|
f39383a3d8 | ||
|
|
b593bdbf1b | ||
|
|
4de93ba3c0 | ||
|
|
2eb7a91987 | ||
|
|
94e4264c2a | ||
|
|
e54d28f157 | ||
|
|
6111c8bde0 | ||
|
|
3bacdfd4fc | ||
|
|
65db645ff9 | ||
|
|
de2e2c45a5 | ||
|
|
9b655e18e3 | ||
|
|
fb4b9b5f76 | ||
|
|
820b39840a | ||
|
|
4b1052eb70 | ||
|
|
c0eb3972a7 | ||
|
|
66ba8d56b7 | ||
|
|
a73baf32f1 | ||
|
|
333cf0a2f0 | ||
|
|
8347d8700a | ||
|
|
09af0e2448 | ||
|
|
001914764a | ||
|
|
6938dd6267 | ||
|
|
941028ba6f | ||
|
|
4ca7ed9f8c | ||
|
|
03d99887c5 | ||
|
|
293e2ff5e3 | ||
|
|
55d242fa08 | ||
|
|
7e9fba2d96 | ||
|
|
175652f23b | ||
|
|
3329e0c4a1 | ||
|
|
3c5ed2c885 | ||
|
|
c4af93c363 | ||
|
|
e2685c4503 | ||
|
|
48b1d3fff8 | ||
|
|
6c4920949d | ||
|
|
69447b75af | ||
|
|
e1104570a9 | ||
|
|
2c23678fb9 | ||
|
|
613070d39f | ||
|
|
6deae64f45 | ||
|
|
aced2b124c | ||
|
|
e6b08de2e8 | ||
|
|
d2d02d0749 | ||
|
|
28d27801b2 | ||
|
|
be340dd275 | ||
|
|
58090fb3de | ||
|
|
ae33c6cf18 | ||
|
|
f8cd6afbf8 | ||
|
|
6fce06906d | ||
|
|
84694a8bbd | ||
|
|
1639e68424 | ||
|
|
ff48fe8b49 | ||
|
|
efe06267ec | ||
|
|
53647ea5a8 | ||
|
|
7742de5af4 | ||
|
|
724a260f71 | ||
|
|
cf75e40332 | ||
|
|
3c67df263c | ||
|
|
c4ae72c3c1 | ||
|
|
f6925fc5b8 | ||
|
|
18be9655d6 | ||
|
|
6235b6123e | ||
|
|
b3555385e6 | ||
|
|
c9fbdb322b | ||
|
|
94bac7d8db | ||
|
|
f38119be96 | ||
|
|
bc1d2ba839 | ||
|
|
18f5b70b1f | ||
|
|
7df9b07305 | ||
|
|
cf01c1fd1f | ||
|
|
2ce6fe420b | ||
|
|
f55381d689 | ||
|
|
c4084c4f97 | ||
|
|
58b720b004 | ||
|
|
f6d0c1f05e | ||
|
|
f4620be859 | ||
|
|
e2b3a98690 | ||
|
|
4cd391d5ef | ||
|
|
01c37c34dd | ||
|
|
f4827cde0e | ||
|
|
3e722295b0 | ||
|
|
0d2eab3ad4 | ||
|
|
9e1bc631cf | ||
|
|
ca9fbe2f11 | ||
|
|
2e28fad102 | ||
|
|
69760200dd | ||
|
|
f945ee1288 | ||
|
|
5e3486c481 | ||
|
|
c15a943cf4 | ||
|
|
b8cb29c66c | ||
|
|
003badcb5a | ||
|
|
5b2493fa68 | ||
|
|
bc342b9b33 | ||
|
|
314615bfef | ||
|
|
44e82217c1 | ||
|
|
cbf364f24f | ||
|
|
44dfcb927b | ||
|
|
12f615c6da | ||
|
|
ed6fc4d848 | ||
|
|
cd515993f5 | ||
|
|
a918eaac3f | ||
|
|
9f63e2d39a | ||
|
|
36248ff046 | ||
|
|
a88f5113e0 | ||
|
|
06fb89fae2 | ||
|
|
733531356f | ||
|
|
3f8fb30066 | ||
|
|
1a4a2d2b30 | ||
|
|
eb6d3b3f8d | ||
|
|
e8e3363d06 | ||
|
|
dd55ad61f4 | ||
|
|
2464bfd70b | ||
|
|
09bc36bb13 | ||
|
|
39da89b556 | ||
|
|
c23de4b3b0 | ||
|
|
7c79d7f5d7 | ||
|
|
710507da51 | ||
|
|
10e95bf1b1 | ||
|
|
66a893c84e | ||
|
|
dd6392e380 | ||
|
|
ea0a0c7c5a | ||
|
|
b8f7ba62c7 | ||
|
|
25b318ba00 | ||
|
|
9add51b59d | ||
|
|
535a0504d8 | ||
|
|
365c49d6d2 | ||
|
|
b70bea48f2 | ||
|
|
996f8644c4 | ||
|
|
b3882ec6e3 | ||
|
|
f87d447397 | ||
|
|
9d8570d0d2 | ||
|
|
796755dad8 | ||
|
|
9387753995 | ||
|
|
618d36dc07 | ||
|
|
f11b0be483 | ||
|
|
1b8b15b136 | ||
|
|
bb63673cce | ||
|
|
6770ad68d5 | ||
|
|
bfe90c58d1 | ||
|
|
f1cbeb3c29 | ||
|
|
554ab4ea16 | ||
|
|
a2becac2e6 | ||
|
|
67a651f5e9 | ||
|
|
ac8efe19d8 | ||
|
|
ffd65d5afa | ||
|
|
e86677178f | ||
|
|
3c49a3341a | ||
|
|
3433b2a73e | ||
|
|
730988e7b7 | ||
|
|
2a3b89e596 | ||
|
|
f1a31bf58c | ||
|
|
f1b62a9056 | ||
|
|
dd943d24c8 | ||
|
|
801320a3f3 | ||
|
|
222ed2debd | ||
|
|
7aab782c5f | ||
|
|
3836f2f353 | ||
|
|
5bfaa9a5db | ||
|
|
95581771d6 | ||
|
|
db5e3f2479 | ||
|
|
b5a9631bcc | ||
|
|
4a2d62ece0 | ||
|
|
c3836decee | ||
|
|
f7a030c895 | ||
|
|
f171a692d3 | ||
|
|
df5e73192b | ||
|
|
fc1447d614 | ||
|
|
77fd206b06 | ||
|
|
23bdc03490 | ||
|
|
9fe4de5709 | ||
|
|
54fd601809 | ||
|
|
63d54e6570 | ||
|
|
71d027a966 | ||
|
|
8b63aa2fe6 | ||
|
|
5a35842c28 | ||
|
|
d6a1ae3b3a | ||
|
|
be5f4cb562 | ||
|
|
d8b5464833 | ||
|
|
5e7bbcd3bc | ||
|
|
5383e53c4d | ||
|
|
5b6fc713d6 | ||
|
|
272be025e1 | ||
|
|
e4ab250729 | ||
|
|
4dcca9d5af | ||
|
|
1988a08631 | ||
|
|
34de0e569f | ||
|
|
343d0fa09d | ||
|
|
8eb6686103 | ||
|
|
9e9687b5b8 | ||
|
|
ef888d1afe | ||
|
|
0e70e1a37a | ||
|
|
06aaceb673 | ||
|
|
703a4b7858 | ||
|
|
32ba2ba83d | ||
|
|
272b03ed92 | ||
|
|
ecf19214ee | ||
|
|
f3eb0c497f | ||
|
|
c1f29a7565 | ||
|
|
fb745b9108 | ||
|
|
9410bf40d3 | ||
|
|
c7a695cb04 | ||
|
|
b991d5cab6 | ||
|
|
42fd318321 | ||
|
|
903aeec383 | ||
|
|
8768fe4dcf | ||
|
|
d8ba2ceed4 | ||
|
|
ef8a1bcf47 | ||
|
|
2b1469e02e | ||
|
|
83ea91586b | ||
|
|
dbb86d25e1 | ||
|
|
794c74e514 | ||
|
|
fbcdaa77e3 | ||
|
|
dbdc04c45e | ||
|
|
a4bb22280f | ||
|
|
c0e1bbbfb6 | ||
|
|
196b9dc771 | ||
|
|
09578b4e46 | ||
|
|
4d88dadf8c | ||
|
|
d4fda5847d | ||
|
|
b1ea7d6cbc | ||
|
|
4e7632949d | ||
|
|
26a8bd147b | ||
|
|
dd726fac02 | ||
|
|
3a3ecc7775 | ||
|
|
6665d630ec | ||
|
|
7706d7471a | ||
|
|
ed87d6b268 | ||
|
|
ed51c8b318 | ||
|
|
6ffbb7b1ed | ||
|
|
06764db118 | ||
|
|
4864fa3f2d | ||
|
|
2d25b6a1f4 | ||
|
|
be76b3d105 | ||
|
|
81cbeb4b24 | ||
|
|
f0b658ba14 | ||
|
|
295836fc7e | ||
|
|
54e9858148 | ||
|
|
b68f015825 | ||
|
|
87ae26ede3 | ||
|
|
49615f81b4 | ||
|
|
87ce5140fa | ||
|
|
3ba9fb375c | ||
|
|
f4bd20361a | ||
|
|
54f8a17aac | ||
|
|
7a1e5026c4 | ||
|
|
323161c6de | ||
|
|
1ac4890893 | ||
|
|
e9c88fecc5 | ||
|
|
33deaaefac | ||
|
|
bb6438ebe4 | ||
|
|
d42af74afa | ||
|
|
ea1f2f4ad4 | ||
|
|
95b45651bb | ||
|
|
0d5730d33e | ||
|
|
0625a35ddf | ||
|
|
2d3271ee13 | ||
|
|
6da2e80027 | ||
|
|
439edbf85c | ||
|
|
e0237a0b86 | ||
|
|
e1845ba603 | ||
|
|
1cf757d401 | ||
|
|
14985b1727 | ||
|
|
6b2788be57 | ||
|
|
ac888f4cb2 | ||
|
|
df06cfc4c5 | ||
|
|
dcba3a681c | ||
|
|
7fd49c22a8 | ||
|
|
314287a6d9 | ||
|
|
f5e7b8f229 | ||
|
|
de84db070e | ||
|
|
c1d5a5cd98 | ||
|
|
160a04c3c7 | ||
|
|
23bfc30c57 | ||
|
|
d9329bffd1 | ||
|
|
bafc1df988 | ||
|
|
30b8835919 | ||
|
|
44b19e75f6 | ||
|
|
2a558ad11d | ||
|
|
123d8972e1 | ||
|
|
e550a8ea27 | ||
|
|
8b29460fed | ||
|
|
e380d63c57 | ||
|
|
89b4f2c4d4 | ||
|
|
6c2f63f738 | ||
|
|
77c612f0f5 | ||
|
|
2d06c01192 | ||
|
|
d13c19f05f | ||
|
|
3d2ba05c77 | ||
|
|
6898b9d9a4 | ||
|
|
a65aaa6b83 | ||
|
|
b3136c20c4 | ||
|
|
0ae3dfd9cc | ||
|
|
0370fa6c00 | ||
|
|
7ab323b00c | ||
|
|
541eb70b9c | ||
|
|
e53e5ca20e | ||
|
|
609bf64856 | ||
|
|
a9fafe91a5 | ||
|
|
0466b320dd | ||
|
|
5b74d22d0a | ||
|
|
4152c7f956 | ||
|
|
d5f603303d | ||
|
|
fc9c073a60 | ||
|
|
9a0c2c40bd | ||
|
|
d0fc9fda71 | ||
|
|
df9823988e | ||
|
|
eeb09c074c | ||
|
|
af0928e2bd | ||
|
|
d9cf4de3f7 | ||
|
|
2d6dd4b3be | ||
|
|
160312393a | ||
|
|
e3ff9f9c86 | ||
|
|
95570d796d | ||
|
|
cd0d58a915 | ||
|
|
2f1007c725 | ||
|
|
b53d5d8c00 | ||
|
|
3c4a4e5384 | ||
|
|
0e5f85db95 | ||
|
|
ad3364671d | ||
|
|
3add24b8aa | ||
|
|
e0f02d4080 | ||
|
|
00c4c10472 | ||
|
|
a2b8cc9dc2 | ||
|
|
3465002cbb | ||
|
|
631cb73305 | ||
|
|
b3b6384bef | ||
|
|
941ca575fb | ||
|
|
2d65c3595d | ||
|
|
b3812d913a | ||
|
|
adcc420c81 | ||
|
|
b97ad99bb4 | ||
|
|
e93a2850d6 | ||
|
|
411d0691fa | ||
|
|
c843e77183 | ||
|
|
093d6e5336 | ||
|
|
8a3c752d42 | ||
|
|
b4e073cde7 | ||
|
|
814efbf8df | ||
|
|
11e048abb1 | ||
|
|
34e7855af6 | ||
|
|
de54dc27ad | ||
|
|
790133978d | ||
|
|
b914d67d9d | ||
|
|
518eb97e3a | ||
|
|
f41549ccf1 | ||
|
|
b69e477ecd | ||
|
|
0062ff9cfa | ||
|
|
f8de72f59f | ||
|
|
7317737e90 | ||
|
|
5b8eda4805 | ||
|
|
886a949a00 | ||
|
|
92e13dafe5 | ||
|
|
c9be812330 | ||
|
|
59e7ebabfa | ||
|
|
1afc48fce3 | ||
|
|
a1e4ef9e8e | ||
|
|
5ada0ae2c7 | ||
|
|
a5312c1341 | ||
|
|
150e156d26 | ||
|
|
6d38615ea8 | ||
|
|
011cc7d337 | ||
|
|
b747d09836 | ||
|
|
4b7311bafd | ||
|
|
eeba9c0a5f | ||
|
|
11d9a037f7 | ||
|
|
883e4fcd7c | ||
|
|
2017e6a3e3 | ||
|
|
bccfe500b3 | ||
|
|
5846fbabce | ||
|
|
52e89c1d1c | ||
|
|
1605e50cef | ||
|
|
2215ce58a4 | ||
|
|
a13e6b69e3 | ||
|
|
1d6370e11c | ||
|
|
bd34c7ede3 | ||
|
|
bc8954fbba | ||
|
|
9cf0bc6c82 | ||
|
|
71b32fe641 | ||
|
|
530f745e44 | ||
|
|
a5a2313851 | ||
|
|
4e98c2e7f6 | ||
|
|
14486782dc | ||
|
|
31814b70da | ||
|
|
408e819d32 | ||
|
|
622676f9bc | ||
|
|
5b631e0387 | ||
|
|
06d54ef77e | ||
|
|
6c5ef567ed | ||
|
|
f86b40302d | ||
|
|
273c287fbf | ||
|
|
6cb16be5df | ||
|
|
a4feb3fc09 | ||
|
|
ba6c7de35a | ||
|
|
79e2bb382f | ||
|
|
0090256ded | ||
|
|
00ce077758 | ||
|
|
a801d0994f | ||
|
|
628575dc5f | ||
|
|
0a22f21410 | ||
|
|
97ff9e9c5b | ||
|
|
8b3a09306b | ||
|
|
7766fd13fd | ||
|
|
c79997ebe3 | ||
|
|
0fd1e2fcd9 | ||
|
|
99442b6e04 | ||
|
|
b8a35e9e4a | ||
|
|
e833d415e3 | ||
|
|
8030312924 | ||
|
|
a84b54f940 | ||
|
|
c66c81294e | ||
|
|
bfdc215c65 | ||
|
|
e10c7beedb | ||
|
|
18c3286364 | ||
|
|
8d2ec30818 | ||
|
|
e5ffddfc6b | ||
|
|
59fc1e4b5f | ||
|
|
7ead581953 | ||
|
|
b860980df4 | ||
|
|
97a366d62e | ||
|
|
9ad68097d0 | ||
|
|
331999fb95 | ||
|
|
6519d7051d | ||
|
|
552d585fca | ||
|
|
24c24d6c72 | ||
|
|
b7f50c3e12 | ||
|
|
aed1687a45 | ||
|
|
daa427dc15 | ||
|
|
e9d4303fdb | ||
|
|
5485e994ee | ||
|
|
87228673b4 | ||
|
|
d306513319 | ||
|
|
e08480f345 | ||
|
|
13c9096417 | ||
|
|
d3d65c8e3a | ||
|
|
12ac5ef781 | ||
|
|
adef9a8acf | ||
|
|
5ef407d15f | ||
|
|
970b636eb4 | ||
|
|
fcf9131aae | ||
|
|
7fd27fac45 | ||
|
|
6e17af91fb | ||
|
|
68555573ad | ||
|
|
fb9905a89e | ||
|
|
1e7504dc5a | ||
|
|
d7af019511 | ||
|
|
04bb070afa | ||
|
|
e693d80857 | ||
|
|
45ae05f1b5 | ||
|
|
05a83beb44 | ||
|
|
8a1a42e83b | ||
|
|
1b3f3cedb3 | ||
|
|
f815ae5973 | ||
|
|
fb7035bf22 | ||
|
|
02bcbc3221 | ||
|
|
f0a51d4ab4 | ||
|
|
24fe8fe9a0 | ||
|
|
244e95d959 | ||
|
|
ad72c64e32 | ||
|
|
767ac6a51b | ||
|
|
3a6f87659a | ||
|
|
b7ac16c7d9 | ||
|
|
f9f84cbd89 | ||
|
|
701c87eefa | ||
|
|
5379cf0544 | ||
|
|
6b5c37f17f | ||
|
|
50b2fad180 | ||
|
|
b9cb65b24e | ||
|
|
d7574973e9 | ||
|
|
ff48c93d59 | ||
|
|
d2e6700dd1 | ||
|
|
05b8c3f35f | ||
|
|
70b643e7ba | ||
|
|
dce973a519 | ||
|
|
eb2f75579a | ||
|
|
773316ce4f | ||
|
|
5fd7ae33b4 | ||
|
|
13a065f2dc | ||
|
|
1a8ff81087 | ||
|
|
65637fce40 | ||
|
|
b11fa7a28e | ||
|
|
45408caf33 | ||
|
|
963ee4dbab | ||
|
|
3b46d5a440 | ||
|
|
212fddd8e1 | ||
|
|
433485470e | ||
|
|
e160a1f794 | ||
|
|
7911b7e637 | ||
|
|
7fc5a77e7e | ||
|
|
f0fb55640e | ||
|
|
d5685f2255 | ||
|
|
a732233db6 | ||
|
|
a5918c29ee | ||
|
|
3b719803bb | ||
|
|
d77463c9f1 | ||
|
|
2d8fd9bedf | ||
|
|
b17a667a9d | ||
|
|
b7287a070b | ||
|
|
fbb5c8cdd6 | ||
|
|
baaf2815e4 | ||
|
|
d8b5549fd9 | ||
|
|
6de03f2bf0 | ||
|
|
caf7c55069 | ||
|
|
7d499ffba1 | ||
|
|
4abf6b2f5c | ||
|
|
a842b06301 | ||
|
|
0bca4925d7 | ||
|
|
ae3953cbec | ||
|
|
a99667c54c | ||
|
|
465963a8c2 | ||
|
|
b9b4762faf | ||
|
|
a56b128a4b | ||
|
|
d43cc089fd | ||
|
|
771513d287 | ||
|
|
c387678217 | ||
|
|
847368718b | ||
|
|
458b3daac3 | ||
|
|
3ea5278b12 | ||
|
|
20b9748a8c | ||
|
|
79487adbec | ||
|
|
89f3fca6b1 | ||
|
|
f290b2bf5a | ||
|
|
04e7d13043 | ||
|
|
e41218c46b | ||
|
|
8562fbdbbe | ||
|
|
c841d7a32b | ||
|
|
8827ae4d2c | ||
|
|
2e1029e157 | ||
|
|
a7dc0c2d55 | ||
|
|
7f1749d853 | ||
|
|
b4a34d58db | ||
|
|
4a50fcab2c | ||
|
|
c9ef089199 | ||
|
|
94ecf9a081 | ||
|
|
67aaa9a655 | ||
|
|
823f5640f7 | ||
|
|
45d1c63895 | ||
|
|
d3b6781bb8 | ||
|
|
21d1f69d6d | ||
|
|
1b9f5989ef | ||
|
|
348e46ff3b | ||
|
|
7918e3a1aa | ||
|
|
d6d8c7830c | ||
|
|
f53a0f0d07 | ||
|
|
17edd1c3d4 | ||
|
|
b0685c153a | ||
|
|
a5ca20ee4c | ||
|
|
03685db2fc | ||
|
|
68ed738dcd | ||
|
|
5293d17e32 | ||
|
|
f2e4b69466 | ||
|
|
ec8b00042b | ||
|
|
08db1d59e5 | ||
|
|
7c79d421e8 | ||
|
|
ade5e38fa5 | ||
|
|
7385aa09a8 | ||
|
|
185a5fad88 | ||
|
|
a1e2477d14 | ||
|
|
a1200a5fff | ||
|
|
91a0257c8f | ||
|
|
53ffc82fe2 | ||
|
|
801267df18 | ||
|
|
6e73e0b395 | ||
|
|
7aa8a5c368 | ||
|
|
3ecbbea7cb | ||
|
|
77cd3182f1 | ||
|
|
c7ccf9bab8 | ||
|
|
06e70abb86 | ||
|
|
88c86e88b0 | ||
|
|
2d6cf48532 | ||
|
|
19e152a54b | ||
|
|
2898bead66 | ||
|
|
381c329441 | ||
|
|
a3de3705f7 | ||
|
|
dc3dc6b77f | ||
|
|
e028a63f30 | ||
|
|
d196f8b4b2 | ||
|
|
4274827dbe | ||
|
|
7a30f4a7d2 | ||
|
|
d0c03a0211 | ||
|
|
787b136d13 | ||
|
|
08412d6108 | ||
|
|
d8f7db4715 | ||
|
|
bff238774e | ||
|
|
d2aaa6f691 | ||
|
|
c900ef036c | ||
|
|
b2164ce5fc | ||
|
|
d088d432c5 | ||
|
|
c9fafbe198 | ||
|
|
2d909b0514 | ||
|
|
c2a012553d | ||
|
|
085f0266ac | ||
|
|
7ede2daa3c | ||
|
|
713c53d170 | ||
|
|
110b3a6a8f | ||
|
|
e12e6dd7a7 | ||
|
|
e183fc6118 | ||
|
|
dd57e246b8 | ||
|
|
f4a4172369 | ||
|
|
b96d1714b5 | ||
|
|
dbd809b040 | ||
|
|
ff4e6b139d | ||
|
|
af098aaac8 | ||
|
|
5d7e62c736 | ||
|
|
e2ead011f5 | ||
|
|
a067c950e1 | ||
|
|
3369618cfa | ||
|
|
4e9b6520e5 | ||
|
|
a074203fae | ||
|
|
42092e3f28 | ||
|
|
c07b34e298 | ||
|
|
ef5f181328 | ||
|
|
a7ea2fcf92 | ||
|
|
e81730715c | ||
|
|
c09f2ad482 | ||
|
|
84aef8b5b5 | ||
|
|
8fac3e8221 | ||
|
|
5874ed781d | ||
|
|
d8f29bd7a7 | ||
|
|
8120b6aaaa | ||
|
|
13a0d1de70 | ||
|
|
20e828be51 | ||
|
|
ccd82fb8b8 | ||
|
|
0711650ff8 | ||
|
|
4194ac2226 | ||
|
|
248e2d7ee0 | ||
|
|
1daa654051 | ||
|
|
d751434979 | ||
|
|
8cc21920b7 | ||
|
|
248212588d | ||
|
|
13c0fdef08 | ||
|
|
dfc27b2480 | ||
|
|
b2d78d380b | ||
|
|
faa6cb5c7d | ||
|
|
452977abdf | ||
|
|
93570b2f59 | ||
|
|
073f5c2c8c | ||
|
|
1b4313f847 | ||
|
|
07cead7e99 | ||
|
|
f128751aba | ||
|
|
9516d9da17 | ||
|
|
e9aafc2a56 | ||
|
|
a91a6575e0 | ||
|
|
5bd4093dfb | ||
|
|
f9890e2016 | ||
|
|
00529fe134 | ||
|
|
5e9dce7d39 | ||
|
|
734680b9f0 | ||
|
|
d0b5345252 | ||
|
|
c2b4a44a59 | ||
|
|
38c79bbc11 | ||
|
|
0d7028a36c | ||
|
|
b63f687491 | ||
|
|
952b636569 | ||
|
|
e14ba48244 | ||
|
|
720c8c31ec | ||
|
|
4e8407ed8f | ||
|
|
26c5f69161 | ||
|
|
078ae15794 | ||
|
|
16ae90dc9f | ||
|
|
3de5afc68e | ||
|
|
a7e8f5087e | ||
|
|
5cc60ed760 | ||
|
|
2e6e75cd4e | ||
|
|
9f3b35634a | ||
|
|
c24dfc63dc | ||
|
|
c029929850 | ||
|
|
d9100913d5 | ||
|
|
a7fbe05a73 | ||
|
|
e03d970bc2 | ||
|
|
fe4516ea23 | ||
|
|
039b47b872 | ||
|
|
0a57a8a7f3 | ||
|
|
8c823f3a2d | ||
|
|
33f3a4f455 | ||
|
|
2ecf8044d7 | ||
|
|
c9a1eb55b5 | ||
|
|
76bb1fd61e | ||
|
|
1275b26ba0 | ||
|
|
a470a4af9b | ||
|
|
487c9ebbd4 | ||
|
|
c796e2ae3c | ||
|
|
746cab92f0 | ||
|
|
33266a96ff | ||
|
|
2816889d8d | ||
|
|
58e177b3e4 | ||
|
|
5cfd8bbb56 | ||
|
|
e6fe6fd645 | ||
|
|
63e167b38e | ||
|
|
abe77c4783 | ||
|
|
72af51fe9d | ||
|
|
36b4134838 | ||
|
|
cf6ee26fdb | ||
|
|
858111e623 | ||
|
|
367e625804 | ||
|
|
ae4d9c7f80 | ||
|
|
6c6ee41346 | ||
|
|
9ef7688f9e | ||
|
|
7d6e226c2b | ||
|
|
17d1346a8a | ||
|
|
59e0c10c42 | ||
|
|
0d29e66092 | ||
|
|
caa000f447 | ||
|
|
267e114354 | ||
|
|
b5375396d2 | ||
|
|
e34f666b70 | ||
|
|
19334b4f96 | ||
|
|
1fa609e539 | ||
|
|
d9ce25a721 | ||
|
|
5b8fc25da6 | ||
|
|
c70968dcf1 | ||
|
|
77147510fb | ||
|
|
7092577482 | ||
|
|
3e70050056 | ||
|
|
1f23c814e5 | ||
|
|
145e0a0b7b | ||
|
|
e68e787e7a | ||
|
|
903308d285 | ||
|
|
a4d8388b2e | ||
|
|
3e83f9f956 | ||
|
|
4063221313 | ||
|
|
c3c8d80919 | ||
|
|
3ae71c73c4 | ||
|
|
b3db8c9549 | ||
|
|
596eb4a0f9 | ||
|
|
6b0381b903 | ||
|
|
049c8f70cd | ||
|
|
353bf69550 | ||
|
|
cdb989ede3 | ||
|
|
74079e4238 | ||
|
|
c89746214c | ||
|
|
1d97db3ef9 | ||
|
|
b2a5ff5f9d | ||
|
|
f47ef2b5ea | ||
|
|
bd7ec3b692 | ||
|
|
52895e7b6b | ||
|
|
a6a82c6477 | ||
|
|
af66ed94b2 | ||
|
|
e43fdf5ef9 | ||
|
|
3984fc075f | ||
|
|
ee2a159374 | ||
|
|
57c9b29ba3 | ||
|
|
927ea72337 | ||
|
|
123bdbd13b | ||
|
|
0baa5a2f04 | ||
|
|
7d98a70028 | ||
|
|
73b72ab01c | ||
|
|
e8cf71f41c | ||
|
|
5f2d2a64d2 | ||
|
|
27bf3901de | ||
|
|
16d4fa03a5 | ||
|
|
bef579ec26 | ||
|
|
0cff10e02e | ||
|
|
4c6f7238dd | ||
|
|
583f1476d6 | ||
|
|
b42bef32fd | ||
|
|
8bb85ccf19 | ||
|
|
3d88c2a5fa | ||
|
|
e350acaf08 | ||
|
|
172f70bef9 | ||
|
|
9d25c0bf8a | ||
|
|
75b377aab3 | ||
|
|
3706f30b44 | ||
|
|
a9697a61ad | ||
|
|
e16a2d7cb6 | ||
|
|
f106e2945b | ||
|
|
1ad7deddb1 | ||
|
|
7b81e98581 | ||
|
|
0cae58ce8e | ||
|
|
7231150115 | ||
|
|
071986a4c9 | ||
|
|
39e7d43f10 | ||
|
|
aa1b17ae66 | ||
|
|
92bae88355 | ||
|
|
067eaf363e | ||
|
|
00efc266d9 | ||
|
|
0b014185e3 | ||
|
|
3eee7378de | ||
|
|
c31428f6bc | ||
|
|
163e561cf9 | ||
|
|
a32ded2829 | ||
|
|
a5b7517fbd | ||
|
|
176d57b35a | ||
|
|
927a1d58e2 | ||
|
|
bbd0df08d3 | ||
|
|
9e57195e14 | ||
|
|
05ab54c30d | ||
|
|
e3e2028153 | ||
|
|
883bcc735d | ||
|
|
158727e2f2 | ||
|
|
899f69d120 | ||
|
|
b575046c05 | ||
|
|
b5c60d2be2 | ||
|
|
631dfee763 | ||
|
|
d7f610113e | ||
|
|
e0e4f6db2b | ||
|
|
c27a26c0aa | ||
|
|
3dcd2468a2 | ||
|
|
ea43b28f74 | ||
|
|
a3e2a085b6 | ||
|
|
635d51b60d | ||
|
|
95eb1c0d95 | ||
|
|
8aca43a7e6 | ||
|
|
f6afe59788 | ||
|
|
8e13161f64 | ||
|
|
97437b8af3 | ||
|
|
9a938093e2 | ||
|
|
3edcc9f9fd | ||
|
|
55db408720 | ||
|
|
caef874814 | ||
|
|
1622639eca | ||
|
|
4df89f4217 | ||
|
|
079b98ed3f | ||
|
|
a0526d2c9c | ||
|
|
169b1cbd32 | ||
|
|
8968081e77 | ||
|
|
93ba7510e1 | ||
|
|
579bb743bb | ||
|
|
9e5e9ea612 | ||
|
|
2241a13cba | ||
|
|
9c1fb0cb92 | ||
|
|
4904514257 | ||
|
|
6d829c26a1 | ||
|
|
c05467fb92 | ||
|
|
d47f7f3348 | ||
|
|
5931a84651 | ||
|
|
5771783d11 | ||
|
|
aae07a60bd | ||
|
|
462d418ee0 | ||
|
|
ce63c2e1db | ||
|
|
52baf8cbe5 | ||
|
|
1779b9ee1a | ||
|
|
eae169236c | ||
|
|
ce0efba0d2 | ||
|
|
87c7ac3970 | ||
|
|
0f8d196a52 | ||
|
|
6dc7dab154 | ||
|
|
dd4cb23005 | ||
|
|
0589017f8c | ||
|
|
55027a9b2b | ||
|
|
0a984ca8c8 | ||
|
|
929d13bfea | ||
|
|
375e18bec8 | ||
|
|
b9de74f183 | ||
|
|
08ca69507f | ||
|
|
65ca982342 | ||
|
|
658281a92c | ||
|
|
a959f61367 | ||
|
|
8e61f744ec | ||
|
|
73d3e52e29 | ||
|
|
49a134845c | ||
|
|
29807f3d39 | ||
|
|
29b79b7725 | ||
|
|
1cdb10a040 | ||
|
|
5f7851df72 | ||
|
|
3d9bc05d7a | ||
|
|
c127428c59 | ||
|
|
7966d8403a | ||
|
|
80cc8a8e02 | ||
|
|
ee4e205fef | ||
|
|
ea443dc80c | ||
|
|
283645513d | ||
|
|
81b99382b8 | ||
|
|
ab74465e6c | ||
|
|
b3eadb557b | ||
|
|
0abd2bcba6 | ||
|
|
9cf76a918e | ||
|
|
ae437b1510 | ||
|
|
1096ec1c09 | ||
|
|
235394d96c | ||
|
|
d25e1d801c | ||
|
|
2dca5ab966 | ||
|
|
89ab57b1c1 | ||
|
|
dc66e6a4bf | ||
|
|
5c2f2fd882 | ||
|
|
5c3ddefbf9 | ||
|
|
a8d3f45ea1 | ||
|
|
cc2c41ddc8 | ||
|
|
b990f30a09 | ||
|
|
5c711322d4 | ||
|
|
b7d4a4f604 | ||
|
|
cc8874b687 | ||
|
|
2d0bc05488 | ||
|
|
1429774487 | ||
|
|
2060312dc1 | ||
|
|
8b6728480f | ||
|
|
cecafdee29 | ||
|
|
05f2af25af | ||
|
|
e3d826f6c4 | ||
|
|
02430bed90 | ||
|
|
3f7005ed9a | ||
|
|
586ee75833 | ||
|
|
1d903f11a8 | ||
|
|
578159b95c | ||
|
|
5c95587284 | ||
|
|
f7e9227ad2 | ||
|
|
c11a4d6867 | ||
|
|
6c2b0448a4 | ||
|
|
bd0eb8cccf | ||
|
|
09bb043952 | ||
|
|
9ca6cfd637 | ||
|
|
3869a66fcc | ||
|
|
d1c94f5120 | ||
|
|
c55e9941ec | ||
|
|
fa9a419d73 | ||
|
|
ab4e0da6b4 | ||
|
|
073572681e | ||
|
|
b630f269c4 | ||
|
|
40b1cd82b1 | ||
|
|
abcbdef63b | ||
|
|
dc8d1b0993 | ||
|
|
56d53d8805 | ||
|
|
7433fe049c | ||
|
|
bb2be49d3b | ||
|
|
cd1b578e84 | ||
|
|
c3df9b4105 | ||
|
|
243f3e21ec | ||
|
|
375291380c | ||
|
|
d221194454 | ||
|
|
6d94a54387 | ||
|
|
910bde88c7 | ||
|
|
72916544ce | ||
|
|
a01975dfce | ||
|
|
10708801ae | ||
|
|
ac096fb4e7 | ||
|
|
620c1397ba | ||
|
|
5b2f2f34f6 | ||
|
|
ae3861a29d | ||
|
|
93e2145254 | ||
|
|
2b6290d275 | ||
|
|
f81af7acb3 | ||
|
|
f0170247a4 | ||
|
|
3430874d11 | ||
|
|
cb5e7532ab | ||
|
|
5b928d679c | ||
|
|
697b9694e5 | ||
|
|
81c3e7e7f6 | ||
|
|
0517bba8ca | ||
|
|
83c7244fe6 | ||
|
|
68fd129042 | ||
|
|
b697e8a616 | ||
|
|
2b281fbde9 | ||
|
|
de8c4018c4 | ||
|
|
9e8af96bbf | ||
|
|
b0415a5289 | ||
|
|
ff7344438b | ||
|
|
f5f8e5d279 | ||
|
|
b4ddc8f96c | ||
|
|
e556c8ee15 | ||
|
|
2f9a0b3376 | ||
|
|
828f07b401 | ||
|
|
36921b3426 | ||
|
|
3988c6491c | ||
|
|
2cf558ec05 | ||
|
|
6fbadbdd94 | ||
|
|
faa1d7effb | ||
|
|
fe73a708d4 | ||
|
|
a5ca262faa | ||
|
|
8ebb1e29fa | ||
|
|
1b44dc9522 | ||
|
|
a66d468dc2 | ||
|
|
ca8beafc2d | ||
|
|
639c589a4a | ||
|
|
cd66836218 | ||
|
|
98662baa26 | ||
|
|
004e5794e3 | ||
|
|
f4f4f062cf | ||
|
|
d8d9c7e171 | ||
|
|
ceff82732e | ||
|
|
d94b1708a9 | ||
|
|
46e1f16012 | ||
|
|
bbd014d409 | ||
|
|
d553ee7c60 | ||
|
|
2df6ab240d | ||
|
|
8a2b9dfd6a | ||
|
|
1f7892d7a9 | ||
|
|
d11c537bea | ||
|
|
2d4d237009 | ||
|
|
daeee6b616 | ||
|
|
eacb0b13b2 | ||
|
|
dc5748059a | ||
|
|
96d75f4bcb | ||
|
|
19bfdf3f9f | ||
|
|
4ea273b297 | ||
|
|
72c9845174 | ||
|
|
77597b329e | ||
|
|
f62f00b4ad | ||
|
|
1975973ff2 | ||
|
|
00262b4a49 | ||
|
|
de6cabe408 | ||
|
|
d65552b59f | ||
|
|
3755f48bce | ||
|
|
8fe75d2015 | ||
|
|
b0c0249ce6 | ||
|
|
17685f3d86 | ||
|
|
30f1c71569 | ||
|
|
137afba1b6 | ||
|
|
b27de5cac1 | ||
|
|
8d43ae9805 | ||
|
|
3cebb028f4 | ||
|
|
28ab9d3515 | ||
|
|
f7739309e8 | ||
|
|
2db0d63c97 | ||
|
|
437b86d1a7 | ||
|
|
5ba1df52e0 | ||
|
|
04ab753b26 | ||
|
|
bc4a598a55 | ||
|
|
651cdec9b5 | ||
|
|
346f9fbacd | ||
|
|
0f493ae808 | ||
|
|
a07f143759 | ||
|
|
0ec22c7a6e | ||
|
|
73611004a0 | ||
|
|
776ddddc83 | ||
|
|
f60cce54ea | ||
|
|
63087a4311 | ||
|
|
5a193d50f6 | ||
|
|
08a6e999b9 | ||
|
|
e33cdca1ef | ||
|
|
9ede7a3c42 | ||
|
|
430d4e1ccd | ||
|
|
de4d6037d3 | ||
|
|
fc1fc6842b | ||
|
|
0649b297f6 | ||
|
|
9a470b9d41 | ||
|
|
47d1ab356d | ||
|
|
1ea5787486 | ||
|
|
1d4695c109 | ||
|
|
b673f9dd7f | ||
|
|
a1dd03472f | ||
|
|
497e545024 | ||
|
|
6892fdb70b | ||
|
|
aa1cc32d17 | ||
|
|
b22398ae6c | ||
|
|
e6eddaff73 | ||
|
|
cd53518897 | ||
|
|
8e9b1b7213 | ||
|
|
7a88fae2e2 | ||
|
|
f066da23c5 | ||
|
|
34aa3d3e00 | ||
|
|
715119fd45 | ||
|
|
bde34fc4c4 | ||
|
|
07b4aa89d4 | ||
|
|
d60351114c | ||
|
|
c932a70bef | ||
|
|
4641d7ee8c | ||
|
|
d4b3ee50f2 | ||
|
|
5392daa3ff | ||
|
|
a70e366fb4 | ||
|
|
dff14268db | ||
|
|
5517e157ad | ||
|
|
bdf4ffc36b | ||
|
|
71455c63c1 | ||
|
|
b1ae2b1a41 | ||
|
|
8a31732ce2 | ||
|
|
e79aed7792 | ||
|
|
973fc08f2d | ||
|
|
00211e1fb2 | ||
|
|
ce7286a72a | ||
|
|
db335d5cec | ||
|
|
ee5ce0c809 | ||
|
|
b8efef7c7a | ||
|
|
e2cbf40957 | ||
|
|
d7d45fb8e2 | ||
|
|
63afacc067 | ||
|
|
3175199787 | ||
|
|
1d0c3de65f | ||
|
|
fe1646caa0 | ||
|
|
72710f075b | ||
|
|
c7c01aedc2 | ||
|
|
c2e2e76fd8 | ||
|
|
f30a87e4e2 | ||
|
|
7a84cfd510 | ||
|
|
7a5a773b07 | ||
|
|
cf1488f6ce | ||
|
|
b02badba0c | ||
|
|
772d84ea5a | ||
|
|
ddaa66f080 | ||
|
|
8fd75833f0 | ||
|
|
1967d60813 | ||
|
|
f38f265cf7 | ||
|
|
79f37ffee0 | ||
|
|
d4b2a3c696 | ||
|
|
39ec365821 | ||
|
|
bc423c471d | ||
|
|
a790f43566 | ||
|
|
9fbdc950d2 | ||
|
|
8319963cbb | ||
|
|
4341219497 | ||
|
|
835504270d | ||
|
|
daed42d208 | ||
|
|
850f51a156 | ||
|
|
d37b195708 | ||
|
|
54ceb85ebe | ||
|
|
ef7a5bc753 | ||
|
|
b7ef60eedd | ||
|
|
70ede70ea8 | ||
|
|
d1d942f3fd | ||
|
|
53b3bda909 | ||
|
|
ac5571a363 | ||
|
|
c42f5eca87 | ||
|
|
9cb6816b3c | ||
|
|
feab633e60 | ||
|
|
a50e430cd9 | ||
|
|
46918ee907 | ||
|
|
fe1889653e | ||
|
|
9487b5367d | ||
|
|
6b47df75a7 | ||
|
|
bd9b2d54aa | ||
|
|
506d1dc1f2 | ||
|
|
90f9819cbd | ||
|
|
9bbd03c14e | ||
|
|
2852815e1a | ||
|
|
41a100613f | ||
|
|
63e489f134 | ||
|
|
914d3c4a66 | ||
|
|
2b47a1b06a | ||
|
|
625419a7db | ||
|
|
2710d9de5b | ||
|
|
e51314b104 | ||
|
|
4c128d837c | ||
|
|
c392804f47 | ||
|
|
cc7a25d9ce | ||
|
|
36b2bea25f | ||
|
|
913796ff0f | ||
|
|
a1b9892c77 | ||
|
|
03de4b29ea | ||
|
|
35a706f745 | ||
|
|
65cd9751d8 | ||
|
|
ff9a1ebb1b | ||
|
|
b5df000e9d | ||
|
|
655522a3e5 | ||
|
|
e4a4af34c5 | ||
|
|
b047e562ca | ||
|
|
1600233c48 | ||
|
|
55c8bcd0e3 | ||
|
|
2d0dadbd34 | ||
|
|
49879bc9db | ||
|
|
2c453c7691 | ||
|
|
5166c22ce9 | ||
|
|
225b9e1b15 | ||
|
|
7a9d2c9a74 | ||
|
|
3855e488cb | ||
|
|
ce75747887 | ||
|
|
c462766cb8 | ||
|
|
bb905b70df | ||
|
|
5c8b9f6b4c | ||
|
|
c726639484 | ||
|
|
100c7b8360 | ||
|
|
5502df89bb | ||
|
|
4491b66872 | ||
|
|
9bc24728b4 | ||
|
|
a3a00ea052 | ||
|
|
1b1534add5 | ||
|
|
a54c8d4f55 | ||
|
|
0cddd15203 | ||
|
|
5653d443d9 | ||
|
|
a5a497c4ea | ||
|
|
d60feb466c | ||
|
|
a435167619 | ||
|
|
99c823c763 | ||
|
|
aebed13a40 | ||
|
|
1347bdd545 | ||
|
|
562754c0b9 | ||
|
|
ec52e144e8 | ||
|
|
3e383a9f57 | ||
|
|
3a2444db0d | ||
|
|
14d01ae358 | ||
|
|
1f4b147ddd | ||
|
|
12405f4059 | ||
|
|
8d6965713c | ||
|
|
fe2858bc75 | ||
|
|
99dd6ae6aa | ||
|
|
cb7ed4079f | ||
|
|
89fbc055f4 | ||
|
|
df12b838ad | ||
|
|
055fa19c9b | ||
|
|
d1b661506e | ||
|
|
180ddcceaa | ||
|
|
512fad207b | ||
|
|
82d514d857 | ||
|
|
037fac7cd4 | ||
|
|
ebad85664d | ||
|
|
4b626c39fe | ||
|
|
81c539f150 | ||
|
|
0e74d82777 | ||
|
|
5ea34a3c07 | ||
|
|
4f8c5c3c0a | ||
|
|
37e45e0984 | ||
|
|
5683ad6666 | ||
|
|
92ca1e6e09 | ||
|
|
6571fdbaa2 | ||
|
|
9c3f138b8e | ||
|
|
0ac2865b74 | ||
|
|
98fc88dec6 | ||
|
|
8cab790030 | ||
|
|
954399b255 | ||
|
|
66c95f901d | ||
|
|
8265922d68 | ||
|
|
2403184845 | ||
|
|
15a53d299d | ||
|
|
24a1a5d680 | ||
|
|
c1cfff1502 | ||
|
|
8b2599297b | ||
|
|
1ed387dd54 | ||
|
|
9c795895ba | ||
|
|
3c193dca58 | ||
|
|
ce2a8fbfab | ||
|
|
4bbcf44351 | ||
|
|
a9a43538be | ||
|
|
905e4f16e2 | ||
|
|
008489361d | ||
|
|
dfa7fb53b2 | ||
|
|
dc6604bcda | ||
|
|
63194ff292 | ||
|
|
53a727955f | ||
|
|
a8fca89045 | ||
|
|
1eff727722 | ||
|
|
d9c6f7acb6 | ||
|
|
faefe957b0 | ||
|
|
2fb8467673 | ||
|
|
401422deb3 | ||
|
|
b52ed8e4c9 | ||
|
|
41061d0289 | ||
|
|
fed9197d23 | ||
|
|
282d3dbf8c | ||
|
|
b6c6dc7282 | ||
|
|
45194061b3 | ||
|
|
55480c8290 | ||
|
|
63bcc04eff | ||
|
|
fda5405e48 | ||
|
|
819e52cab3 | ||
|
|
ea917c82b6 | ||
|
|
65d5303765 | ||
|
|
08d37a4bef | ||
|
|
da2b059802 | ||
|
|
62d934dd8e | ||
|
|
96086b7733 | ||
|
|
182fc104bb | ||
|
|
06a897fba0 | ||
|
|
ff4b13245c | ||
|
|
00a8906128 | ||
|
|
8b73d2d39f | ||
|
|
e976b3e43e | ||
|
|
1585ca7c85 | ||
|
|
f7697007e5 | ||
|
|
ba151a8b83 | ||
|
|
a5153f5375 | ||
|
|
4899f01d6e | ||
|
|
e6b3107997 | ||
|
|
053440c4a8 | ||
|
|
249e9c0b52 | ||
|
|
099d5570f4 | ||
|
|
6a84f433ea | ||
|
|
12bf409e10 | ||
|
|
a01f1e64fe | ||
|
|
5f549a8fc6 | ||
|
|
eb144af0b9 | ||
|
|
0d020d3a54 | ||
|
|
e1042e326d | ||
|
|
f085e7d362 | ||
|
|
b995f39206 | ||
|
|
713bf58c44 | ||
|
|
a7af21958f | ||
|
|
8fed3df681 | ||
|
|
0be78b1204 | ||
|
|
0a67ecbc3a | ||
|
|
c57b4ee965 | ||
|
|
d4db7b3fc1 | ||
|
|
e9db964a70 | ||
|
|
90c8a714fc | ||
|
|
d958c0c68a | ||
|
|
377d02ab1a | ||
|
|
2c0ad89a07 | ||
|
|
101a8adbc2 | ||
|
|
5009fa461c | ||
|
|
f48f6ae5df | ||
|
|
be504212d0 | ||
|
|
0f075137c9 | ||
|
|
8714664e00 | ||
|
|
bfaf938543 | ||
|
|
b605bc086c | ||
|
|
e394b16335 | ||
|
|
0457423498 | ||
|
|
91d30dbb83 | ||
|
|
63642a4d3e | ||
|
|
1b64900c14 | ||
|
|
2660178658 | ||
|
|
3a0271cd4d | ||
|
|
76e2a2f032 | ||
|
|
932de969f0 | ||
|
|
9082ab6ff7 | ||
|
|
2461173e26 | ||
|
|
48c2f8b91b | ||
|
|
6766b25414 | ||
|
|
aacaf1b3fe | ||
|
|
4cc154cbe1 | ||
|
|
ca030dd5ed | ||
|
|
f000409a90 | ||
|
|
697f0659e2 | ||
|
|
5169ef1f00 | ||
|
|
10faa45182 | ||
|
|
edff3c35f2 | ||
|
|
3a611adc11 | ||
|
|
381b491845 | ||
|
|
6aca344bf7 | ||
|
|
512046e300 | ||
|
|
9cb3cf250c | ||
|
|
1cc5a67d82 | ||
|
|
fa6823599a | ||
|
|
398cbe9284 | ||
|
|
d87e488c23 | ||
|
|
5c2ff9b777 | ||
|
|
6a3a72eb06 | ||
|
|
56544802e8 | ||
|
|
6d7e37610c | ||
|
|
a47e6dd8c5 | ||
|
|
5bd0c701c7 | ||
|
|
50a2771d87 | ||
|
|
e6df041613 | ||
|
|
f334a2740f | ||
|
|
caa1de8aff | ||
|
|
fac13fb8cb | ||
|
|
26e487c01a | ||
|
|
cc438fdb7b | ||
|
|
92ff98d99a | ||
|
|
d1609cba90 | ||
|
|
0c394b123c | ||
|
|
421b8214cb | ||
|
|
6fc91312d2 | ||
|
|
22bb129bd9 | ||
|
|
4c57893312 | ||
|
|
a2d5314cf7 | ||
|
|
e063967734 | ||
|
|
4519dd010d | ||
|
|
55d2637214 | ||
|
|
bc2dc8d933 | ||
|
|
fc9b63298c | ||
|
|
c45514b989 | ||
|
|
8c9015b57b | ||
|
|
a0cb96abff | ||
|
|
3f51114129 | ||
|
|
29136d633a | ||
|
|
20bff1389e | ||
|
|
106e538d08 | ||
|
|
dc7ae3917e | ||
|
|
c0fb96a911 | ||
|
|
a1e02f7704 | ||
|
|
436c75ca6c | ||
|
|
7d75950624 | ||
|
|
5f051a9766 | ||
|
|
5716cf8cb2 | ||
|
|
7bb5cacb0d | ||
|
|
9801cf50e3 | ||
|
|
b5558a8b78 | ||
|
|
a7c31e6bcc | ||
|
|
6e76610f30 | ||
|
|
6da2b399e8 | ||
|
|
79c962fc88 | ||
|
|
28fb864ed0 | ||
|
|
d23227d427 | ||
|
|
eb6d26b6a4 | ||
|
|
a8a28294d3 | ||
|
|
7db1ba40eb | ||
|
|
d8bd8d87ec | ||
|
|
d29e0aa1a7 | ||
|
|
644ad110c0 | ||
|
|
6791de5fc0 | ||
|
|
1bb96ef405 | ||
|
|
7dc4ccf144 | ||
|
|
2b39438eba | ||
|
|
8952e2b0cd | ||
|
|
4806ac62ee | ||
|
|
eaa1179572 |
74
.github/CONTRIBUTING.md
vendored
74
.github/CONTRIBUTING.md
vendored
@@ -1,68 +1,46 @@
|
||||
NewPipe contribution guidelines
|
||||
===============================
|
||||
|
||||
PLEASE READ THESE GUIDELINES CAREFULLY BEFORE ANY CONTRIBUTION!
|
||||
|
||||
## Crash reporting
|
||||
|
||||
Do not report crashes in the GitHub issue tracker. NewPipe has an automated crash report system that will ask you to
|
||||
send a report via e-mail when a crash occurs. This contains all the data we need for debugging, and allows you to even
|
||||
add a comment to it. You'll see exactly what is sent, the system is 100% transparent.
|
||||
Report crashes through the automated crash report system of NewPipe.
|
||||
This way all the data needed for debugging is included in your bugreport for GitHub.
|
||||
You'll see exactly what is sent, be able to add your comments, and then send it.
|
||||
|
||||
## Issue reporting/feature requests
|
||||
|
||||
* Search the [existing issues](https://github.com/TeamNewPipe/NewPipe/issues) first to make sure your issue/feature
|
||||
hasn't been reported/requested before
|
||||
* Check whether your issue/feature is already fixed/implemented
|
||||
* Check if the issue still exists in the latest release/beta version
|
||||
* If you are an Android/Java developer, you are always welcome to fix/implement an issue/a feature yourself. PRs welcome!
|
||||
* We use English for development. Issues in other languages will be closed and ignored.
|
||||
* Please only add *one* issue at a time. Do not put multiple issues into one thread.
|
||||
* When reporting a bug please give us a context, and a description how to reproduce it.
|
||||
* Issues that only contain a generated bug report, but no description might be closed.
|
||||
* **Already reported**? Browse the [existing issues](https://github.com/TeamNewPipe/NewPipe/issues) to make sure your issue/feature hasn't been reported/requested.
|
||||
* **Already fixed**? Check whether your issue/feature is already fixed/implemented.
|
||||
* **Still relevant**? Check if the issue still exists in the latest release/beta version.
|
||||
* **Can you fix it**? If you are an Android/Java developer, you are always welcome to fix an issue or implement a feature yourself. PRs welcome! See [Code contribution](#code-contribution) for more info.
|
||||
* **Is it in English**? Issues in other languages will be ignored unless someone translates them.
|
||||
* **Is it one issue**? Multiple issues require multiple reports, that can be linked to track their statuses.
|
||||
* **The template**: Fill it out, everyone wins. Your issue has a chance of getting fixed.
|
||||
|
||||
## Bug Fixing
|
||||
* If you want to help NewPipe to become free of bugs (this is our utopic goal for NewPipe), you can send us an email to
|
||||
tnp@newpipe.schabi.org to let me know that you intend to help. We'll send you further instructions. You may, on request,
|
||||
register at our [Sentry](https://sentry.schabi.org) instance (see section "Crash reporting" for more information.
|
||||
|
||||
## Translation
|
||||
|
||||
* NewPipe can be translated via [Weblate](https://hosted.weblate.org/projects/newpipe/strings/). You can log in there
|
||||
with your GitHub account.
|
||||
* NewPipe is translated via [Weblate](https://hosted.weblate.org/projects/newpipe/strings/). Log in there with your GitHub account, or register.
|
||||
* Add the language you want to translate if it is not there already: see [How to add a new language](https://github.com/TeamNewPipe/NewPipe/wiki/How-to-add-a-new-language-to-NewPipe) in the wiki.
|
||||
|
||||
## Code contribution
|
||||
|
||||
* Stick to NewPipe's style conventions (well, just look the other code and then do it the same way :))
|
||||
* Do not bring non-free software (e.g., binary blobs) into the project. Also, make sure you do not introduce Google
|
||||
* If you want to help out with an existing bug report or feature request, leave a comment on that issue saying you want to try your hand at it.
|
||||
* If there is no existing issue for what you want to work on, open a new one describing your changes. This gives the team and the community a chance to give feedback before you spend time on something that is already in development, should be done differently, or should be avoided completely.
|
||||
* Stick to NewPipe's style conventions of [checkStyle](https://github.com/checkstyle/checkstyle). It runs each time you build the project.
|
||||
* Do not bring non-free software (e.g. binary blobs) into the project. Make sure you do not introduce Google
|
||||
libraries.
|
||||
* Stick to [F-Droid contribution guidelines](https://f-droid.org/wiki/page/Inclusion_Policy)
|
||||
* Make changes on a separate branch, not on the master branch. This is commonly known as *feature branch workflow*. You
|
||||
may then send your changes as a pull request on GitHub. Patches to the email address mentioned in this document might
|
||||
not be considered, GitHub is the primary platform. (This only affects you if you are a member of TeamNewPipe)
|
||||
* When submitting changes, you confirm that your code is licensed under the terms of the
|
||||
[GNU General Public License v3](https://www.gnu.org/licenses/gpl-3.0.html).
|
||||
* Please test (compile and run) your code before you submit changes! Ideally, provide test feedback in the PR
|
||||
description. Untested code will **not** be merged!
|
||||
* Stick to [F-Droid contribution guidelines](https://f-droid.org/wiki/page/Inclusion_Policy).
|
||||
* Make changes on a separate branch with a meaningful name, not on the _master_ branch or the _dev_ branch. This is commonly known as *feature branch workflow*. You may then send your changes as a pull request (PR) on GitHub.
|
||||
* Please test (compile and run) your code before submitting changes! Ideally, provide test feedback in the PR description. Untested code will **not** be merged!
|
||||
* Make sure your PR is up-to-date with the rest of the code. Often, a simple click on "Update branch" will do the job, but if not, you must rebase the dev branch manually and resolve the problems on your own. You can find help [on the wiki](https://github.com/TeamNewPipe/NewPipe/wiki/How-to-merge-a-PR). That makes the maintainers' jobs way easier.
|
||||
* Please show intention to maintain your features and code after you contribute a PR. Unmaintained code is a hassle for core developers. If you do not intend to maintain features you plan to contribute, please rethink your submission, or clearly state that in the PR description.
|
||||
* Respond if someone requests changes or otherwise raises issues about your PRs.
|
||||
* Send PRs that only cover one specific issue/solution/bug. Do not send PRs that are huge and consist of multiple independent solutions.
|
||||
* Try to figure out yourself why builds on our CI fail.
|
||||
* Make sure your PR is up-to-date with the rest of the code. Often, a simple click on "Update branch" will do the job,
|
||||
but if not, you are asked to merge the master branch manually and resolve the problems on your own. That will make the
|
||||
maintainers' jobs way easier.
|
||||
* Please show intention to maintain your features and code after you contributed it. Unmaintained code is a hassle for
|
||||
the core developers, and just adds work. If you do not intend to maintain features you contributed, please think again
|
||||
about submission, or clearly state that in the description of your PR.
|
||||
* Respond yourselves if someone requests changes or otherwise raises issues about your PRs.
|
||||
* Check if your contributions align with the [fdroid inclusion guidelines](https://f-droid.org/en/docs/Inclusion_Policy/).
|
||||
* Check if your submission can be build with the current fdroid build server setup.
|
||||
* Send PR that only cover one specific issue/solution/bug. Do not send PRs that are huge and consists of multiple
|
||||
independent solutions.
|
||||
|
||||
## Communication
|
||||
|
||||
* WE DO NOW HAVE A MAILING LIST: [newpipe@list.schabi.org](https://list.schabi.org/cgi-bin/mailman/listinfo/newpipe).
|
||||
* There is an IRC channel on Freenode which is regularly visited by the core team and other developers:
|
||||
[#newpipe](irc:irc.freenode.net/newpipe). [Click here for Webchat](https://webchat.freenode.net/?channels=newpipe)!
|
||||
* If you want to get in touch with the core team or one of our other contributors you can send an email to
|
||||
tnp(at)schabi.org. Please do not send issue reports, they will be ignored and remain unanswered! Use the GitHub issue
|
||||
tracker described above!
|
||||
* Feel free to post suggestions, changes, ideas etc. on GitHub, IRC or the mailing list!
|
||||
* The [#newpipe](irc:irc.freenode.net/newpipe) channel on freenode has the core team and other developers in it. [Click here for webchat](https://webchat.freenode.net/?channels=newpipe)!
|
||||
* You can also use a Matrix account to join the Newpipe channel at [#freenode_#newpipe:matrix.org](https://matrix.to/#/#freenode_#newpipe:matrix.org).
|
||||
* Post suggestions, changes, ideas etc. on GitHub or IRC.
|
||||
|
||||
61
.github/ISSUE_TEMPLATE/bug_report.md
vendored
61
.github/ISSUE_TEMPLATE/bug_report.md
vendored
@@ -7,38 +7,59 @@ assignees: ''
|
||||
---
|
||||
|
||||
<!--
|
||||
Oh no, a bug! It happens. Thanks for reporting an issue with NewPipe.
|
||||
|
||||
Use this template to notify us if you found a bug.
|
||||
|
||||
To make it easier for us to help you please enter detailed information below.
|
||||
|
||||
Please note, we only support the latest version of NewPipe and the master branch. Make sure you have that version installed. If you don't, upgrade & reproduce the problem before opening the issue. The release page (https://github.com/TeamNewPipe/NewPipe/releases/latest) is the go-to place to get this version. In order to check your app version, open the left drawer and click on "About".
|
||||
|
||||
P.S.: Our contribution guidelines might be a nice document to read before you fill out the report :) You can find it at https://github.com/TeamNewPipe/NewPipe/blob/HEAD/.github/CONTRIBUTING.md
|
||||
Oh no, a bug! It happens. Thanks for reporting an issue with NewPipe. To make it easier for us to help you please enter detailed information in the template we have provided below. If a section isn't relevant, just delete it, though it would be helpful to still provide as much detail as possible.
|
||||
-->
|
||||
### Version
|
||||
<!-- Which version are you using? -->
|
||||
-
|
||||
|
||||
<!-- IF YOU DON'T FILL IN THE TEMPLATE PROPERLY, YOUR ISSUE IS LIABLE TO BE CLOSED. If you feel tired/lazy right now, open your issue some other time. We'll wait. -->
|
||||
|
||||
<!-- The comments between these brackets won't show up in the submitted issue (as you can see in the Preview). -->
|
||||
|
||||
### Checklist
|
||||
<!-- The first box has been checked for you to show you how it is done. -->
|
||||
|
||||
- [x] I am using the latest version - x.xx.x <!-- Check https://github.com/TeamNewPipe/NewPipe/releases -->
|
||||
- [ ] I checked, but didn't find any duplicates (open OR closed) of this issue in the repo. <!-- Seriously, check. O_O -->
|
||||
- [ ] I have read the contribution guidelines given at https://github.com/TeamNewPipe/NewPipe/blob/HEAD/.github/CONTRIBUTING.md.
|
||||
- [ ] This issue contains only one bug. I will open one issue for every bug report I want to file.
|
||||
|
||||
### Steps to reproduce the bug
|
||||
<!-- If you can't reproduce it, please try to give as many details as possible on how you think you got to the bug. -->
|
||||
Steps to reproduce the behavior:
|
||||
<!--
|
||||
1. Go to '...'
|
||||
2. Press on '....'
|
||||
3. Swipe down to '....'
|
||||
-->
|
||||
|
||||
<!-- If you can't cause the bug to show up again reliably (and hence don't have a proper set of steps to give us), please still try to give as many details as possible on how you think you encountered the bug. -->
|
||||
|
||||
|
||||
### Expected behavior
|
||||
Tell us what you expected to happen.
|
||||
|
||||
### Actual behaviour
|
||||
Tell us what happens instead.
|
||||
<!-- Tell us what happens with the steps given above. -->
|
||||
|
||||
|
||||
|
||||
### Expected behavior
|
||||
<!-- Tell us what you expect to happen. -->
|
||||
|
||||
|
||||
|
||||
### Screenshots/Screen recordings
|
||||
<!-- If applicable, add screenshots or a screen recording to help explain your problem. GitHub supports uploading them directly in the issue text box. If your file is too big for Github to accept, feel free to paste a link from an image/video hoster here instead. -->
|
||||
|
||||
<!-- DON'T POST SCREENSHOTS OF THE ERROR PAGE. Use the buttons given on the error page to paste the error as text in the Logs section below. -->
|
||||
|
||||
|
||||
### Screenshots/Screen records
|
||||
If applicable, add screenshots or a screen recording to help explain your problem. GitHub should support uploading them directly in the issue field. If your file is too big, feel free to paste a link from an image/video hoster here instead.
|
||||
|
||||
### Logs
|
||||
If your bug includes a crash, please head over to the [incredible bugreport to markdown converter](https://teamnewpipe.github.io/CrashReportToMarkdown/). Copy the result. Paste it here:
|
||||
<!-- If your bug includes a crash (where you're shown the Error Report page with a bunch of info), tap on "Copy formatted report" at the bottom and paste it here: -->
|
||||
|
||||
<!-- That's right, here! -->
|
||||
|
||||
|
||||
|
||||
<!-- Please fill this out when you do not provide a log generate by NewPipe -->
|
||||
|
||||
### Device info
|
||||
|
||||
- Android version/Custom ROM version:
|
||||
- Device model:
|
||||
|
||||
1
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
1
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
@@ -0,0 +1 @@
|
||||
blank_issues_enabled: false
|
||||
44
.github/ISSUE_TEMPLATE/feature_request.md
vendored
44
.github/ISSUE_TEMPLATE/feature_request.md
vendored
@@ -5,24 +5,42 @@ labels: enhancement
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
<!-- Hey. Our contribution guidelines (https://github.com/TeamNewPipe/NewPipe/blob/HEAD/.github/CONTRIBUTING.md) might be an appropriate
|
||||
document to read before you fill out the request :) -->
|
||||
#### Is your feature request related to a problem? Please describe it
|
||||
A clear and concise description of what the problem is.
|
||||
Example: *I want to do X, but there is no way to do it.*
|
||||
<!-- IF YOU DON'T FILL IN THE TEMPLATE PROPERLY, YOUR ISSUE IS LIABLE TO BE CLOSED. If you feel tired/lazy right now, open your issue some other time. We'll wait. -->
|
||||
|
||||
#### Describe the solution you'd like
|
||||
A clear and concise description of what you want to happen.
|
||||
|
||||
<!-- The comments between these brackets won't show up in the submitted issue (as you can see in the Preview). -->
|
||||
|
||||
### Checklist
|
||||
<!-- The first box has been checked for you to show you how it is done. -->
|
||||
|
||||
- [x] I checked, but didn't find any duplicates (open OR closed) of this issue in the repo. <!-- Seriously, check. O_O -->
|
||||
- [ ] I have read the contribution guidelines given at https://github.com/TeamNewPipe/NewPipe/blob/HEAD/.github/CONTRIBUTING.md.
|
||||
- [ ] This issue contains only one feature request. I will open one issue for every feature I want to request.
|
||||
|
||||
|
||||
#### Describe the feature you want
|
||||
<!-- A clear and concise description of what you wish should happen.
|
||||
Example: *I think it would be nice if you add feature Y which makes X possible.*
|
||||
|
||||
#### (Optional) Describe alternatives you've considered
|
||||
A clear and concise description of any alternative solutions or features you've considered.
|
||||
Example: *I considered Z, but that didn't turn out to be a good idea because...*
|
||||
Optionally, also describe alternatives you've considered.
|
||||
Example: *Z is also a good alternative. Not as good as Y, but at least...* or *I considered Z, but that didn't turn out to be a good idea because...* -->
|
||||
|
||||
|
||||
|
||||
#### Is your feature request related to a problem? Please describe it
|
||||
<!-- A clear and concise description of what the problem is. Maybe the developers and the community could brainstorm and come up with a better solution to your problem. If they exist, link to related Issues and/or PRs for developers to keep track easier.
|
||||
Example: *I want to do X, but there is no way to do it.* -->
|
||||
|
||||
|
||||
|
||||
#### Additional context
|
||||
Add any other context or screenshots about the feature request here.
|
||||
Example: *Here's a photo of my cat!*
|
||||
<!-- Add any other context, like screenshots, about the feature request here.
|
||||
Example: *Here's a photo of my cat!* -->
|
||||
|
||||
|
||||
|
||||
#### How will you/everyone benefit from this feature?
|
||||
Convince us! How does it change your NewPipe experience and/or your life?
|
||||
<!-- Convince us! How does it change your NewPipe experience and/or your life?
|
||||
The better this paragraph is, the more likely a developer will think about working on it.
|
||||
Example: *This feature will help us colonize the galaxy! -->
|
||||
|
||||
|
||||
22
.github/PULL_REQUEST_TEMPLATE.md
vendored
22
.github/PULL_REQUEST_TEMPLATE.md
vendored
@@ -1,26 +1,28 @@
|
||||
<!-- Hey there. Thank you so much for improving NewPipe. Please take a moment to fill out the following suggestion on how to structure this PR description. Having roughly the same layout helps everyone considerably :)-->
|
||||
<!-- Hey there. Thank you so much for improving NewPipe, and filling out the details. Having roughly the same layout helps everyone considerably :)-->
|
||||
|
||||
#### What is it?
|
||||
- [ ] Bug fix
|
||||
- [ ] Feature
|
||||
- [ ] Bugfix (user facing)
|
||||
- [ ] Feature (user facing)
|
||||
- [ ] Codebase improvement (dev facing)
|
||||
- [ ] Meta improvement to the project (dev facing)
|
||||
|
||||
#### Long description of the changes in your PR
|
||||
<!-- While bullet points are the norm in this section, feel free to write a text instead if you can't fit it in a list -->
|
||||
#### Description of the changes in your PR
|
||||
<!-- While bullet points are the norm in this section, feel free to write free-form text instead of a list -->
|
||||
- record videos
|
||||
- create clones
|
||||
- take over the world
|
||||
|
||||
#### Fixes the following issue(s)
|
||||
<!-- Also add reddit or other links which are relevant to your change. -->
|
||||
<!-- Also add any other links relevant to your change. -->
|
||||
-
|
||||
|
||||
#### Relies on the following changes
|
||||
<!-- Delete this if it doesn't apply to you. -->
|
||||
-
|
||||
|
||||
#### Testing apk
|
||||
<!-- Ensure that you have your changes on a new branch which has a meaningful name. This name will be used as a suffix for the app ID to allow installing and testing multiple versions of NewPipe. Do NOT name your branches like "patch-0" and "feature-1". For example, if your PR implements a bug fix for comments, an appropriate branch name would be "commentfix". -->
|
||||
#### APK testing
|
||||
<!-- Use a new, meaningfully named branch. The name is used as a suffix for the app ID to allow installing and testing multiple versions of NewPipe, e.g. "commentfix", if your PR implements a bugfix for comments. (No names like "patch-0" and "feature-1".) -->
|
||||
debug.zip
|
||||
|
||||
#### Agreement
|
||||
- [ ] I carefully read the [contribution guidelines](https://github.com/TeamNewPipe/NewPipe/blob/HEAD/.github/CONTRIBUTING.md) and agree to them.
|
||||
#### Due diligence
|
||||
- [ ] I read the [contribution guidelines](https://github.com/TeamNewPipe/NewPipe/blob/HEAD/.github/CONTRIBUTING.md).
|
||||
|
||||
8
.gitignore
vendored
8
.gitignore
vendored
@@ -10,3 +10,11 @@
|
||||
*~
|
||||
.weblate
|
||||
*.class
|
||||
|
||||
# vscode / eclipse files
|
||||
*.classpath
|
||||
*.project
|
||||
*.settings
|
||||
bin/
|
||||
.vscode/
|
||||
*.code-workspace
|
||||
|
||||
@@ -5,13 +5,13 @@ android:
|
||||
components:
|
||||
# The BuildTools version used by NewPipe
|
||||
- tools
|
||||
- build-tools-28.0.3
|
||||
- build-tools-29.0.3
|
||||
|
||||
# The SDK version used to compile NewPipe
|
||||
- android-28
|
||||
- android-29
|
||||
|
||||
before_install:
|
||||
- yes | sdkmanager "platforms;android-28"
|
||||
- yes | sdkmanager "platforms;android-29"
|
||||
script: ./gradlew -Dorg.gradle.jvmargs=-Xmx1536m assembleDebug lintDebug testDebugUnitTest
|
||||
|
||||
licenses:
|
||||
|
||||
144
README.ko.md
Normal file
144
README.ko.md
Normal file
@@ -0,0 +1,144 @@
|
||||
<p align="center"><a href="https://newpipe.schabi.org"><img src="assets/new_pipe_icon_5.png" width="150"></a></p>
|
||||
<h2 align="center"><b>NewPipe</b></h2>
|
||||
<h4 align="center">A libre lightweight streaming frontend for Android.</h4>
|
||||
<p align="center"><a href="https://f-droid.org/packages/org.schabi.newpipe/"><img src="https://f-droid.org/wiki/images/0/06/F-Droid-button_get-it-on.png"></a></p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://github.com/TeamNewPipe/NewPipe/releases" alt="GitHub release"><img src="https://img.shields.io/github/release/TeamNewPipe/NewPipe.svg" ></a>
|
||||
<a href="https://www.gnu.org/licenses/gpl-3.0" alt="License: GPLv3"><img src="https://img.shields.io/badge/License-GPL%20v3-blue.svg"></a>
|
||||
<a href="https://travis-ci.org/TeamNewPipe/NewPipe" alt="Build Status"><img src="https://travis-ci.org/TeamNewPipe/NewPipe.svg"></a>
|
||||
<a href="https://hosted.weblate.org/engage/newpipe/" alt="Translation Status"><img src="https://hosted.weblate.org/widgets/newpipe/-/svg-badge.svg"></a>
|
||||
<a href="http://webchat.freenode.net/?channels=%23newpipe" alt="IRC channel: #newpipe"><img src="https://img.shields.io/badge/IRC%20chat-%23newpipe-brightgreen.svg"></a>
|
||||
<a href="https://www.bountysource.com/teams/newpipe" alt="Bountysource bounties"><img src="https://img.shields.io/bountysource/team/newpipe/activity.svg?colorB=cd201f"></a>
|
||||
</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/FAQ/">FAQ</a> • <a href="https://newpipe.schabi.org/press/">Press</a></p>
|
||||
<hr>
|
||||
|
||||
*Read this in other languages: [English](README.md), [한국어](README.ko.md).*
|
||||
|
||||
<b>경고: 이 버전은 베타 버전이므로, 버그가 발생할 수도 있습니다. 만약 버그가 발생하였다면, 우리의 GITHUB 저장소에서 ISSUE를 열람하여 주십시오.</b>
|
||||
|
||||
<b>NEWPIPE 또는 이것의 FORK을 구글 플레이스토어에 올리는 것은 그들의 이용약관을 위반합니다.</b>
|
||||
|
||||
## Screenshots
|
||||
|
||||
[<img src="fastlane/metadata/android/en-US/images/phoneScreenshots/shot_01.png" width=160>](fastlane/metadata/android/en-US/images/phoneScreenshots/shot_01.png)
|
||||
[<img src="fastlane/metadata/android/en-US/images/phoneScreenshots/shot_02.png" width=160>](fastlane/metadata/android/en-US/images/phoneScreenshots/shot_02.png)
|
||||
[<img src="fastlane/metadata/android/en-US/images/phoneScreenshots/shot_03.png" width=160>](fastlane/metadata/android/en-US/images/phoneScreenshots/shot_03.png)
|
||||
[<img src="fastlane/metadata/android/en-US/images/phoneScreenshots/shot_04.png" width=160>](fastlane/metadata/android/en-US/images/phoneScreenshots/shot_04.png)
|
||||
[<img src="fastlane/metadata/android/en-US/images/phoneScreenshots/shot_05.png" width=160>](fastlane/metadata/android/en-US/images/phoneScreenshots/shot_05.png)
|
||||
[<img src="fastlane/metadata/android/en-US/images/phoneScreenshots/shot_06.png" width=160>](fastlane/metadata/android/en-US/images/phoneScreenshots/shot_06.png)
|
||||
[<img src="fastlane/metadata/android/en-US/images/phoneScreenshots/shot_07.png" width=160>](fastlane/metadata/android/en-US/images/phoneScreenshots/shot_07.png)
|
||||
[<img src="fastlane/metadata/android/en-US/images/phoneScreenshots/shot_08.png" width=160>](fastlane/metadata/android/en-US/images/phoneScreenshots/shot_08.png)
|
||||
[<img src="fastlane/metadata/android/en-US/images/phoneScreenshots/shot_09.png" width=160>](fastlane/metadata/android/en-US/images/phoneScreenshots/shot_09.png)
|
||||
[<img src="fastlane/metadata/android/en-US/images/phoneScreenshots/shot_10.png" width=160>](fastlane/metadata/android/en-US/images/phoneScreenshots/shot_10.png)
|
||||
[<img src="fastlane/metadata/android/en-US/images/tenInchScreenshots/shot_11.png" width=405>](fastlane/metadata/android/en-US/images/tenInchScreenshots/shot_11.png)
|
||||
[<img src="fastlane/metadata/android/en-US/images/tenInchScreenshots/shot_12.png" width=405>](fastlane/metadata/android/en-US/images/tenInchScreenshots/shot_12.png)
|
||||
|
||||
## Description
|
||||
|
||||
NewPipe는 어떤 구글 프레임워크 라이브러리나, 유튜브 API를 사용하지 않습니다. 웹사이트는 단지 필요한 정보를 가져오기 위해 구문 분석 됩니다. 따라서 이 앱은 구글 서비스의 설치 없이 기기에서 사용될 수 있습니다. 또한, 카피레프트 자유 소프트웨어인 NewPipe를 사용하기 위해 유튜브 계정이 필요하지 않습니다.
|
||||
|
||||
### Features
|
||||
|
||||
* 영상 검색
|
||||
* 영상의 일반적인 정보 표시
|
||||
* 유튜브 영상 보기
|
||||
* 유튜브 영상 듣기
|
||||
* 팝업 모드 (floating player)
|
||||
* 영상 공유
|
||||
* 영상 다운로드
|
||||
* 음성만 다운로드
|
||||
* Kodi에서 영상 열람
|
||||
* 다음/관련된 영상 표시
|
||||
* 특정 언어로 유튜브 검색
|
||||
* 연령 제한 컨텐츠 시청/차단
|
||||
* 채널에 대한 일반적인 정보 표시
|
||||
* 채널 검색
|
||||
* 채널에서 영상 시청
|
||||
* Orbot/Tor 지원 (아직 직접적이지 않음)
|
||||
* 1080p/2K/4K 지원
|
||||
* 기록 보기
|
||||
* 채널 구독
|
||||
* 기록 검색
|
||||
* 재생목록 검색/시청
|
||||
* 추가된 재생목록 시청
|
||||
* 영상 추가
|
||||
* 지역 재생목록
|
||||
* 자막
|
||||
* 실시간 방송 지원
|
||||
* 댓글 표시
|
||||
|
||||
### Supported Services
|
||||
|
||||
NewPipe는 여러가지 서비스를 지원합니다. 우리의 [문서](https://teamnewpipe.github.io/documentation/)는 새로운 서비스가 앱과 추출기에 어떻게 추가될 수 있는지에 대한 더 많은 정보를 제공합니다. 만약 새로운 서비스를 추가하고자 한다면, 우리에게 연락해 주시기 바랍니다. 현재 지원되는 서비스:
|
||||
|
||||
* YouTube
|
||||
* SoundCloud \[beta\]
|
||||
* media.ccc.de \[beta\]
|
||||
* PeerTube instances \[beta\]
|
||||
|
||||
## Updates
|
||||
NewPipe 코드의 변경이 있을 때(기능 추가 또는 버그 수정으로 인해), 결국 릴리즈가 발생할 것입니다. 이것들의 형식은 x.xx.x 입니다.
|
||||
이 새로운 버전을 얻기 위해서, 당신은:
|
||||
1. 직접 디버그 APK를 생성할 수 있습니다. 이 방법은 당신의 기기에서 새로운 기능을 얻을 수 있는 가장 빠른 방법이지만, 꽤 많이 복잡합니다.
|
||||
따라서 우리는 다른 방법들 중 하나를 사용하는 것을 추천합니다.
|
||||
2. 우리의 커스텀 저장소를 F-Droid에 추가하고 우리가 릴리즈를 게시하는 대로 저곳에서 릴리즈를 설치할 수 있습니다.
|
||||
이에 대한 설명서는 이곳에서 확인할 수 있습니다: https://newpipe.schabi.org/FAQ/tutorials/install-add-fdroid-repo/
|
||||
3. 우리가 릴리즈를 게시하는 대로 [Github Releases](https://github.com/TeamNewPipe/NewPipe/releases)에서 APK를 다운받고 이것을 설치할 수 있습니다.
|
||||
4. F-Droid를 통해 업데이트 할 수 있습니다. F-Droid는 변화를 인식하고, 스스로 APK를 생성하고, 이것에 서명하고, 사용자들에서 업데이트를 전달해야만 하기 때문에,
|
||||
이것은 업데이트를 받는 가장 느린 방법입니다.
|
||||
|
||||
우리는 대부분의 사용자에게 2번쨰 방법을 추천합니다. 방법 2 또는 3을 사용하여 설치된 APK는 서로 호환되지만, 방법 4를 사용하여 설치된 것들과는 호환되지 않습니다. 이것은 방법 2 또는 3에서는 같은 (우리의)서명 키가 사용되지만, 방법 4에서는 다른 (F-Droid의)서명 키가 사용되기 때문입니다. 방법 1을 사용하여 디버그 APK를 생성하는 것에서는 키가 완전히 제외됩니다. 서명 키는 사용자가 앱에 악의적인 업데이트를 설치하는 것에 대해 속지 않도록 보장하는 것을 도와줍니다.
|
||||
|
||||
한편, 만약 어떠한 이유(예. NewPipe의 핵심 기능이 손상되었고 F-Droid가 아직 업데이트를 가지지 않는 경우) 때문에 소스를 바꾸길 원한다면,
|
||||
우리는 다음과 같은 절차를 따르는 것을 권장합니다:
|
||||
1. 당신의 기록, 구독, 그리고 재생목록을 유지할 수 있도록 Settings > Content > Export Database 를 통해 데이터를 백업하십시오.
|
||||
2. NewPipe를 삭제하십시오.
|
||||
3. 새로운 소스에서 APK를 다운로드하고 이것을 설치하십시오.
|
||||
4. Step 1의 Settings > Content > Export Database 을 통해 데이터를 불러오십시오.
|
||||
|
||||
## Contribution
|
||||
당신이 아이디어, 번역, 디자인 변경, 코드 정리, 또는 정말 큰 코드 수정에 대한 의견이 있다면, 도움은 항상 환영합니다.
|
||||
더 많이 수행될수록 더 많이 발전할 수 있습니다!
|
||||
|
||||
만약 참여하고 싶다면, 우리의 [컨트리뷰션 공지](.github/CONTRIBUTING.md)를 참고하십시오.
|
||||
|
||||
<a href="https://hosted.weblate.org/engage/newpipe/">
|
||||
<img src="https://hosted.weblate.org/widgets/newpipe/-/287x66-grey.png" alt="Translation status" />
|
||||
</a>
|
||||
|
||||
## Donate
|
||||
만약 NewPipe가 마음에 들었다면, 우리는 기부에 대해 기꺼이 환영합니다. bitcoin을 보내거나, Bountysource 또는 Liberapay를 통해 기부할 수 있습니다. NewPipe에 기부하는 것에 대한 자세한 정보를 원한다면, 우리의 [웹사이트](https://newpipe.schabi.org/donate)를 방문하여 주십시오.
|
||||
|
||||
<table>
|
||||
<tr>
|
||||
<td><img src="https://bitcoin.org/img/icons/logotop.svg" alt="Bitcoin"></td>
|
||||
<td><img src="assets/bitcoin_qr_code.png" alt="Bitcoin QR code" width="100px"></td>
|
||||
<td><samp>16A9J59ahMRqkLSZjhYj33n9j3fMztFxnh</samp></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><a href="https://liberapay.com/TeamNewPipe/"><img src="https://upload.wikimedia.org/wikipedia/commons/2/27/Liberapay_logo_v2_white-on-yellow.svg" alt="Liberapay" width="80px" ></a></td>
|
||||
<td><a href="https://liberapay.com/TeamNewPipe/"><img src="assets/liberapay_qr_code.png" alt="Visit NewPipe at liberapay.com" width="100px"></a></td>
|
||||
<td><a href="https://liberapay.com/TeamNewPipe/donate"><img src="assets/liberapay_donate_button.svg" alt="Donate via Liberapay" height="35px"></a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><a href="https://www.bountysource.com/teams/newpipe"><img src="https://upload.wikimedia.org/wikipedia/commons/thumb/2/22/Bountysource.png/320px-Bountysource.png" alt="Bountysource" width="190px"></a></td>
|
||||
<td><a href="https://www.bountysource.com/teams/newpipe"><img src="assets/bountysource_qr_code.png" alt="Visit NewPipe at bountysource.com" width="100px"></a></td>
|
||||
<td><a href="https://www.bountysource.com/teams/newpipe/issues"><img src="https://img.shields.io/bountysource/team/newpipe/activity.svg?colorB=cd201f" height="30px" alt="Check out how many bounties you can earn."></a></td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
## Privacy Policy
|
||||
|
||||
NewPipe 프로젝트는 미디어 웹 서비스를 사용하는 것에 대한 사적의, 익명의 경험을 제공하는 것을 목표로 하고 있습니다.
|
||||
그러므로, 앱은 당신의 동의 없이 어떤 데이터도 수집하지 않습니다. NewPipe의 개인정보보호정책은 당신이 충돌 리포트를 보내거나, 또는 우리의 블로그에 글을 남길 때 어떤 데이터가 보내지고 저장되는지에 대해 상세히 설명합니다. 이 문서는 [여기](https://newpipe.schabi.org/legal/privacy/)에서 확인할 수 있습니다.
|
||||
|
||||
## License
|
||||
[](http://www.gnu.org/licenses/gpl-3.0.en.html)
|
||||
|
||||
NewPipe는 자유 소프트웨어입니다: 당신의 마음대로 이것을 사용하고, 연구하고, 공유하고, 개선할 수 있습니다.
|
||||
구체적으로 당신은 자유 소프트웨어 재단에서 발행되는, 버전 3 또는 (당신의 선택에 따라)이후 버전의,
|
||||
[GNU General Public License](https://www.gnu.org/licenses/gpl.html) 하에서 이것을 재배포 및/또는 수정할 수 있습니다.
|
||||
30
README.md
30
README.md
@@ -4,10 +4,10 @@
|
||||
<p align="center"><a href="https://f-droid.org/packages/org.schabi.newpipe/"><img src="https://f-droid.org/wiki/images/0/06/F-Droid-button_get-it-on.png"></a></p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://github.com/TeamNewPipe/NewPipe" alt="GitHub release"><img src="https://img.shields.io/github/release/TeamNewPipe/NewPipe.svg" ></a>
|
||||
<a href="https://github.com/TeamNewPipe/NewPipe/releases" alt="GitHub release"><img src="https://img.shields.io/github/release/TeamNewPipe/NewPipe.svg" ></a>
|
||||
<a href="https://www.gnu.org/licenses/gpl-3.0" alt="License: GPLv3"><img src="https://img.shields.io/badge/License-GPL%20v3-blue.svg"></a>
|
||||
<a href="https://travis-ci.org/TeamNewPipe/NewPipe" alt="Build Status"><img src="https://travis-ci.org/TeamNewPipe/NewPipe.svg"></a>
|
||||
<a href="https://hosted.weblate.org/engage/NewPipe/" alt="Translation Status"><img src="https://hosted.weblate.org/widgets/NewPipe/-/svg-badge.svg"></a>
|
||||
<a href="https://hosted.weblate.org/engage/newpipe/" alt="Translation Status"><img src="https://hosted.weblate.org/widgets/newpipe/-/svg-badge.svg"></a>
|
||||
<a href="http://webchat.freenode.net/?channels=%23newpipe" alt="IRC channel: #newpipe"><img src="https://img.shields.io/badge/IRC%20chat-%23newpipe-brightgreen.svg"></a>
|
||||
<a href="https://www.bountysource.com/teams/newpipe" alt="Bountysource bounties"><img src="https://img.shields.io/bountysource/team/newpipe/activity.svg?colorB=cd201f"></a>
|
||||
</p>
|
||||
@@ -16,9 +16,11 @@
|
||||
<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>
|
||||
|
||||
*Read this in other languages: [English](README.md), [한국어](README.ko.md).*
|
||||
|
||||
<b>WARNING: THIS IS A BETA VERSION, THEREFORE YOU MAY ENCOUNTER BUGS. IF YOU DO, OPEN AN ISSUE VIA OUR GITHUB REPOSITORY.</b>
|
||||
|
||||
<b>PUTTING NEWPIPE OR ANY FORK OF IT INTO GOOGLE PLAYSTORE VIOLATES THEIR TERMS OF CONDITIONS.</b>
|
||||
<b>PUTTING NEWPIPE OR ANY FORK OF IT INTO THE GOOGLE PLAY STORE VIOLATES THEIR TERMS AND CONDITIONS.</b>
|
||||
|
||||
## Screenshots
|
||||
|
||||
@@ -69,11 +71,6 @@ NewPipe does not use any Google framework libraries, nor the YouTube API. Websit
|
||||
* Livestream support
|
||||
* Show comments
|
||||
|
||||
### Coming Features
|
||||
|
||||
* Cast to UPnP and Cast
|
||||
* … and many more
|
||||
|
||||
### Supported Services
|
||||
|
||||
NewPipe supports multiple services. Our [docs](https://teamnewpipe.github.io/documentation/) provide more info on how a new service can be added to the app and the extractor. Please get in touch with us if you intend to add a new one. Currently supported services are:
|
||||
@@ -85,17 +82,18 @@ NewPipe supports multiple services. Our [docs](https://teamnewpipe.github.io/doc
|
||||
|
||||
## 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:
|
||||
* Build a debug APK yourself. This is the fastest way to get new features on your device, but is much more complicated, so we recommend using one of the other methods.
|
||||
* Download the APK from [releases](https://github.com/TeamNewPipe/NewPipe/releases) and install it.
|
||||
* Update via F-droid. This is the slowest method of getting updates, as F-Droid must recognize changes, build the APK itself, sign it, then push the update to users.
|
||||
1. Build a debug APK yourself. This is the fastest way to get new features on your device, but is much more complicated, so we recommend using one of the other methods.
|
||||
2. Add our custom repo to F-Droid and install it from there as soon as we publish a release. The instructions are here: https://newpipe.schabi.org/FAQ/tutorials/install-add-fdroid-repo/
|
||||
3. Download the APK from [Github Releases](https://github.com/TeamNewPipe/NewPipe/releases) and install it as soon as we publish a release.
|
||||
4. Update via F-droid. This is the slowest method of getting updates, as F-Droid must recognize changes, build the APK itself, sign it, then push the update to users.
|
||||
|
||||
When you install an APK from one of these options, it will be incompatible with an APK from one of the other options. This is due to different signing keys being used. Signing keys help ensure that a user isn't tricked into installing a malicious update to an app, and are independent. F-Droid and GitHub use different signing keys, and building an APK debug excludes a key. The signing key issue is being discussed in issue [#1981](https://github.com/TeamNewPipe/NewPipe/issues/1981), and may be fixed by setting up our own repository on F-Droid.
|
||||
We recommend method 2 for most users. APKs installed using method 2 or 3 are compatible with each other, but not with those installed using method 4. This is due to the same signing key (ours) being used for 2 and 3, but a different signing key (F-Droid's) being used for 4. Building a debug APK using method 1 excludes a key entirely. Signing keys help ensure that a user isn't tricked into installing a malicious update to an app.
|
||||
|
||||
In the meanwhile, if you want to switch sources for some reason (e.g. NewPipe's core functionality was broken and F-Droid doesn't have the update yet), we recommend following this procedure:
|
||||
1. Back up your data via "Settings>Content>Export Database" so you keep your history, subscriptions, and playlists
|
||||
1. Back up your data via Settings > Content > Export Database so you keep your history, subscriptions, and playlists
|
||||
2. Uninstall NewPipe
|
||||
3. Download the APK from the new source and install it
|
||||
4. Import the data from step 1 via "Settings>Content>Import Database"
|
||||
4. Import the data from step 1 via Settings > Content > Import Database
|
||||
|
||||
## Contribution
|
||||
Whether you have ideas, translations, design changes, code cleaning, or real heavy code changes, help is always welcome.
|
||||
@@ -103,6 +101,10 @@ The more is done the better it gets!
|
||||
|
||||
If you'd like to get involved, check our [contribution notes](.github/CONTRIBUTING.md).
|
||||
|
||||
<a href="https://hosted.weblate.org/engage/newpipe/">
|
||||
<img src="https://hosted.weblate.org/widgets/newpipe/-/287x66-grey.png" alt="Translation status" />
|
||||
</a>
|
||||
|
||||
## Donate
|
||||
If you like NewPipe we'd be happy about a donation. You can either send bitcoin or donate via Bountysource or Liberapay. For further info on donating to NewPipe, please visit our [website](https://newpipe.schabi.org/donate).
|
||||
|
||||
|
||||
224
app/build.gradle
224
app/build.gradle
@@ -2,18 +2,21 @@ apply plugin: 'com.android.application'
|
||||
apply plugin: 'kotlin-android'
|
||||
apply plugin: 'kotlin-android-extensions'
|
||||
apply plugin: 'kotlin-kapt'
|
||||
apply plugin: 'checkstyle'
|
||||
|
||||
android {
|
||||
compileSdkVersion 28
|
||||
buildToolsVersion '28.0.3'
|
||||
compileSdkVersion 29
|
||||
buildToolsVersion '29.0.3'
|
||||
|
||||
defaultConfig {
|
||||
applicationId "org.schabi.newpipe"
|
||||
resValue "string", "app_name", "NewPipe"
|
||||
minSdkVersion 19
|
||||
targetSdkVersion 28
|
||||
versionCode 900
|
||||
versionName "0.19.0"
|
||||
targetSdkVersion 29
|
||||
versionCode 960
|
||||
versionName "0.20.6"
|
||||
|
||||
multiDexEnabled true
|
||||
|
||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||
vectorDrawables.useSupportLibrary = true
|
||||
@@ -26,19 +29,12 @@ android {
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
release {
|
||||
minifyEnabled true
|
||||
shrinkResources true
|
||||
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
|
||||
}
|
||||
|
||||
debug {
|
||||
multiDexEnabled true
|
||||
debuggable true
|
||||
|
||||
// suffix the app id and the app name with git branch name
|
||||
def workingBranch = getGitWorkingBranch()
|
||||
def normalizedWorkingBranch = workingBranch.replaceAll("[^A-Za-z]+", "").toLowerCase()
|
||||
def normalizedWorkingBranch = workingBranch.replaceFirst("^[^A-Za-z]+", "").replaceAll("[^0-9A-Za-z]+", "")
|
||||
if (normalizedWorkingBranch.isEmpty() || workingBranch == "master" || workingBranch == "dev") {
|
||||
// default values when branch name could not be determined or is master or dev
|
||||
applicationIdSuffix ".debug"
|
||||
@@ -46,8 +42,19 @@ android {
|
||||
} else {
|
||||
applicationIdSuffix ".debug." + normalizedWorkingBranch
|
||||
resValue "string", "app_name", "NewPipe " + workingBranch
|
||||
archivesBaseName = 'NewPipe_' + normalizedWorkingBranch
|
||||
}
|
||||
}
|
||||
|
||||
// Keep the release build type at the end of the list to override 'archivesBaseName' of
|
||||
// debug build. This seems to be a Gradle bug, therefore
|
||||
// TODO: update Gradle version
|
||||
release {
|
||||
minifyEnabled true
|
||||
shrinkResources false // disabled to fix F-Droid's reproducible build
|
||||
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
|
||||
archivesBaseName = 'app'
|
||||
}
|
||||
}
|
||||
|
||||
lintOptions {
|
||||
@@ -58,8 +65,16 @@ android {
|
||||
}
|
||||
|
||||
compileOptions {
|
||||
// Flag to enable support for the new language APIs
|
||||
coreLibraryDesugaringEnabled true
|
||||
|
||||
sourceCompatibility JavaVersion.VERSION_1_8
|
||||
targetCompatibility JavaVersion.VERSION_1_8
|
||||
encoding 'utf-8'
|
||||
}
|
||||
|
||||
kotlinOptions {
|
||||
jvmTarget = JavaVersion.VERSION_1_8
|
||||
}
|
||||
|
||||
// Required and used only by groupie
|
||||
@@ -73,81 +88,154 @@ android {
|
||||
}
|
||||
|
||||
ext {
|
||||
androidxLibVersion = '1.0.0'
|
||||
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'
|
||||
icepickVersion = '3.2.0'
|
||||
checkstyleVersion = '8.37'
|
||||
stethoVersion = '1.5.1'
|
||||
leakCanaryVersion = '2.5'
|
||||
exoPlayerVersion = '2.11.8'
|
||||
androidxLifecycleVersion = '2.2.0'
|
||||
androidxRoomVersion = '2.3.0-alpha03'
|
||||
groupieVersion = '2.8.1'
|
||||
markwonVersion = '4.6.0'
|
||||
googleAutoServiceVersion = '1.0-rc7'
|
||||
}
|
||||
|
||||
configurations {
|
||||
checkstyle
|
||||
ktlint
|
||||
}
|
||||
|
||||
checkstyle {
|
||||
configFile rootProject.file('checkstyle.xml')
|
||||
ignoreFailures false
|
||||
showViolations true
|
||||
toolVersion = checkstyleVersion
|
||||
}
|
||||
|
||||
task runCheckstyle(type: Checkstyle) {
|
||||
source 'src'
|
||||
include '**/*.java'
|
||||
exclude '**/gen/**'
|
||||
exclude '**/R.java'
|
||||
exclude '**/BuildConfig.java'
|
||||
exclude 'main/java/us/shandian/giga/**'
|
||||
|
||||
classpath = configurations.checkstyle
|
||||
|
||||
showViolations true
|
||||
|
||||
reports {
|
||||
xml.enabled true
|
||||
html.enabled true
|
||||
}
|
||||
}
|
||||
|
||||
def outputDir = "${project.buildDir}/reports/ktlint/"
|
||||
def inputFiles = project.fileTree(dir: "src", include: "**/*.kt")
|
||||
|
||||
task runKtlint(type: JavaExec) {
|
||||
inputs.files(inputFiles)
|
||||
outputs.dir(outputDir)
|
||||
main = "com.pinterest.ktlint.Main"
|
||||
classpath = configurations.ktlint
|
||||
args "src/**/*.kt"
|
||||
}
|
||||
|
||||
task formatKtlint(type: JavaExec) {
|
||||
inputs.files(inputFiles)
|
||||
outputs.dir(outputDir)
|
||||
main = "com.pinterest.ktlint.Main"
|
||||
classpath = configurations.ktlint
|
||||
args "-F", "src/**/*.kt"
|
||||
}
|
||||
|
||||
afterEvaluate {
|
||||
preDebugBuild.dependsOn formatKtlint, runCheckstyle, runKtlint
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
|
||||
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.1.1'
|
||||
|
||||
androidTestImplementation 'androidx.test.ext:junit:1.1.1'
|
||||
androidTestImplementation "android.arch.persistence.room:testing:1.1.1"
|
||||
androidTestImplementation('androidx.test.espresso:espresso-core:3.1.0', {
|
||||
exclude module: 'support-annotations'
|
||||
})
|
||||
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:${kotlin_version}"
|
||||
|
||||
implementation 'com.github.TeamNewPipe:NewPipeExtractor:69e0624e3'
|
||||
testImplementation 'junit:junit:4.12'
|
||||
testImplementation 'org.mockito:mockito-core:2.23.0'
|
||||
implementation "frankiesardo:icepick:${icepickVersion}"
|
||||
kapt "frankiesardo:icepick-processor:${icepickVersion}"
|
||||
|
||||
implementation 'androidx.appcompat:appcompat:1.1.0'
|
||||
implementation "androidx.legacy:legacy-support-v4:${androidxLibVersion}"
|
||||
implementation "com.google.android.material:material:${androidxLibVersion}"
|
||||
implementation "androidx.recyclerview:recyclerview:${androidxLibVersion}"
|
||||
implementation "androidx.legacy:legacy-preference-v14:${androidxLibVersion}"
|
||||
implementation "androidx.cardview:cardview:${androidxLibVersion}"
|
||||
implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
|
||||
checkstyle "com.puppycrawl.tools:checkstyle:${checkstyleVersion}"
|
||||
ktlint "com.pinterest:ktlint:0.39.0"
|
||||
|
||||
implementation 'com.xwray:groupie:2.7.0'
|
||||
implementation 'com.xwray:groupie-kotlin-android-extensions:2.7.0'
|
||||
debugImplementation "com.facebook.stetho:stetho:${stethoVersion}"
|
||||
debugImplementation "com.facebook.stetho:stetho-okhttp3:${stethoVersion}"
|
||||
|
||||
implementation 'androidx.lifecycle:lifecycle-livedata:2.0.0'
|
||||
implementation 'androidx.lifecycle:lifecycle-viewmodel:2.0.0'
|
||||
implementation 'androidx.lifecycle:lifecycle-extensions:2.0.0'
|
||||
debugImplementation "com.squareup.leakcanary:leakcanary-android:${leakCanaryVersion}"
|
||||
implementation "com.squareup.leakcanary:leakcanary-object-watcher-android:${leakCanaryVersion}"
|
||||
implementation "com.squareup.leakcanary:plumber-android:${leakCanaryVersion}"
|
||||
|
||||
// Originally in NewPipeExtractor
|
||||
implementation 'com.grack:nanojson:1.1'
|
||||
implementation 'org.jsoup:jsoup:1.9.2'
|
||||
implementation "androidx.multidex:multidex:2.0.1"
|
||||
|
||||
implementation 'ch.acra:acra:4.9.2' //4.11
|
||||
// NewPipe dependencies
|
||||
// You can use a local version by uncommenting a few lines in settings.gradle
|
||||
implementation 'com.github.TeamNewPipe:NewPipeExtractor:b3835bd616ab28b861c83dcefd56e1754c6d20be'
|
||||
implementation "com.github.TeamNewPipe:nanojson:1d9e1aea9049fc9f85e68b43ba39fe7be1c1f751"
|
||||
|
||||
implementation 'com.nostra13.universalimageloader:universal-image-loader:1.9.5'
|
||||
implementation 'de.hdodenhof:circleimageview:2.2.0'
|
||||
implementation 'com.nononsenseapps:filepicker:4.2.1'
|
||||
implementation "org.jsoup:jsoup:1.13.1"
|
||||
|
||||
implementation "com.google.android.exoplayer:exoplayer:${exoPlayerLibVersion}"
|
||||
implementation "com.google.android.exoplayer:extension-mediasession:${exoPlayerLibVersion}"
|
||||
//noinspection GradleDependency --> do not update okhttp to keep supporting Android 4.4 users
|
||||
implementation "com.squareup.okhttp3:okhttp:3.12.12"
|
||||
|
||||
debugImplementation "com.facebook.stetho:stetho:${stethoLibVersion}"
|
||||
debugImplementation "com.facebook.stetho:stetho-urlconnection:${stethoLibVersion}"
|
||||
debugImplementation 'androidx.multidex:multidex:2.0.1'
|
||||
implementation "com.google.android.exoplayer:exoplayer:${exoPlayerVersion}"
|
||||
implementation "com.google.android.exoplayer:extension-mediasession:${exoPlayerVersion}"
|
||||
|
||||
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 "com.google.android.material:material:1.2.1"
|
||||
|
||||
implementation "androidx.room:room-runtime:${roomDbLibVersion}"
|
||||
implementation "androidx.room:room-rxjava2:${roomDbLibVersion}"
|
||||
kapt "androidx.room:room-compiler:${roomDbLibVersion}"
|
||||
compileOnly "com.google.auto.service:auto-service-annotations:${googleAutoServiceVersion}"
|
||||
kapt "com.google.auto.service:auto-service:${googleAutoServiceVersion}"
|
||||
|
||||
implementation "frankiesardo:icepick:${icepickLibVersion}"
|
||||
kapt "frankiesardo:icepick-processor:${icepickLibVersion}"
|
||||
implementation "androidx.appcompat:appcompat:1.2.0"
|
||||
implementation "androidx.preference:preference:1.1.1"
|
||||
implementation "androidx.recyclerview:recyclerview:1.1.0"
|
||||
implementation "androidx.cardview:cardview:1.0.0"
|
||||
implementation "androidx.constraintlayout:constraintlayout:2.0.4"
|
||||
implementation 'androidx.core:core-ktx:1.3.2'
|
||||
implementation 'androidx.documentfile:documentfile:1.0.1'
|
||||
implementation 'androidx.localbroadcastmanager:localbroadcastmanager:1.0.0'
|
||||
|
||||
debugImplementation "com.squareup.leakcanary:leakcanary-android:${leakCanaryLibVersion}"
|
||||
releaseImplementation "com.squareup.leakcanary:leakcanary-android-no-op:${leakCanaryLibVersion}"
|
||||
implementation "androidx.lifecycle:lifecycle-livedata:${androidxLifecycleVersion}"
|
||||
implementation "androidx.lifecycle:lifecycle-viewmodel:${androidxLifecycleVersion}"
|
||||
|
||||
implementation "com.squareup.okhttp3:okhttp:${okHttpLibVersion}"
|
||||
debugImplementation "com.facebook.stetho:stetho-okhttp3:${stethoLibVersion}"
|
||||
implementation "androidx.room:room-runtime:${androidxRoomVersion}"
|
||||
implementation "androidx.room:room-rxjava3:${androidxRoomVersion}"
|
||||
kapt "androidx.room:room-compiler:${androidxRoomVersion}"
|
||||
|
||||
implementation "androidx.swiperefreshlayout:swiperefreshlayout:1.1.0"
|
||||
|
||||
implementation "com.xwray:groupie:${groupieVersion}"
|
||||
implementation "com.xwray:groupie-kotlin-android-extensions:${groupieVersion}"
|
||||
|
||||
implementation "de.hdodenhof:circleimageview:3.1.0"
|
||||
implementation "com.nostra13.universalimageloader:universal-image-loader:1.9.5"
|
||||
|
||||
implementation "io.noties.markwon:core:${markwonVersion}"
|
||||
implementation "io.noties.markwon:linkify:${markwonVersion}"
|
||||
|
||||
implementation "com.nononsenseapps:filepicker:4.2.1"
|
||||
|
||||
implementation "ch.acra:acra-core:5.7.0"
|
||||
|
||||
implementation "io.reactivex.rxjava3:rxjava:3.0.7"
|
||||
implementation "io.reactivex.rxjava3:rxandroid:3.0.0"
|
||||
implementation "com.jakewharton.rxbinding4:rxbinding:4.0.0"
|
||||
|
||||
implementation "org.ocpsoft.prettytime:prettytime:4.0.6.Final"
|
||||
|
||||
testImplementation 'junit:junit:4.13.1'
|
||||
testImplementation 'org.mockito:mockito-core:3.6.0'
|
||||
|
||||
androidTestImplementation "androidx.test.ext:junit:1.1.2"
|
||||
androidTestImplementation "androidx.room:room-testing:${androidxRoomVersion}"
|
||||
androidTestImplementation "androidx.test.espresso:espresso-core:3.3.0", {
|
||||
exclude module: 'support-annotations'
|
||||
}
|
||||
}
|
||||
|
||||
static String getGitWorkingBranch() {
|
||||
@@ -164,4 +252,4 @@ static String getGitWorkingBranch() {
|
||||
// git was not found
|
||||
return ""
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,37 +25,76 @@ class AppDatabaseTest {
|
||||
private const val DEFAULT_DURATION = 480L
|
||||
private const val DEFAULT_UPLOADER_NAME = "Uploader Test"
|
||||
private const val DEFAULT_THUMBNAIL = "https://example.com/example.jpg"
|
||||
|
||||
private const val DEFAULT_SECOND_SERVICE_ID = 0
|
||||
private const val DEFAULT_SECOND_URL = "https://www.youtube.com/watch?v=ncQU6iBn5Fc"
|
||||
}
|
||||
|
||||
@get:Rule val testHelper = MigrationTestHelper(InstrumentationRegistry.getInstrumentation(),
|
||||
AppDatabase::class.java.canonicalName, FrameworkSQLiteOpenHelperFactory());
|
||||
@get:Rule
|
||||
val testHelper = MigrationTestHelper(
|
||||
InstrumentationRegistry.getInstrumentation(),
|
||||
AppDatabase::class.java.canonicalName, FrameworkSQLiteOpenHelperFactory()
|
||||
)
|
||||
|
||||
@Test
|
||||
fun migrateDatabaseFrom2to3() {
|
||||
val databaseInV2 = testHelper.createDatabase(AppDatabase.DATABASE_NAME, Migrations.DB_VER_2)
|
||||
|
||||
databaseInV2.run {
|
||||
insert("streams", SQLiteDatabase.CONFLICT_FAIL, ContentValues().apply {
|
||||
// put("uid", null)
|
||||
put("service_id", DEFAULT_SERVICE_ID)
|
||||
put("url", DEFAULT_URL)
|
||||
put("title", DEFAULT_TITLE)
|
||||
put("stream_type", DEFAULT_TYPE.name)
|
||||
put("duration", DEFAULT_DURATION)
|
||||
put("uploader", DEFAULT_UPLOADER_NAME)
|
||||
put("thumbnail_url", DEFAULT_THUMBNAIL)
|
||||
})
|
||||
insert(
|
||||
"streams", SQLiteDatabase.CONFLICT_FAIL,
|
||||
ContentValues().apply {
|
||||
// put("uid", null)
|
||||
put("service_id", DEFAULT_SERVICE_ID)
|
||||
put("url", DEFAULT_URL)
|
||||
put("title", DEFAULT_TITLE)
|
||||
put("stream_type", DEFAULT_TYPE.name)
|
||||
put("duration", DEFAULT_DURATION)
|
||||
put("uploader", DEFAULT_UPLOADER_NAME)
|
||||
put("thumbnail_url", DEFAULT_THUMBNAIL)
|
||||
}
|
||||
)
|
||||
insert(
|
||||
"streams", SQLiteDatabase.CONFLICT_FAIL,
|
||||
ContentValues().apply {
|
||||
// put("uid", null)
|
||||
put("service_id", DEFAULT_SECOND_SERVICE_ID)
|
||||
put("url", DEFAULT_SECOND_URL)
|
||||
// put("title", null)
|
||||
// put("stream_type", null)
|
||||
// put("duration", null)
|
||||
// put("uploader", null)
|
||||
// put("thumbnail_url", null)
|
||||
}
|
||||
)
|
||||
insert(
|
||||
"streams", SQLiteDatabase.CONFLICT_FAIL,
|
||||
ContentValues().apply {
|
||||
// put("uid", null)
|
||||
put("service_id", DEFAULT_SERVICE_ID)
|
||||
// put("url", null)
|
||||
// put("title", null)
|
||||
// put("stream_type", null)
|
||||
// put("duration", null)
|
||||
// put("uploader", null)
|
||||
// put("thumbnail_url", null)
|
||||
}
|
||||
)
|
||||
close()
|
||||
}
|
||||
|
||||
testHelper.runMigrationsAndValidate(AppDatabase.DATABASE_NAME, Migrations.DB_VER_3,
|
||||
true, Migrations.MIGRATION_2_3);
|
||||
testHelper.runMigrationsAndValidate(
|
||||
AppDatabase.DATABASE_NAME, Migrations.DB_VER_3,
|
||||
true, Migrations.MIGRATION_2_3
|
||||
)
|
||||
|
||||
val migratedDatabaseV3 = getMigratedDatabase()
|
||||
val listFromDB = migratedDatabaseV3.streamDAO().all.blockingFirst()
|
||||
assertEquals(1, listFromDB.size)
|
||||
|
||||
val streamFromMigratedDatabase = listFromDB.first()
|
||||
// Only expect 2, the one with the null url will be ignored
|
||||
assertEquals(2, listFromDB.size)
|
||||
|
||||
val streamFromMigratedDatabase = listFromDB[0]
|
||||
assertEquals(DEFAULT_SERVICE_ID, streamFromMigratedDatabase.serviceId)
|
||||
assertEquals(DEFAULT_URL, streamFromMigratedDatabase.url)
|
||||
assertEquals(DEFAULT_TITLE, streamFromMigratedDatabase.title)
|
||||
@@ -67,13 +106,29 @@ class AppDatabaseTest {
|
||||
assertNull(streamFromMigratedDatabase.textualUploadDate)
|
||||
assertNull(streamFromMigratedDatabase.uploadDate)
|
||||
assertNull(streamFromMigratedDatabase.isUploadDateApproximation)
|
||||
|
||||
val secondStreamFromMigratedDatabase = listFromDB[1]
|
||||
assertEquals(DEFAULT_SECOND_SERVICE_ID, secondStreamFromMigratedDatabase.serviceId)
|
||||
assertEquals(DEFAULT_SECOND_URL, secondStreamFromMigratedDatabase.url)
|
||||
assertEquals("", secondStreamFromMigratedDatabase.title)
|
||||
// Should fallback to VIDEO_STREAM
|
||||
assertEquals(StreamType.VIDEO_STREAM, secondStreamFromMigratedDatabase.streamType)
|
||||
assertEquals(0, secondStreamFromMigratedDatabase.duration)
|
||||
assertEquals("", secondStreamFromMigratedDatabase.uploader)
|
||||
assertEquals("", secondStreamFromMigratedDatabase.thumbnailUrl)
|
||||
assertNull(secondStreamFromMigratedDatabase.viewCount)
|
||||
assertNull(secondStreamFromMigratedDatabase.textualUploadDate)
|
||||
assertNull(secondStreamFromMigratedDatabase.uploadDate)
|
||||
assertNull(secondStreamFromMigratedDatabase.isUploadDateApproximation)
|
||||
}
|
||||
|
||||
private fun getMigratedDatabase(): AppDatabase {
|
||||
val database: AppDatabase = Room.databaseBuilder(ApplicationProvider.getApplicationContext(),
|
||||
AppDatabase::class.java, AppDatabase.DATABASE_NAME)
|
||||
.build()
|
||||
val database: AppDatabase = Room.databaseBuilder(
|
||||
ApplicationProvider.getApplicationContext(),
|
||||
AppDatabase::class.java, AppDatabase.DATABASE_NAME
|
||||
)
|
||||
.build()
|
||||
testHelper.closeWhenFinished(database)
|
||||
return database
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,37 +1,38 @@
|
||||
package org.schabi.newpipe.report;
|
||||
|
||||
import android.os.Parcel;
|
||||
import androidx.test.filters.LargeTest;
|
||||
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
||||
import androidx.test.filters.LargeTest;
|
||||
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.report.ErrorActivity.ErrorInfo;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
|
||||
/**
|
||||
* Instrumented tests for {@link ErrorInfo}
|
||||
* Instrumented tests for {@link ErrorInfo}.
|
||||
*/
|
||||
@RunWith(AndroidJUnit4.class)
|
||||
@LargeTest
|
||||
public class ErrorInfoTest {
|
||||
|
||||
@Test
|
||||
public void errorInfo_testParcelable() {
|
||||
ErrorInfo info = ErrorInfo.make(UserAction.USER_REPORT, "youtube", "request", R.string.general_error);
|
||||
public void errorInfoTestParcelable() {
|
||||
final ErrorInfo info = ErrorInfo.make(UserAction.USER_REPORT, "youtube", "request",
|
||||
R.string.general_error);
|
||||
// Obtain a Parcel object and write the parcelable object to it:
|
||||
Parcel parcel = Parcel.obtain();
|
||||
final Parcel parcel = Parcel.obtain();
|
||||
info.writeToParcel(parcel, 0);
|
||||
parcel.setDataPosition(0);
|
||||
ErrorInfo infoFromParcel = ErrorInfo.CREATOR.createFromParcel(parcel);
|
||||
final ErrorInfo infoFromParcel = ErrorInfo.CREATOR.createFromParcel(parcel);
|
||||
|
||||
assertEquals(UserAction.USER_REPORT, infoFromParcel.userAction);
|
||||
assertEquals("youtube", infoFromParcel.serviceName);
|
||||
assertEquals("request", infoFromParcel.request);
|
||||
assertEquals(R.string.general_error, infoFromParcel.message);
|
||||
assertEquals(UserAction.USER_REPORT, infoFromParcel.getUserAction());
|
||||
assertEquals("youtube", infoFromParcel.getServiceName());
|
||||
assertEquals("request", infoFromParcel.getRequest());
|
||||
assertEquals(R.string.general_error, infoFromParcel.getMessage());
|
||||
|
||||
parcel.recycle();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,104 +0,0 @@
|
||||
package org.schabi.newpipe;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.SharedPreferences;
|
||||
import android.preference.PreferenceManager;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.multidex.MultiDex;
|
||||
|
||||
import com.facebook.stetho.Stetho;
|
||||
import com.facebook.stetho.okhttp3.StethoInterceptor;
|
||||
import com.squareup.leakcanary.AndroidHeapDumper;
|
||||
import com.squareup.leakcanary.DefaultLeakDirectoryProvider;
|
||||
import com.squareup.leakcanary.HeapDumper;
|
||||
import com.squareup.leakcanary.LeakCanary;
|
||||
import com.squareup.leakcanary.LeakDirectoryProvider;
|
||||
import com.squareup.leakcanary.RefWatcher;
|
||||
|
||||
import org.schabi.newpipe.extractor.downloader.Downloader;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import okhttp3.OkHttpClient;
|
||||
|
||||
public class DebugApp extends App {
|
||||
private static final String TAG = DebugApp.class.toString();
|
||||
|
||||
@Override
|
||||
protected void attachBaseContext(Context base) {
|
||||
super.attachBaseContext(base);
|
||||
MultiDex.install(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate() {
|
||||
super.onCreate();
|
||||
initStetho();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Downloader getDownloader() {
|
||||
return DownloaderImpl.init(new OkHttpClient.Builder()
|
||||
.addNetworkInterceptor(new StethoInterceptor()));
|
||||
}
|
||||
|
||||
private void initStetho() {
|
||||
// Create an InitializerBuilder
|
||||
Stetho.InitializerBuilder initializerBuilder =
|
||||
Stetho.newInitializerBuilder(this);
|
||||
|
||||
// Enable Chrome DevTools
|
||||
initializerBuilder.enableWebKitInspector(
|
||||
Stetho.defaultInspectorModulesProvider(this)
|
||||
);
|
||||
|
||||
// Enable command line interface
|
||||
initializerBuilder.enableDumpapp(
|
||||
Stetho.defaultDumperPluginsProvider(getApplicationContext())
|
||||
);
|
||||
|
||||
// Use the InitializerBuilder to generate an Initializer
|
||||
Stetho.Initializer initializer = initializerBuilder.build();
|
||||
|
||||
// Initialize Stetho with the Initializer
|
||||
Stetho.initialize(initializer);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean isDisposedRxExceptionsReported() {
|
||||
return PreferenceManager.getDefaultSharedPreferences(this)
|
||||
.getBoolean(getString(R.string.allow_disposed_exceptions_key), false);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected RefWatcher installLeakCanary() {
|
||||
return LeakCanary.refWatcher(this)
|
||||
.heapDumper(new ToggleableHeapDumper(this))
|
||||
// give each object 10 seconds to be gc'ed, before leak canary gets nosy on it
|
||||
.watchDelay(10, TimeUnit.SECONDS)
|
||||
.buildAndInstall();
|
||||
}
|
||||
|
||||
public static class ToggleableHeapDumper implements HeapDumper {
|
||||
private final HeapDumper dumper;
|
||||
private final SharedPreferences preferences;
|
||||
private final String dumpingAllowanceKey;
|
||||
|
||||
ToggleableHeapDumper(@NonNull final Context context) {
|
||||
LeakDirectoryProvider leakDirectoryProvider = new DefaultLeakDirectoryProvider(context);
|
||||
this.dumper = new AndroidHeapDumper(context, leakDirectoryProvider);
|
||||
this.preferences = PreferenceManager.getDefaultSharedPreferences(context);
|
||||
this.dumpingAllowanceKey = context.getString(R.string.allow_heap_dumping_key);
|
||||
}
|
||||
|
||||
private boolean isDumpingAllowed() {
|
||||
return preferences.getBoolean(dumpingAllowanceKey, false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public File dumpHeap() {
|
||||
return isDumpingAllowed() ? dumper.dumpHeap() : HeapDumper.RETRY_LATER;
|
||||
}
|
||||
}
|
||||
}
|
||||
61
app/src/debug/java/org/schabi/newpipe/DebugApp.kt
Normal file
61
app/src/debug/java/org/schabi/newpipe/DebugApp.kt
Normal file
@@ -0,0 +1,61 @@
|
||||
package org.schabi.newpipe
|
||||
|
||||
import androidx.preference.PreferenceManager
|
||||
import com.facebook.stetho.Stetho
|
||||
import com.facebook.stetho.okhttp3.StethoInterceptor
|
||||
import leakcanary.AppWatcher
|
||||
import leakcanary.LeakCanary
|
||||
import okhttp3.OkHttpClient
|
||||
import org.schabi.newpipe.extractor.downloader.Downloader
|
||||
|
||||
class DebugApp : App() {
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
initStetho()
|
||||
|
||||
// Give each object 10 seconds to be GC'ed, before LeakCanary gets nosy on it
|
||||
AppWatcher.config = AppWatcher.config.copy(watchDurationMillis = 10000)
|
||||
LeakCanary.config = LeakCanary.config.copy(
|
||||
dumpHeap = PreferenceManager
|
||||
.getDefaultSharedPreferences(this).getBoolean(
|
||||
getString(
|
||||
R.string.allow_heap_dumping_key
|
||||
),
|
||||
false
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
override fun getDownloader(): Downloader {
|
||||
val downloader = DownloaderImpl.init(
|
||||
OkHttpClient.Builder()
|
||||
.addNetworkInterceptor(StethoInterceptor())
|
||||
)
|
||||
setCookiesToDownloader(downloader)
|
||||
return downloader
|
||||
}
|
||||
|
||||
private fun initStetho() {
|
||||
// Create an InitializerBuilder
|
||||
val initializerBuilder = Stetho.newInitializerBuilder(this)
|
||||
|
||||
// Enable Chrome DevTools
|
||||
initializerBuilder.enableWebKitInspector(Stetho.defaultInspectorModulesProvider(this))
|
||||
|
||||
// Enable command line interface
|
||||
initializerBuilder.enableDumpapp(
|
||||
Stetho.defaultDumperPluginsProvider(applicationContext)
|
||||
)
|
||||
|
||||
// Use the InitializerBuilder to generate an Initializer
|
||||
val initializer = initializerBuilder.build()
|
||||
|
||||
// Initialize Stetho with the Initializer
|
||||
Stetho.initialize(initializer)
|
||||
}
|
||||
|
||||
override fun isDisposedRxExceptionsReported(): Boolean {
|
||||
return PreferenceManager.getDefaultSharedPreferences(this)
|
||||
.getBoolean(getString(R.string.allow_disposed_exceptions_key), false)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
package org.schabi.newpipe.settings;
|
||||
|
||||
import android.os.Bundle;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import org.schabi.newpipe.R;
|
||||
|
||||
import leakcanary.LeakCanary;
|
||||
|
||||
public class DebugSettingsFragment extends BasePreferenceFragment {
|
||||
@Override
|
||||
public void onCreate(@Nullable final Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
findPreference(getString(R.string.show_memory_leaks_key))
|
||||
.setOnPreferenceClickListener(preference -> {
|
||||
startActivity(LeakCanary.INSTANCE.newLeakDisplayActivityIntent());
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreatePreferences(final Bundle savedInstanceState, final String rootKey) {
|
||||
addPreferencesFromResource(R.xml.debug_settings);
|
||||
}
|
||||
}
|
||||
56
app/src/debug/res/xml/main_settings.xml
Normal file
56
app/src/debug/res/xml/main_settings.xml
Normal file
@@ -0,0 +1,56 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:key="general_preferences"
|
||||
android:title="@string/settings">
|
||||
|
||||
<PreferenceScreen
|
||||
android:fragment="org.schabi.newpipe.settings.VideoAudioSettingsFragment"
|
||||
android:icon="?attr/ic_headset"
|
||||
android:title="@string/settings_category_video_audio_title"
|
||||
app:iconSpaceReserved="false" />
|
||||
|
||||
<PreferenceScreen
|
||||
android:fragment="org.schabi.newpipe.settings.DownloadSettingsFragment"
|
||||
android:icon="?attr/ic_file_download"
|
||||
android:title="@string/settings_category_downloads_title"
|
||||
app:iconSpaceReserved="false" />
|
||||
|
||||
<PreferenceScreen
|
||||
android:fragment="org.schabi.newpipe.settings.AppearanceSettingsFragment"
|
||||
android:icon="?attr/ic_palette"
|
||||
android:title="@string/settings_category_appearance_title"
|
||||
app:iconSpaceReserved="false" />
|
||||
|
||||
<PreferenceScreen
|
||||
android:fragment="org.schabi.newpipe.settings.HistorySettingsFragment"
|
||||
android:icon="?attr/ic_history"
|
||||
android:title="@string/settings_category_history_title"
|
||||
app:iconSpaceReserved="false" />
|
||||
|
||||
<PreferenceScreen
|
||||
android:fragment="org.schabi.newpipe.settings.ContentSettingsFragment"
|
||||
android:icon="?attr/ic_language"
|
||||
android:title="@string/content"
|
||||
app:iconSpaceReserved="false" />
|
||||
|
||||
<PreferenceScreen
|
||||
android:fragment="org.schabi.newpipe.settings.NotificationSettingsFragment"
|
||||
android:icon="?attr/ic_play_arrow"
|
||||
android:title="@string/settings_category_notification_title"
|
||||
app:iconSpaceReserved="false" />
|
||||
|
||||
<PreferenceScreen
|
||||
android:fragment="org.schabi.newpipe.settings.UpdateSettingsFragment"
|
||||
android:icon="?attr/ic_settings_update"
|
||||
android:key="update_pref_screen_key"
|
||||
android:title="@string/settings_category_updates_title"
|
||||
app:iconSpaceReserved="false" />
|
||||
|
||||
<PreferenceScreen
|
||||
android:fragment="org.schabi.newpipe.settings.DebugSettingsFragment"
|
||||
android:icon="?attr/ic_bug_report"
|
||||
android:key="@string/debug_pref_screen_key"
|
||||
android:title="@string/settings_category_debug_title"
|
||||
app:iconSpaceReserved="false" />
|
||||
</PreferenceScreen>
|
||||
@@ -1,43 +1,52 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
package="org.schabi.newpipe">
|
||||
|
||||
<uses-permission android:name="android.permission.INTERNET"/>
|
||||
<uses-permission android:name="android.permission.WAKE_LOCK"/>
|
||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
|
||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
|
||||
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW"/>
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<uses-permission android:name="android.permission.WAKE_LOCK" />
|
||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
|
||||
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||
|
||||
<uses-feature
|
||||
android:name="android.hardware.touchscreen"
|
||||
android:required="false" />
|
||||
|
||||
<application
|
||||
android:name=".App"
|
||||
android:allowBackup="true"
|
||||
android:banner="@mipmap/newpipe_tv_banner"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:label="@string/app_name"
|
||||
android:logo="@mipmap/ic_launcher"
|
||||
android:requestLegacyExternalStorage="true"
|
||||
android:theme="@style/OpeningTheme"
|
||||
android:resizeableActivity="true"
|
||||
tools:ignore="AllowBackup">
|
||||
<activity
|
||||
android:name=".MainActivity"
|
||||
android:label="@string/app_name"
|
||||
android:launchMode="singleTask">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN"/>
|
||||
<category android:name="android.intent.category.LAUNCHER"/>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
<category android:name="android.intent.category.LEANBACK_LAUNCHER" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<receiver android:name="androidx.media.session.MediaButtonReceiver" >
|
||||
<receiver android:name="androidx.media.session.MediaButtonReceiver">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MEDIA_BUTTON" />
|
||||
</intent-filter>
|
||||
</receiver>
|
||||
|
||||
<service
|
||||
android:name=".player.BackgroundPlayer"
|
||||
android:exported="false">
|
||||
android:name=".player.MainPlayer"
|
||||
android:exported="false"
|
||||
android:foregroundServiceType="mediaPlayback">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MEDIA_BUTTON" />
|
||||
</intent-filter>
|
||||
@@ -45,36 +54,20 @@
|
||||
|
||||
<activity
|
||||
android:name=".player.BackgroundPlayerActivity"
|
||||
android:launchMode="singleTask"
|
||||
android:label="@string/title_activity_background_player"/>
|
||||
|
||||
<activity
|
||||
android:name=".player.PopupVideoPlayerActivity"
|
||||
android:launchMode="singleTask"
|
||||
android:label="@string/title_activity_popup_player"/>
|
||||
|
||||
<service
|
||||
android:name=".player.PopupVideoPlayer"
|
||||
android:exported="false"/>
|
||||
|
||||
<activity
|
||||
android:name=".player.MainVideoPlayer"
|
||||
android:configChanges="keyboard|keyboardHidden|orientation|screenSize"
|
||||
android:label="@string/app_name"
|
||||
android:launchMode="singleTask"
|
||||
android:theme="@style/VideoPlayerTheme"/>
|
||||
android:label="@string/title_activity_play_queue"
|
||||
android:launchMode="singleTask" />
|
||||
|
||||
<activity
|
||||
android:name=".settings.SettingsActivity"
|
||||
android:label="@string/settings"/>
|
||||
android:label="@string/settings" />
|
||||
|
||||
<activity
|
||||
android:name=".about.AboutActivity"
|
||||
android:label="@string/title_activity_about"/>
|
||||
android:label="@string/title_activity_about" />
|
||||
|
||||
<service android:name=".local.subscription.services.SubscriptionsImportService"/>
|
||||
<service android:name=".local.subscription.services.SubscriptionsExportService"/>
|
||||
<service android:name=".local.feed.service.FeedLoadService"/>
|
||||
<service android:name=".local.subscription.services.SubscriptionsImportService" />
|
||||
<service android:name=".local.subscription.services.SubscriptionsExportService" />
|
||||
<service android:name=".local.feed.service.FeedLoadService" />
|
||||
|
||||
<activity
|
||||
android:name=".PanicResponderActivity"
|
||||
@@ -82,25 +75,25 @@
|
||||
android:noHistory="true"
|
||||
android:theme="@android:style/Theme.NoDisplay">
|
||||
<intent-filter>
|
||||
<action android:name="info.guardianproject.panic.action.TRIGGER"/>
|
||||
<action android:name="info.guardianproject.panic.action.TRIGGER" />
|
||||
|
||||
<category android:name="android.intent.category.DEFAULT"/>
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<activity
|
||||
android:name=".ExitActivity"
|
||||
android:label="@string/general_error"
|
||||
android:theme="@android:style/Theme.NoDisplay"/>
|
||||
<activity android:name=".report.ErrorActivity"/>
|
||||
android:theme="@android:style/Theme.NoDisplay" />
|
||||
<activity android:name=".report.ErrorActivity" />
|
||||
|
||||
<!-- giga get related -->
|
||||
<activity
|
||||
android:name=".download.DownloadActivity"
|
||||
android:label="@string/app_name"
|
||||
android:launchMode="singleTask"/>
|
||||
android:launchMode="singleTask" />
|
||||
|
||||
<service android:name="us.shandian.giga.service.DownloadManagerService"/>
|
||||
<service android:name="us.shandian.giga.service.DownloadManagerService" />
|
||||
|
||||
<activity
|
||||
android:name=".util.FilePickerActivityHelper"
|
||||
@@ -114,7 +107,7 @@
|
||||
|
||||
<activity
|
||||
android:name=".ReCaptchaActivity"
|
||||
android:label="@string/recaptcha"/>
|
||||
android:label="@string/recaptcha" />
|
||||
|
||||
<provider
|
||||
android:name="androidx.core.content.FileProvider"
|
||||
@@ -123,7 +116,7 @@
|
||||
android:grantUriPermissions="true">
|
||||
<meta-data
|
||||
android:name="android.support.FILE_PROVIDER_PATHS"
|
||||
android:resource="@xml/nnf_provider_paths"/>
|
||||
android:resource="@xml/nnf_provider_paths" />
|
||||
</provider>
|
||||
|
||||
<activity
|
||||
@@ -135,165 +128,202 @@
|
||||
|
||||
<!-- Youtube filter -->
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW"/>
|
||||
<action android:name="android.media.action.MEDIA_PLAY_FROM_SEARCH"/>
|
||||
<action android:name="android.nfc.action.NDEF_DISCOVERED"/>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
<action android:name="android.media.action.MEDIA_PLAY_FROM_SEARCH" />
|
||||
<action android:name="android.nfc.action.NDEF_DISCOVERED" />
|
||||
|
||||
<category android:name="android.intent.category.DEFAULT"/>
|
||||
<category android:name="android.intent.category.BROWSABLE"/>
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
|
||||
<data android:scheme="http"/>
|
||||
<data android:scheme="https"/>
|
||||
<data android:host="youtube.com"/>
|
||||
<data android:host="m.youtube.com"/>
|
||||
<data android:host="www.youtube.com"/>
|
||||
<data android:host="music.youtube.com"/>
|
||||
<data android:scheme="http" />
|
||||
<data android:scheme="https" />
|
||||
<data android:host="youtube.com" />
|
||||
<data android:host="m.youtube.com" />
|
||||
<data android:host="www.youtube.com" />
|
||||
<data android:host="music.youtube.com" />
|
||||
<!-- video prefix -->
|
||||
<data android:pathPrefix="/v/"/>
|
||||
<data android:pathPrefix="/embed/"/>
|
||||
<data android:pathPrefix="/watch"/>
|
||||
<data android:pathPrefix="/attribution_link"/>
|
||||
<data android:pathPrefix="/v/" />
|
||||
<data android:pathPrefix="/embed/" />
|
||||
<data android:pathPrefix="/watch" />
|
||||
<data android:pathPrefix="/attribution_link" />
|
||||
<!-- channel prefix -->
|
||||
<data android:pathPrefix="/channel/"/>
|
||||
<data android:pathPrefix="/user/"/>
|
||||
<data android:pathPrefix="/c/"/>
|
||||
<data android:pathPrefix="/channel/" />
|
||||
<data android:pathPrefix="/user/" />
|
||||
<data android:pathPrefix="/c/" />
|
||||
<!-- playlist prefix -->
|
||||
<data android:pathPrefix="/playlist"/>
|
||||
<data android:pathPrefix="/playlist" />
|
||||
</intent-filter>
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW"/>
|
||||
<action android:name="android.media.action.MEDIA_PLAY_FROM_SEARCH"/>
|
||||
<action android:name="android.nfc.action.NDEF_DISCOVERED"/>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
<action android:name="android.media.action.MEDIA_PLAY_FROM_SEARCH" />
|
||||
<action android:name="android.nfc.action.NDEF_DISCOVERED" />
|
||||
|
||||
<category android:name="android.intent.category.DEFAULT"/>
|
||||
<category android:name="android.intent.category.BROWSABLE"/>
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
|
||||
<data android:scheme="http"/>
|
||||
<data android:scheme="https"/>
|
||||
<data android:host="youtu.be"/>
|
||||
<data android:pathPrefix="/"/>
|
||||
<data android:scheme="http" />
|
||||
<data android:scheme="https" />
|
||||
<data android:host="youtu.be" />
|
||||
<data android:pathPrefix="/" />
|
||||
</intent-filter>
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW"/>
|
||||
<action android:name="android.media.action.MEDIA_PLAY_FROM_SEARCH"/>
|
||||
<action android:name="android.nfc.action.NDEF_DISCOVERED"/>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
<action android:name="android.media.action.MEDIA_PLAY_FROM_SEARCH" />
|
||||
<action android:name="android.nfc.action.NDEF_DISCOVERED" />
|
||||
|
||||
<category android:name="android.intent.category.DEFAULT"/>
|
||||
<category android:name="android.intent.category.BROWSABLE"/>
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
|
||||
<data android:scheme="http"/>
|
||||
<data android:scheme="https"/>
|
||||
<data android:host="www.youtube-nocookie.com"/>
|
||||
<data android:pathPrefix="/embed/"/>
|
||||
<data android:scheme="http" />
|
||||
<data android:scheme="https" />
|
||||
<data android:host="www.youtube-nocookie.com" />
|
||||
<data android:pathPrefix="/embed/" />
|
||||
</intent-filter>
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW"/>
|
||||
<action android:name="android.media.action.MEDIA_PLAY_FROM_SEARCH"/>
|
||||
<action android:name="android.nfc.action.NDEF_DISCOVERED"/>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
<action android:name="android.media.action.MEDIA_PLAY_FROM_SEARCH" />
|
||||
<action android:name="android.nfc.action.NDEF_DISCOVERED" />
|
||||
|
||||
<category android:name="android.intent.category.DEFAULT"/>
|
||||
<category android:name="android.intent.category.BROWSABLE"/>
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
|
||||
<data android:scheme="vnd.youtube"/>
|
||||
<data android:scheme="vnd.youtube.launch"/>
|
||||
<data android:scheme="vnd.youtube" />
|
||||
<data android:scheme="vnd.youtube.launch" />
|
||||
</intent-filter>
|
||||
|
||||
<!-- Hooktube filter -->
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW"/>
|
||||
<action android:name="android.media.action.MEDIA_PLAY_FROM_SEARCH"/>
|
||||
<action android:name="android.nfc.action.NDEF_DISCOVERED"/>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
<action android:name="android.media.action.MEDIA_PLAY_FROM_SEARCH" />
|
||||
<action android:name="android.nfc.action.NDEF_DISCOVERED" />
|
||||
|
||||
<category android:name="android.intent.category.DEFAULT"/>
|
||||
<category android:name="android.intent.category.BROWSABLE"/>
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
|
||||
<data android:scheme="http"/>
|
||||
<data android:scheme="https"/>
|
||||
<data android:host="hooktube.com"/>
|
||||
<data android:host="*.hooktube.com"/>
|
||||
<data android:scheme="http" />
|
||||
<data android:scheme="https" />
|
||||
<data android:host="hooktube.com" />
|
||||
<data android:host="*.hooktube.com" />
|
||||
<!-- video prefix -->
|
||||
<data android:pathPrefix="/v/"/>
|
||||
<data android:pathPrefix="/embed/"/>
|
||||
<data android:pathPrefix="/watch"/>
|
||||
<data android:pathPrefix="/v/" />
|
||||
<data android:pathPrefix="/embed/" />
|
||||
<data android:pathPrefix="/watch" />
|
||||
<!-- channel prefix -->
|
||||
<data android:pathPrefix="/channel/"/>
|
||||
<data android:pathPrefix="/user/"/>
|
||||
<data android:pathPrefix="/channel/" />
|
||||
<data android:pathPrefix="/user/" />
|
||||
</intent-filter>
|
||||
|
||||
<!-- Invidious filter -->
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW"/>
|
||||
<action android:name="android.media.action.MEDIA_PLAY_FROM_SEARCH"/>
|
||||
<action android:name="android.nfc.action.NDEF_DISCOVERED"/>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
<action android:name="android.media.action.MEDIA_PLAY_FROM_SEARCH" />
|
||||
<action android:name="android.nfc.action.NDEF_DISCOVERED" />
|
||||
|
||||
<category android:name="android.intent.category.DEFAULT"/>
|
||||
<category android:name="android.intent.category.BROWSABLE"/>
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
|
||||
<data android:scheme="http"/>
|
||||
<data android:scheme="https"/>
|
||||
<data android:host="invidio.us"/>
|
||||
<data android:host="dev.invidio.us"/>
|
||||
<data android:host="www.invidio.us"/>
|
||||
<data android:host="invidious.snopyta.org"/>
|
||||
<data android:host="de.invidious.snopyta.org"/>
|
||||
<data android:host="fi.invidious.snopyta.org"/>
|
||||
<data android:host="vid.wxzm.sx"/>
|
||||
<data android:host="invidious.kabi.tk"/>
|
||||
<data android:host="invidiou.sh"/>
|
||||
<data android:host="www.invidiou.sh"/>
|
||||
<data android:host="no.invidiou.sh"/>
|
||||
<data android:host="invidious.enkirton.net"/>
|
||||
<data android:host="tube.poal.co"/>
|
||||
<data android:host="invidious.13ad.de"/>
|
||||
<data android:host="yt.elukerio.org"/>
|
||||
<data android:pathPrefix="/"/>
|
||||
<data android:scheme="http" />
|
||||
<data android:scheme="https" />
|
||||
<data android:host="invidio.us" />
|
||||
<data android:host="dev.invidio.us" />
|
||||
<data android:host="www.invidio.us" />
|
||||
<data android:host="invidious.snopyta.org" />
|
||||
<data android:host="fi.invidious.snopyta.org" />
|
||||
<data android:host="yewtu.be" />
|
||||
<data android:host="invidious.ggc-project.de" />
|
||||
<data android:host="yt.maisputain.ovh" />
|
||||
<data android:host="invidious.13ad.de" />
|
||||
<data android:host="invidious.toot.koeln" />
|
||||
<data android:host="invidious.fdn.fr" />
|
||||
<data android:host="watch.nettohikari.com" />
|
||||
<data android:host="invidious.snwmds.net" />
|
||||
<data android:host="invidious.snwmds.org" />
|
||||
<data android:host="invidious.snwmds.com" />
|
||||
<data android:host="invidious.sunsetravens.com" />
|
||||
<data android:host="invidious.gachirangers.com" />
|
||||
<data android:pathPrefix="/" />
|
||||
</intent-filter>
|
||||
|
||||
<!-- Soundcloud filter -->
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW"/>
|
||||
<action android:name="android.media.action.MEDIA_PLAY_FROM_SEARCH"/>
|
||||
<action android:name="android.nfc.action.NDEF_DISCOVERED"/>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
<action android:name="android.media.action.MEDIA_PLAY_FROM_SEARCH" />
|
||||
<action android:name="android.nfc.action.NDEF_DISCOVERED" />
|
||||
|
||||
<category android:name="android.intent.category.DEFAULT"/>
|
||||
<category android:name="android.intent.category.BROWSABLE"/>
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
|
||||
<data android:scheme="http"/>
|
||||
<data android:scheme="https"/>
|
||||
<data android:host="soundcloud.com"/>
|
||||
<data android:host="m.soundcloud.com"/>
|
||||
<data android:host="www.soundcloud.com"/>
|
||||
<data android:pathPrefix="/"/>
|
||||
<data android:scheme="http" />
|
||||
<data android:scheme="https" />
|
||||
<data android:host="soundcloud.com" />
|
||||
<data android:host="m.soundcloud.com" />
|
||||
<data android:host="www.soundcloud.com" />
|
||||
<data android:pathPrefix="/" />
|
||||
</intent-filter>
|
||||
|
||||
<!-- Share filter -->
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.SEND"/>
|
||||
<category android:name="android.intent.category.DEFAULT"/>
|
||||
<data android:mimeType="text/plain"/>
|
||||
<action android:name="android.intent.action.SEND" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<data android:mimeType="text/plain" />
|
||||
</intent-filter>
|
||||
|
||||
<!-- MediaCCC filter -->
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW"/>
|
||||
<action android:name="android.media.action.MEDIA_PLAY_FROM_SEARCH"/>
|
||||
<action android:name="android.nfc.action.NDEF_DISCOVERED"/>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
<action android:name="android.media.action.MEDIA_PLAY_FROM_SEARCH" />
|
||||
<action android:name="android.nfc.action.NDEF_DISCOVERED" />
|
||||
|
||||
<category android:name="android.intent.category.DEFAULT"/>
|
||||
<category android:name="android.intent.category.BROWSABLE"/>
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
|
||||
<data android:scheme="http"/>
|
||||
<data android:scheme="https"/>
|
||||
<data android:host="media.ccc.de"/>
|
||||
<data android:scheme="http" />
|
||||
<data android:scheme="https" />
|
||||
<data android:host="media.ccc.de" />
|
||||
<!-- video prefix -->
|
||||
<data android:pathPrefix="/v/"/>
|
||||
<data android:pathPrefix="/v/" />
|
||||
<!-- channel prefix-->
|
||||
<data android:pathPrefix="/c/"/>
|
||||
<data android:pathPrefix="/b/"/>
|
||||
<data android:pathPrefix="/c/" />
|
||||
<data android:pathPrefix="/b/" />
|
||||
</intent-filter>
|
||||
|
||||
<!-- PeerTube filter -->
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
<action android:name="android.media.action.MEDIA_PLAY_FROM_SEARCH" />
|
||||
<action android:name="android.nfc.action.NDEF_DISCOVERED" />
|
||||
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
|
||||
<data android:scheme="http" />
|
||||
<data android:scheme="https" />
|
||||
|
||||
<data android:host="framatube.org" />
|
||||
<data android:host="media.assassinate-you.net" />
|
||||
<data android:host="peertube.co.uk" />
|
||||
<data android:host="peertube.cpy.re" />
|
||||
<data android:host="peertube.mastodon.host" />
|
||||
<data android:host="peertube.fr" />
|
||||
<data android:host="tilvids.com" />
|
||||
<data android:host="tube.privacytools.io" />
|
||||
<data android:host="video.ploud.fr" />
|
||||
<data android:host="video.lqdn.fr" />
|
||||
<data android:host="skeptikon.fr" />
|
||||
|
||||
<data android:pathPrefix="/videos/" /> <!-- it contains playlists -->
|
||||
<data android:pathPrefix="/accounts/" />
|
||||
<data android:pathPrefix="/video-channels/" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
<service
|
||||
android:name=".RouterActivity$FetcherService"
|
||||
android:exported="false"/>
|
||||
android:exported="false" />
|
||||
|
||||
<!-- see https://github.com/TeamNewPipe/NewPipe/issues/3947 -->
|
||||
<!-- Version < 3.0. DeX Mode and Screen Mirroring support -->
|
||||
<meta-data android:name="com.samsung.android.keepalive.density" android:value="true"/>
|
||||
<!-- Version >= 3.0. DeX Dual Mode support -->
|
||||
<meta-data android:name="com.samsung.android.multidisplay.keep_process_alive" android:value="true"/>
|
||||
</application>
|
||||
</manifest>
|
||||
|
||||
@@ -1,400 +0,0 @@
|
||||
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
|
||||
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
|
||||
|
||||
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
|
||||
<head>
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
|
||||
<title>GNU General Public License v2.0 - GNU Project - Free Software Foundation (FSF)</title>
|
||||
<link rel="alternate" type="application/rdf+xml"
|
||||
href="http://www.gnu.org/licenses/old-licenses/gpl-2.0.rdf" />
|
||||
</head>
|
||||
<body>
|
||||
<h3><a id="SEC1">GNU GENERAL PUBLIC LICENSE</a></h3>
|
||||
<p>
|
||||
Version 2, June 1991
|
||||
</p>
|
||||
|
||||
<pre>
|
||||
Copyright (C) 1989, 1991 Free Software Foundation, Inc.<br/>
|
||||
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA<br/>
|
||||
<br/>
|
||||
Everyone is permitted to copy and distribute verbatim copies
|
||||
of this license document, but changing it is not allowed.
|
||||
</pre>
|
||||
|
||||
<h3 id="preamble"><a id="SEC2">Preamble</a></h3>
|
||||
|
||||
<p>
|
||||
The licenses for most software are designed to take away your
|
||||
freedom to share and change it. By contrast, the GNU General Public
|
||||
License is intended to guarantee your freedom to share and change free
|
||||
software--to make sure the software is free for all its users. This
|
||||
General Public License applies to most of the Free Software
|
||||
Foundation's software and to any other program whose authors commit to
|
||||
using it. (Some other Free Software Foundation software is covered by
|
||||
the GNU Lesser General Public License instead.) You can apply it to
|
||||
your programs, too.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
When we speak of free software, we are referring to freedom, not
|
||||
price. Our General Public Licenses are designed to make sure that you
|
||||
have the freedom to distribute copies of free software (and charge for
|
||||
this service if you wish), that you receive source code or can get it
|
||||
if you want it, that you can change the software or use pieces of it
|
||||
in new free programs; and that you know you can do these things.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
To protect your rights, we need to make restrictions that forbid
|
||||
anyone to deny you these rights or to ask you to surrender the rights.
|
||||
These restrictions translate to certain responsibilities for you if you
|
||||
distribute copies of the software, or if you modify it.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
For example, if you distribute copies of such a program, whether
|
||||
gratis or for a fee, you must give the recipients all the rights that
|
||||
you have. You must make sure that they, too, receive or can get the
|
||||
source code. And you must show them these terms so they know their
|
||||
rights.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
We protect your rights with two steps: (1) copyright the software, and
|
||||
(2) offer you this license which gives you legal permission to copy,
|
||||
distribute and/or modify the software.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
Also, for each author's protection and ours, we want to make certain
|
||||
that everyone understands that there is no warranty for this free
|
||||
software. If the software is modified by someone else and passed on, we
|
||||
want its recipients to know that what they have is not the original, so
|
||||
that any problems introduced by others will not reflect on the original
|
||||
authors' reputations.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
Finally, any free program is threatened constantly by software
|
||||
patents. We wish to avoid the danger that redistributors of a free
|
||||
program will individually obtain patent licenses, in effect making the
|
||||
program proprietary. To prevent this, we have made it clear that any
|
||||
patent must be licensed for everyone's free use or not licensed at all.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
The precise terms and conditions for copying, distribution and
|
||||
modification follow.
|
||||
</p>
|
||||
|
||||
|
||||
<h3 id="terms"><a id="SEC3">TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION</a></h3>
|
||||
|
||||
|
||||
<p id="section0">
|
||||
<strong>0.</strong>
|
||||
This License applies to any program or other work which contains
|
||||
a notice placed by the copyright holder saying it may be distributed
|
||||
under the terms of this General Public License. The "Program", below,
|
||||
refers to any such program or work, and a "work based on the Program"
|
||||
means either the Program or any derivative work under copyright law:
|
||||
that is to say, a work containing the Program or a portion of it,
|
||||
either verbatim or with modifications and/or translated into another
|
||||
language. (Hereinafter, translation is included without limitation in
|
||||
the term "modification".) Each licensee is addressed as "you".
|
||||
</p>
|
||||
|
||||
<p>
|
||||
Activities other than copying, distribution and modification are not
|
||||
covered by this License; they are outside its scope. The act of
|
||||
running the Program is not restricted, and the output from the Program
|
||||
is covered only if its contents constitute a work based on the
|
||||
Program (independent of having been made by running the Program).
|
||||
Whether that is true depends on what the Program does.
|
||||
</p>
|
||||
|
||||
<p id="section1">
|
||||
<strong>1.</strong>
|
||||
You may copy and distribute verbatim copies of the Program's
|
||||
source code as you receive it, in any medium, provided that you
|
||||
conspicuously and appropriately publish on each copy an appropriate
|
||||
copyright notice and disclaimer of warranty; keep intact all the
|
||||
notices that refer to this License and to the absence of any warranty;
|
||||
and give any other recipients of the Program a copy of this License
|
||||
along with the Program.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
You may charge a fee for the physical act of transferring a copy, and
|
||||
you may at your option offer warranty protection in exchange for a fee.
|
||||
</p>
|
||||
|
||||
<p id="section2">
|
||||
<strong>2.</strong>
|
||||
You may modify your copy or copies of the Program or any portion
|
||||
of it, thus forming a work based on the Program, and copy and
|
||||
distribute such modifications or work under the terms of Section 1
|
||||
above, provided that you also meet all of these conditions:
|
||||
</p>
|
||||
|
||||
<dl>
|
||||
<dt></dt>
|
||||
<dd>
|
||||
<strong>a)</strong>
|
||||
You must cause the modified files to carry prominent notices
|
||||
stating that you changed the files and the date of any change.
|
||||
</dd>
|
||||
<dt></dt>
|
||||
<dd>
|
||||
<strong>b)</strong>
|
||||
You must cause any work that you distribute or publish, that in
|
||||
whole or in part contains or is derived from the Program or any
|
||||
part thereof, to be licensed as a whole at no charge to all third
|
||||
parties under the terms of this License.
|
||||
</dd>
|
||||
<dt></dt>
|
||||
<dd>
|
||||
<strong>c)</strong>
|
||||
If the modified program normally reads commands interactively
|
||||
when run, you must cause it, when started running for such
|
||||
interactive use in the most ordinary way, to print or display an
|
||||
announcement including an appropriate copyright notice and a
|
||||
notice that there is no warranty (or else, saying that you provide
|
||||
a warranty) and that users may redistribute the program under
|
||||
these conditions, and telling the user how to view a copy of this
|
||||
License. (Exception: if the Program itself is interactive but
|
||||
does not normally print such an announcement, your work based on
|
||||
the Program is not required to print an announcement.)
|
||||
</dd>
|
||||
</dl>
|
||||
|
||||
<p>
|
||||
These requirements apply to the modified work as a whole. If
|
||||
identifiable sections of that work are not derived from the Program,
|
||||
and can be reasonably considered independent and separate works in
|
||||
themselves, then this License, and its terms, do not apply to those
|
||||
sections when you distribute them as separate works. But when you
|
||||
distribute the same sections as part of a whole which is a work based
|
||||
on the Program, the distribution of the whole must be on the terms of
|
||||
this License, whose permissions for other licensees extend to the
|
||||
entire whole, and thus to each and every part regardless of who wrote it.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
Thus, it is not the intent of this section to claim rights or contest
|
||||
your rights to work written entirely by you; rather, the intent is to
|
||||
exercise the right to control the distribution of derivative or
|
||||
collective works based on the Program.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
In addition, mere aggregation of another work not based on the Program
|
||||
with the Program (or with a work based on the Program) on a volume of
|
||||
a storage or distribution medium does not bring the other work under
|
||||
the scope of this License.
|
||||
</p>
|
||||
|
||||
<p id="section3">
|
||||
<strong>3.</strong>
|
||||
You may copy and distribute the Program (or a work based on it,
|
||||
under Section 2) in object code or executable form under the terms of
|
||||
Sections 1 and 2 above provided that you also do one of the following:
|
||||
</p>
|
||||
|
||||
<!-- we use this doubled UL to get the sub-sections indented, -->
|
||||
<!-- while making the bullets as unobvious as possible. -->
|
||||
|
||||
<dl>
|
||||
<dt></dt>
|
||||
<dd>
|
||||
<strong>a)</strong>
|
||||
Accompany it with the complete corresponding machine-readable
|
||||
source code, which must be distributed under the terms of Sections
|
||||
1 and 2 above on a medium customarily used for software interchange; or,
|
||||
</dd>
|
||||
<dt></dt>
|
||||
<dd>
|
||||
<strong>b)</strong>
|
||||
Accompany it with a written offer, valid for at least three
|
||||
years, to give any third party, for a charge no more than your
|
||||
cost of physically performing source distribution, a complete
|
||||
machine-readable copy of the corresponding source code, to be
|
||||
distributed under the terms of Sections 1 and 2 above on a medium
|
||||
customarily used for software interchange; or,
|
||||
</dd>
|
||||
<dt></dt>
|
||||
<dd>
|
||||
<strong>c)</strong>
|
||||
Accompany it with the information you received as to the offer
|
||||
to distribute corresponding source code. (This alternative is
|
||||
allowed only for noncommercial distribution and only if you
|
||||
received the program in object code or executable form with such
|
||||
an offer, in accord with Subsection b above.)
|
||||
</dd>
|
||||
</dl>
|
||||
|
||||
<p>
|
||||
The source code for a work means the preferred form of the work for
|
||||
making modifications to it. For an executable work, complete source
|
||||
code means all the source code for all modules it contains, plus any
|
||||
associated interface definition files, plus the scripts used to
|
||||
control compilation and installation of the executable. However, as a
|
||||
special exception, the source code distributed need not include
|
||||
anything that is normally distributed (in either source or binary
|
||||
form) with the major softwareComponents (compiler, kernel, and so on) of the
|
||||
operating system on which the executable runs, unless that component
|
||||
itself accompanies the executable.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
If distribution of executable or object code is made by offering
|
||||
access to copy from a designated place, then offering equivalent
|
||||
access to copy the source code from the same place counts as
|
||||
distribution of the source code, even though third parties are not
|
||||
compelled to copy the source along with the object code.
|
||||
</p>
|
||||
|
||||
<p id="section4">
|
||||
<strong>4.</strong>
|
||||
You may not copy, modify, sublicense, or distribute the Program
|
||||
except as expressly provided under this License. Any attempt
|
||||
otherwise to copy, modify, sublicense or distribute the Program is
|
||||
void, and will automatically terminate your rights under this License.
|
||||
However, parties who have received copies, or rights, from you under
|
||||
this License will not have their licenses terminated so long as such
|
||||
parties remain in full compliance.
|
||||
</p>
|
||||
|
||||
<p id="section5">
|
||||
<strong>5.</strong>
|
||||
You are not required to accept this License, since you have not
|
||||
signed it. However, nothing else grants you permission to modify or
|
||||
distribute the Program or its derivative works. These actions are
|
||||
prohibited by law if you do not accept this License. Therefore, by
|
||||
modifying or distributing the Program (or any work based on the
|
||||
Program), you indicate your acceptance of this License to do so, and
|
||||
all its terms and conditions for copying, distributing or modifying
|
||||
the Program or works based on it.
|
||||
</p>
|
||||
|
||||
<p id="section6">
|
||||
<strong>6.</strong>
|
||||
Each time you redistribute the Program (or any work based on the
|
||||
Program), the recipient automatically receives a license from the
|
||||
original licensor to copy, distribute or modify the Program subject to
|
||||
these terms and conditions. You may not impose any further
|
||||
restrictions on the recipients' exercise of the rights granted herein.
|
||||
You are not responsible for enforcing compliance by third parties to
|
||||
this License.
|
||||
</p>
|
||||
|
||||
<p id="section7">
|
||||
<strong>7.</strong>
|
||||
If, as a consequence of a court judgment or allegation of patent
|
||||
infringement or for any other reason (not limited to patent issues),
|
||||
conditions are imposed on you (whether by court order, agreement or
|
||||
otherwise) that contradict the conditions of this License, they do not
|
||||
excuse you from the conditions of this License. If you cannot
|
||||
distribute so as to satisfy simultaneously your obligations under this
|
||||
License and any other pertinent obligations, then as a consequence you
|
||||
may not distribute the Program at all. For example, if a patent
|
||||
license would not permit royalty-free redistribution of the Program by
|
||||
all those who receive copies directly or indirectly through you, then
|
||||
the only way you could satisfy both it and this License would be to
|
||||
refrain entirely from distribution of the Program.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
If any portion of this section is held invalid or unenforceable under
|
||||
any particular circumstance, the balance of the section is intended to
|
||||
apply and the section as a whole is intended to apply in other
|
||||
circumstances.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
It is not the purpose of this section to induce you to infringe any
|
||||
patents or other property right claims or to contest validity of any
|
||||
such claims; this section has the sole purpose of protecting the
|
||||
integrity of the free software distribution system, which is
|
||||
implemented by public license practices. Many people have made
|
||||
generous contributions to the wide range of software distributed
|
||||
through that system in reliance on consistent application of that
|
||||
system; it is up to the author/donor to decide if he or she is willing
|
||||
to distribute software through any other system and a licensee cannot
|
||||
impose that choice.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
This section is intended to make thoroughly clear what is believed to
|
||||
be a consequence of the rest of this License.
|
||||
</p>
|
||||
|
||||
<p id="section8">
|
||||
<strong>8.</strong>
|
||||
If the distribution and/or use of the Program is restricted in
|
||||
certain countries either by patents or by copyrighted interfaces, the
|
||||
original copyright holder who places the Program under this License
|
||||
may add an explicit geographical distribution limitation excluding
|
||||
those countries, so that distribution is permitted only in or among
|
||||
countries not thus excluded. In such case, this License incorporates
|
||||
the limitation as if written in the body of this License.
|
||||
</p>
|
||||
|
||||
<p id="section9">
|
||||
<strong>9.</strong>
|
||||
The Free Software Foundation may publish revised and/or new versions
|
||||
of the General Public License from time to time. Such new versions will
|
||||
be similar in spirit to the present version, but may differ in detail to
|
||||
address new problems or concerns.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
Each version is given a distinguishing version number. If the Program
|
||||
specifies a version number of this License which applies to it and "any
|
||||
later version", you have the option of following the terms and conditions
|
||||
either of that version or of any later version published by the Free
|
||||
Software Foundation. If the Program does not specify a version number of
|
||||
this License, you may choose any version ever published by the Free Software
|
||||
Foundation.
|
||||
</p>
|
||||
|
||||
<p id="section10">
|
||||
<strong>10.</strong>
|
||||
If you wish to incorporate parts of the Program into other free
|
||||
programs whose distribution conditions are different, write to the author
|
||||
to ask for permission. For software which is copyrighted by the Free
|
||||
Software Foundation, write to the Free Software Foundation; we sometimes
|
||||
make exceptions for this. Our decision will be guided by the two goals
|
||||
of preserving the free status of all derivatives of our free software and
|
||||
of promoting the sharing and reuse of software generally.
|
||||
</p>
|
||||
|
||||
<p id="section11"><strong>NO WARRANTY</strong></p>
|
||||
|
||||
<p>
|
||||
<strong>11.</strong>
|
||||
BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
|
||||
FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN
|
||||
OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
|
||||
PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED
|
||||
OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
|
||||
MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS
|
||||
TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE
|
||||
PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,
|
||||
REPAIR OR CORRECTION.
|
||||
</p>
|
||||
|
||||
<p id="section12">
|
||||
<strong>12.</strong>
|
||||
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
|
||||
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
|
||||
REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
|
||||
INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING
|
||||
OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED
|
||||
TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY
|
||||
YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER
|
||||
PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE
|
||||
POSSIBILITY OF SUCH DAMAGES.
|
||||
</p>
|
||||
</body></html>
|
||||
@@ -38,12 +38,15 @@ import java.util.ArrayList;
|
||||
* This is a copy from {@link androidx.fragment.app.FragmentStatePagerAdapter}.
|
||||
* <p>
|
||||
* It includes a workaround to fix the menu visibility when the adapter is restored.
|
||||
* </p>
|
||||
* <p>
|
||||
* When restoring the state of this adapter, all the fragments' menu visibility were set to false,
|
||||
* effectively disabling the menu from the user until he switched pages or another event that triggered the
|
||||
* menu to be visible again happened.
|
||||
* effectively disabling the menu from the user until he switched pages or another event
|
||||
* that triggered the menu to be visible again happened.
|
||||
* </p>
|
||||
* <p>
|
||||
* <br><b>Check out the changes in:</b>
|
||||
* <b>Check out the changes in:</b>
|
||||
* </p>
|
||||
* <ul>
|
||||
* <li>{@link #saveState()}</li>
|
||||
* <li>{@link #restoreState(Parcelable, ClassLoader)}</li>
|
||||
@@ -83,13 +86,13 @@ public abstract class FragmentStatePagerAdapterMenuWorkaround extends PagerAdapt
|
||||
private final int mBehavior;
|
||||
private FragmentTransaction mCurTransaction = null;
|
||||
|
||||
private ArrayList<Fragment.SavedState> mSavedState = new ArrayList<Fragment.SavedState>();
|
||||
private ArrayList<Fragment> mFragments = new ArrayList<Fragment>();
|
||||
private final ArrayList<Fragment.SavedState> mSavedState = new ArrayList<Fragment.SavedState>();
|
||||
private final ArrayList<Fragment> mFragments = new ArrayList<Fragment>();
|
||||
private Fragment mCurrentPrimaryItem = null;
|
||||
|
||||
/**
|
||||
* Constructor for {@link FragmentStatePagerAdapterMenuWorkaround} that sets the fragment manager for the
|
||||
* adapter. This is the equivalent of calling
|
||||
* Constructor for {@link FragmentStatePagerAdapterMenuWorkaround}
|
||||
* that sets the fragment manager for the adapter. This is the equivalent of calling
|
||||
* {@link #FragmentStatePagerAdapterMenuWorkaround(FragmentManager, int)} and passing in
|
||||
* {@link #BEHAVIOR_SET_USER_VISIBLE_HINT}.
|
||||
*
|
||||
@@ -101,7 +104,7 @@ public abstract class FragmentStatePagerAdapterMenuWorkaround extends PagerAdapt
|
||||
* {@link #BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT}
|
||||
*/
|
||||
@Deprecated
|
||||
public FragmentStatePagerAdapterMenuWorkaround(@NonNull FragmentManager fm) {
|
||||
public FragmentStatePagerAdapterMenuWorkaround(@NonNull final FragmentManager fm) {
|
||||
this(fm, BEHAVIOR_SET_USER_VISIBLE_HINT);
|
||||
}
|
||||
|
||||
@@ -117,20 +120,21 @@ public abstract class FragmentStatePagerAdapterMenuWorkaround extends PagerAdapt
|
||||
* @param fm fragment manager that will interact with this adapter
|
||||
* @param behavior determines if only current fragments are in a resumed state
|
||||
*/
|
||||
public FragmentStatePagerAdapterMenuWorkaround(@NonNull FragmentManager fm,
|
||||
@Behavior int behavior) {
|
||||
public FragmentStatePagerAdapterMenuWorkaround(@NonNull final FragmentManager fm,
|
||||
@Behavior final int behavior) {
|
||||
mFragmentManager = fm;
|
||||
mBehavior = behavior;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the Fragment associated with a specified position.
|
||||
* @param position the position of the item you want
|
||||
* @return the {@link Fragment} associated with a specified position
|
||||
*/
|
||||
@NonNull
|
||||
public abstract Fragment getItem(int position);
|
||||
|
||||
@Override
|
||||
public void startUpdate(@NonNull ViewGroup container) {
|
||||
public void startUpdate(@NonNull final ViewGroup container) {
|
||||
if (container.getId() == View.NO_ID) {
|
||||
throw new IllegalStateException("ViewPager with adapter " + this
|
||||
+ " requires a view id");
|
||||
@@ -140,13 +144,13 @@ public abstract class FragmentStatePagerAdapterMenuWorkaround extends PagerAdapt
|
||||
@SuppressWarnings("deprecation")
|
||||
@NonNull
|
||||
@Override
|
||||
public Object instantiateItem(@NonNull ViewGroup container, int position) {
|
||||
public Object instantiateItem(@NonNull final ViewGroup container, final int position) {
|
||||
// If we already have this item instantiated, there is nothing
|
||||
// to do. This can happen when we are restoring the entire pager
|
||||
// from its saved state, where the fragment manager has already
|
||||
// taken care of restoring the fragments we previously had instantiated.
|
||||
if (mFragments.size() > position) {
|
||||
Fragment f = mFragments.get(position);
|
||||
final Fragment f = mFragments.get(position);
|
||||
if (f != null) {
|
||||
return f;
|
||||
}
|
||||
@@ -156,10 +160,12 @@ public abstract class FragmentStatePagerAdapterMenuWorkaround extends PagerAdapt
|
||||
mCurTransaction = mFragmentManager.beginTransaction();
|
||||
}
|
||||
|
||||
Fragment fragment = getItem(position);
|
||||
if (DEBUG) Log.v(TAG, "Adding item #" + position + ": f=" + fragment);
|
||||
final Fragment fragment = getItem(position);
|
||||
if (DEBUG) {
|
||||
Log.v(TAG, "Adding item #" + position + ": f=" + fragment);
|
||||
}
|
||||
if (mSavedState.size() > position) {
|
||||
Fragment.SavedState fss = mSavedState.get(position);
|
||||
final Fragment.SavedState fss = mSavedState.get(position);
|
||||
if (fss != null) {
|
||||
fragment.setInitialSavedState(fss);
|
||||
}
|
||||
@@ -183,14 +189,17 @@ public abstract class FragmentStatePagerAdapterMenuWorkaround extends PagerAdapt
|
||||
}
|
||||
|
||||
@Override
|
||||
public void destroyItem(@NonNull ViewGroup container, int position, @NonNull Object object) {
|
||||
Fragment fragment = (Fragment) object;
|
||||
public void destroyItem(@NonNull final ViewGroup container, final int position,
|
||||
@NonNull final Object object) {
|
||||
final Fragment fragment = (Fragment) object;
|
||||
|
||||
if (mCurTransaction == null) {
|
||||
mCurTransaction = mFragmentManager.beginTransaction();
|
||||
}
|
||||
if (DEBUG) Log.v(TAG, "Removing item #" + position + ": f=" + object
|
||||
+ " v=" + ((Fragment)object).getView());
|
||||
if (DEBUG) {
|
||||
Log.v(TAG, "Removing item #" + position + ": f=" + object
|
||||
+ " v=" + ((Fragment) object).getView());
|
||||
}
|
||||
while (mSavedState.size() <= position) {
|
||||
mSavedState.add(null);
|
||||
}
|
||||
@@ -206,8 +215,9 @@ public abstract class FragmentStatePagerAdapterMenuWorkaround extends PagerAdapt
|
||||
|
||||
@Override
|
||||
@SuppressWarnings({"ReferenceEquality", "deprecation"})
|
||||
public void setPrimaryItem(@NonNull ViewGroup container, int position, @NonNull Object object) {
|
||||
Fragment fragment = (Fragment)object;
|
||||
public void setPrimaryItem(@NonNull final ViewGroup container, final int position,
|
||||
@NonNull final Object object) {
|
||||
final Fragment fragment = (Fragment) object;
|
||||
if (fragment != mCurrentPrimaryItem) {
|
||||
if (mCurrentPrimaryItem != null) {
|
||||
mCurrentPrimaryItem.setMenuVisibility(false);
|
||||
@@ -235,7 +245,7 @@ public abstract class FragmentStatePagerAdapterMenuWorkaround extends PagerAdapt
|
||||
}
|
||||
|
||||
@Override
|
||||
public void finishUpdate(@NonNull ViewGroup container) {
|
||||
public void finishUpdate(@NonNull final ViewGroup container) {
|
||||
if (mCurTransaction != null) {
|
||||
mCurTransaction.commitNowAllowingStateLoss();
|
||||
mCurTransaction = null;
|
||||
@@ -243,12 +253,12 @@ public abstract class FragmentStatePagerAdapterMenuWorkaround extends PagerAdapt
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isViewFromObject(@NonNull View view, @NonNull Object object) {
|
||||
return ((Fragment)object).getView() == view;
|
||||
public boolean isViewFromObject(@NonNull final View view, @NonNull final Object object) {
|
||||
return ((Fragment) object).getView() == view;
|
||||
}
|
||||
|
||||
//!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
|
||||
private final String SELECTED_FRAGMENT = "selected_fragment";
|
||||
private final String selectedFragment = "selected_fragment";
|
||||
//!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
|
||||
|
||||
@Override
|
||||
@@ -257,23 +267,23 @@ public abstract class FragmentStatePagerAdapterMenuWorkaround extends PagerAdapt
|
||||
Bundle state = null;
|
||||
if (mSavedState.size() > 0) {
|
||||
state = new Bundle();
|
||||
Fragment.SavedState[] fss = new Fragment.SavedState[mSavedState.size()];
|
||||
final Fragment.SavedState[] fss = new Fragment.SavedState[mSavedState.size()];
|
||||
mSavedState.toArray(fss);
|
||||
state.putParcelableArray("states", fss);
|
||||
}
|
||||
for (int i=0; i<mFragments.size(); i++) {
|
||||
Fragment f = mFragments.get(i);
|
||||
for (int i = 0; i < mFragments.size(); i++) {
|
||||
final Fragment f = mFragments.get(i);
|
||||
if (f != null && f.isAdded()) {
|
||||
if (state == null) {
|
||||
state = new Bundle();
|
||||
}
|
||||
String key = "f" + i;
|
||||
final String key = "f" + i;
|
||||
mFragmentManager.putFragment(state, key, f);
|
||||
|
||||
//!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
|
||||
// Check if it's the same fragment instance
|
||||
if (f == mCurrentPrimaryItem) {
|
||||
state.putString(SELECTED_FRAGMENT, key);
|
||||
state.putString(selectedFragment, key);
|
||||
}
|
||||
//!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
|
||||
}
|
||||
@@ -282,29 +292,30 @@ public abstract class FragmentStatePagerAdapterMenuWorkaround extends PagerAdapt
|
||||
}
|
||||
|
||||
@Override
|
||||
public void restoreState(@Nullable Parcelable state, @Nullable ClassLoader loader) {
|
||||
public void restoreState(@Nullable final Parcelable state, @Nullable final ClassLoader loader) {
|
||||
if (state != null) {
|
||||
Bundle bundle = (Bundle)state;
|
||||
final Bundle bundle = (Bundle) state;
|
||||
bundle.setClassLoader(loader);
|
||||
Parcelable[] fss = bundle.getParcelableArray("states");
|
||||
final Parcelable[] fss = bundle.getParcelableArray("states");
|
||||
mSavedState.clear();
|
||||
mFragments.clear();
|
||||
if (fss != null) {
|
||||
for (int i=0; i<fss.length; i++) {
|
||||
mSavedState.add((Fragment.SavedState)fss[i]);
|
||||
for (final Parcelable parcelable : fss) {
|
||||
mSavedState.add((Fragment.SavedState) parcelable);
|
||||
}
|
||||
}
|
||||
Iterable<String> keys = bundle.keySet();
|
||||
for (String key: keys) {
|
||||
final Iterable<String> keys = bundle.keySet();
|
||||
for (final String key : keys) {
|
||||
if (key.startsWith("f")) {
|
||||
int index = Integer.parseInt(key.substring(1));
|
||||
Fragment f = mFragmentManager.getFragment(bundle, key);
|
||||
final int index = Integer.parseInt(key.substring(1));
|
||||
final Fragment f = mFragmentManager.getFragment(bundle, key);
|
||||
if (f != null) {
|
||||
while (mFragments.size() <= index) {
|
||||
mFragments.add(null);
|
||||
}
|
||||
//!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
|
||||
final boolean wasSelected = bundle.getString(SELECTED_FRAGMENT, "").equals(key);
|
||||
final boolean wasSelected = bundle.getString(selectedFragment, "")
|
||||
.equals(key);
|
||||
f.setMenuVisibility(wasSelected);
|
||||
//!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
|
||||
mFragments.set(index, f);
|
||||
|
||||
@@ -1,24 +1,80 @@
|
||||
package com.google.android.material.appbar;
|
||||
|
||||
import android.content.Context;
|
||||
import android.graphics.Rect;
|
||||
import android.util.AttributeSet;
|
||||
import android.view.MotionEvent;
|
||||
import android.view.View;
|
||||
import android.widget.OverScroller;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.coordinatorlayout.widget.CoordinatorLayout;
|
||||
import org.schabi.newpipe.R;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
// check this https://stackoverflow.com/questions/56849221/recyclerview-fling-causes-laggy-while-appbarlayout-is-scrolling/57997489#57997489
|
||||
// See https://stackoverflow.com/questions/56849221#57997489
|
||||
public final class FlingBehavior extends AppBarLayout.Behavior {
|
||||
private final Rect focusScrollRect = new Rect();
|
||||
|
||||
public FlingBehavior(Context context, AttributeSet attrs) {
|
||||
public FlingBehavior(final Context context, final AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
}
|
||||
|
||||
private boolean allowScroll = true;
|
||||
private final Rect globalRect = new Rect();
|
||||
private final List<Integer> skipInterceptionOfElements = Arrays.asList(
|
||||
R.id.playQueuePanel, R.id.playbackSeekBar,
|
||||
R.id.playPauseButton, R.id.playPreviousButton, R.id.playNextButton);
|
||||
|
||||
@Override
|
||||
public boolean onInterceptTouchEvent(CoordinatorLayout parent, AppBarLayout child, MotionEvent ev) {
|
||||
public boolean onRequestChildRectangleOnScreen(
|
||||
@NonNull final CoordinatorLayout coordinatorLayout, @NonNull final AppBarLayout child,
|
||||
@NonNull final Rect rectangle, final boolean immediate) {
|
||||
focusScrollRect.set(rectangle);
|
||||
|
||||
coordinatorLayout.offsetDescendantRectToMyCoords(child, focusScrollRect);
|
||||
|
||||
final int height = coordinatorLayout.getHeight();
|
||||
|
||||
if (focusScrollRect.top <= 0 && focusScrollRect.bottom >= height) {
|
||||
// the child is too big to fit inside ourselves completely, ignore request
|
||||
return false;
|
||||
}
|
||||
|
||||
final int dy;
|
||||
|
||||
if (focusScrollRect.bottom > height) {
|
||||
dy = focusScrollRect.top;
|
||||
} else if (focusScrollRect.top < 0) {
|
||||
// scrolling up
|
||||
dy = -(height - focusScrollRect.bottom);
|
||||
} else {
|
||||
// nothing to do
|
||||
return false;
|
||||
}
|
||||
|
||||
final int consumed = scroll(coordinatorLayout, child, dy, getMaxDragOffset(child), 0);
|
||||
|
||||
return consumed == dy;
|
||||
}
|
||||
|
||||
public boolean onInterceptTouchEvent(final CoordinatorLayout parent, final AppBarLayout child,
|
||||
final MotionEvent ev) {
|
||||
for (final Integer element : skipInterceptionOfElements) {
|
||||
final View view = child.findViewById(element);
|
||||
if (view != null) {
|
||||
final boolean visible = view.getGlobalVisibleRect(globalRect);
|
||||
if (visible && globalRect.contains((int) ev.getRawX(), (int) ev.getRawY())) {
|
||||
allowScroll = false;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
allowScroll = true;
|
||||
switch (ev.getActionMasked()) {
|
||||
case MotionEvent.ACTION_DOWN:
|
||||
// remove reference to old nested scrolling child
|
||||
@@ -32,16 +88,37 @@ public final class FlingBehavior extends AppBarLayout.Behavior {
|
||||
return super.onInterceptTouchEvent(parent, child, ev);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onStartNestedScroll(@NonNull final CoordinatorLayout parent,
|
||||
@NonNull final AppBarLayout child,
|
||||
@NonNull final View directTargetChild,
|
||||
final View target,
|
||||
final int nestedScrollAxes,
|
||||
final int type) {
|
||||
return allowScroll && super.onStartNestedScroll(
|
||||
parent, child, directTargetChild, target, nestedScrollAxes, type);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onNestedFling(@NonNull final CoordinatorLayout coordinatorLayout,
|
||||
@NonNull final AppBarLayout child,
|
||||
@NonNull final View target, final float velocityX,
|
||||
final float velocityY, final boolean consumed) {
|
||||
return allowScroll && super.onNestedFling(
|
||||
coordinatorLayout, child, target, velocityX, velocityY, consumed);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private OverScroller getScrollerField() {
|
||||
try {
|
||||
Class<?> headerBehaviorType = this.getClass().getSuperclass().getSuperclass().getSuperclass();
|
||||
final Class<?> headerBehaviorType = this.getClass()
|
||||
.getSuperclass().getSuperclass().getSuperclass();
|
||||
if (headerBehaviorType != null) {
|
||||
Field field = headerBehaviorType.getDeclaredField("scroller");
|
||||
final Field field = headerBehaviorType.getDeclaredField("scroller");
|
||||
field.setAccessible(true);
|
||||
return ((OverScroller) field.get(this));
|
||||
}
|
||||
} catch (NoSuchFieldException | IllegalAccessException e) {
|
||||
} catch (final NoSuchFieldException | IllegalAccessException e) {
|
||||
// ?
|
||||
}
|
||||
return null;
|
||||
@@ -50,33 +127,37 @@ public final class FlingBehavior extends AppBarLayout.Behavior {
|
||||
@Nullable
|
||||
private Field getLastNestedScrollingChildRefField() {
|
||||
try {
|
||||
Class<?> headerBehaviorType = this.getClass().getSuperclass().getSuperclass();
|
||||
final Class<?> headerBehaviorType = this.getClass().getSuperclass().getSuperclass();
|
||||
if (headerBehaviorType != null) {
|
||||
Field field = headerBehaviorType.getDeclaredField("lastNestedScrollingChildRef");
|
||||
final Field field
|
||||
= headerBehaviorType.getDeclaredField("lastNestedScrollingChildRef");
|
||||
field.setAccessible(true);
|
||||
return field;
|
||||
}
|
||||
} catch (NoSuchFieldException e) {
|
||||
} catch (final NoSuchFieldException e) {
|
||||
// ?
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private void resetNestedScrollingChild(){
|
||||
Field field = getLastNestedScrollingChildRefField();
|
||||
if(field != null){
|
||||
private void resetNestedScrollingChild() {
|
||||
final Field field = getLastNestedScrollingChildRefField();
|
||||
if (field != null) {
|
||||
try {
|
||||
Object value = field.get(this);
|
||||
if(value != null) field.set(this, null);
|
||||
} catch (IllegalAccessException e) {
|
||||
final Object value = field.get(this);
|
||||
if (value != null) {
|
||||
field.set(this, null);
|
||||
}
|
||||
} catch (final IllegalAccessException e) {
|
||||
// ?
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void stopAppBarLayoutFling() {
|
||||
OverScroller scroller = getScrollerField();
|
||||
if (scroller != null) scroller.forceFinished(true);
|
||||
final OverScroller scroller = getScrollerField();
|
||||
if (scroller != null) {
|
||||
scroller.forceFinished(true);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,17 +23,25 @@ package org.schabi.newpipe;
|
||||
/**
|
||||
* Singleton:
|
||||
* Used to send data between certain Activity/Services within the same process.
|
||||
* This can be considered as an ugly hack inside the Android universe. **/
|
||||
* This can be considered as an ugly hack inside the Android universe.
|
||||
**/
|
||||
public class ActivityCommunicator {
|
||||
|
||||
private static ActivityCommunicator activityCommunicator;
|
||||
private volatile Class returnActivity;
|
||||
|
||||
public static ActivityCommunicator getCommunicator() {
|
||||
if(activityCommunicator == null) {
|
||||
if (activityCommunicator == null) {
|
||||
activityCommunicator = new ActivityCommunicator();
|
||||
}
|
||||
return activityCommunicator;
|
||||
}
|
||||
|
||||
public volatile Class returnActivity;
|
||||
public Class getReturnActivity() {
|
||||
return returnActivity;
|
||||
}
|
||||
|
||||
public void setReturnActivity(final Class returnActivity) {
|
||||
this.returnActivity = returnActivity;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,33 +1,32 @@
|
||||
package org.schabi.newpipe;
|
||||
|
||||
import android.annotation.TargetApi;
|
||||
import android.app.Application;
|
||||
import android.app.NotificationChannel;
|
||||
import android.app.NotificationManager;
|
||||
import android.content.Context;
|
||||
import android.content.SharedPreferences;
|
||||
import android.os.Build;
|
||||
import android.util.Log;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.multidex.MultiDexApplication;
|
||||
import androidx.preference.PreferenceManager;
|
||||
|
||||
import com.nostra13.universalimageloader.cache.memory.impl.LRULimitedMemoryCache;
|
||||
import com.nostra13.universalimageloader.core.ImageLoader;
|
||||
import com.nostra13.universalimageloader.core.ImageLoaderConfiguration;
|
||||
import com.squareup.leakcanary.LeakCanary;
|
||||
import com.squareup.leakcanary.RefWatcher;
|
||||
|
||||
import org.acra.ACRA;
|
||||
import org.acra.config.ACRAConfiguration;
|
||||
import org.acra.config.ACRAConfigurationException;
|
||||
import org.acra.config.ConfigurationBuilder;
|
||||
import org.acra.sender.ReportSenderFactory;
|
||||
import org.acra.config.CoreConfiguration;
|
||||
import org.acra.config.CoreConfigurationBuilder;
|
||||
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.ErrorInfo;
|
||||
import org.schabi.newpipe.report.UserAction;
|
||||
import org.schabi.newpipe.settings.SettingsActivity;
|
||||
import org.schabi.newpipe.util.ExtractorHelper;
|
||||
import org.schabi.newpipe.util.ExceptionUtils;
|
||||
import org.schabi.newpipe.util.Localization;
|
||||
import org.schabi.newpipe.util.ServiceHelper;
|
||||
import org.schabi.newpipe.util.StateSaver;
|
||||
@@ -35,16 +34,17 @@ import org.schabi.newpipe.util.StateSaver;
|
||||
import java.io.IOException;
|
||||
import java.io.InterruptedIOException;
|
||||
import java.net.SocketException;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
import io.reactivex.annotations.NonNull;
|
||||
import io.reactivex.exceptions.CompositeException;
|
||||
import io.reactivex.exceptions.MissingBackpressureException;
|
||||
import io.reactivex.exceptions.OnErrorNotImplementedException;
|
||||
import io.reactivex.exceptions.UndeliverableException;
|
||||
import io.reactivex.functions.Consumer;
|
||||
import io.reactivex.plugins.RxJavaPlugins;
|
||||
import io.reactivex.rxjava3.disposables.Disposable;
|
||||
import io.reactivex.rxjava3.exceptions.CompositeException;
|
||||
import io.reactivex.rxjava3.exceptions.MissingBackpressureException;
|
||||
import io.reactivex.rxjava3.exceptions.OnErrorNotImplementedException;
|
||||
import io.reactivex.rxjava3.exceptions.UndeliverableException;
|
||||
import io.reactivex.rxjava3.functions.Consumer;
|
||||
import io.reactivex.rxjava3.plugins.RxJavaPlugins;
|
||||
|
||||
/*
|
||||
* Copyright (C) Hans-Christoph Steiner 2016 <hans@eds.org>
|
||||
@@ -64,19 +64,20 @@ import io.reactivex.plugins.RxJavaPlugins;
|
||||
* along with NewPipe. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
public class App extends Application {
|
||||
public class App extends MultiDexApplication {
|
||||
protected static final String TAG = App.class.toString();
|
||||
private RefWatcher refWatcher;
|
||||
private static App app;
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private static final Class<? extends ReportSenderFactory>[]
|
||||
reportSenderFactoryClasses = new Class[]{AcraReportSenderFactory.class};
|
||||
@Nullable private Disposable disposable = null;
|
||||
|
||||
@NonNull
|
||||
public static App getApp() {
|
||||
return app;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void attachBaseContext(Context base) {
|
||||
protected void attachBaseContext(final Context base) {
|
||||
super.attachBaseContext(base);
|
||||
|
||||
initACRA();
|
||||
}
|
||||
|
||||
@@ -84,13 +85,6 @@ public class App extends Application {
|
||||
public void onCreate() {
|
||||
super.onCreate();
|
||||
|
||||
if (LeakCanary.isInAnalyzerProcess(this)) {
|
||||
// This process is dedicated to LeakCanary for heap analysis.
|
||||
// You should not init your app in this process.
|
||||
return;
|
||||
}
|
||||
refWatcher = installLeakCanary();
|
||||
|
||||
app = this;
|
||||
|
||||
// Initialize settings first because others inits can use its values
|
||||
@@ -102,7 +96,7 @@ public class App extends Application {
|
||||
Localization.init(getApplicationContext());
|
||||
|
||||
StateSaver.init(this);
|
||||
initNotificationChannel();
|
||||
initNotificationChannels();
|
||||
|
||||
ServiceHelper.initServices(this);
|
||||
|
||||
@@ -112,35 +106,59 @@ public class App extends Application {
|
||||
configureRxJavaErrorHandler();
|
||||
|
||||
// Check for new version
|
||||
new CheckForNewAppVersionTask().execute();
|
||||
disposable = CheckForNewAppVersion.checkNewVersion(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTerminate() {
|
||||
if (disposable != null) {
|
||||
disposable.dispose();
|
||||
}
|
||||
super.onTerminate();
|
||||
}
|
||||
|
||||
protected Downloader getDownloader() {
|
||||
return DownloaderImpl.init(null);
|
||||
final DownloaderImpl downloader = DownloaderImpl.init(null);
|
||||
setCookiesToDownloader(downloader);
|
||||
return downloader;
|
||||
}
|
||||
|
||||
protected void setCookiesToDownloader(final DownloaderImpl downloader) {
|
||||
final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(
|
||||
getApplicationContext());
|
||||
final String key = getApplicationContext().getString(R.string.recaptcha_cookies_key);
|
||||
downloader.setCookie(ReCaptchaActivity.RECAPTCHA_COOKIES_KEY, prefs.getString(key, ""));
|
||||
downloader.updateYoutubeRestrictedModeCookies(getApplicationContext());
|
||||
}
|
||||
|
||||
private void configureRxJavaErrorHandler() {
|
||||
// https://github.com/ReactiveX/RxJava/wiki/What's-different-in-2.0#error-handling
|
||||
RxJavaPlugins.setErrorHandler(new Consumer<Throwable>() {
|
||||
@Override
|
||||
public void accept(@NonNull Throwable throwable) {
|
||||
Log.e(TAG, "RxJavaPlugins.ErrorHandler called with -> : " +
|
||||
"throwable = [" + throwable.getClass().getName() + "]");
|
||||
public void accept(@NonNull final Throwable throwable) {
|
||||
Log.e(TAG, "RxJavaPlugins.ErrorHandler called with -> : "
|
||||
+ "throwable = [" + throwable.getClass().getName() + "]");
|
||||
|
||||
final Throwable actualThrowable;
|
||||
if (throwable instanceof UndeliverableException) {
|
||||
// As UndeliverableException is a wrapper, get the cause of it to get the "real" exception
|
||||
throwable = throwable.getCause();
|
||||
// As UndeliverableException is a wrapper,
|
||||
// get the cause of it to get the "real" exception
|
||||
actualThrowable = throwable.getCause();
|
||||
} else {
|
||||
actualThrowable = throwable;
|
||||
}
|
||||
|
||||
final List<Throwable> errors;
|
||||
if (throwable instanceof CompositeException) {
|
||||
errors = ((CompositeException) throwable).getExceptions();
|
||||
if (actualThrowable instanceof CompositeException) {
|
||||
errors = ((CompositeException) actualThrowable).getExceptions();
|
||||
} else {
|
||||
errors = Collections.singletonList(throwable);
|
||||
errors = Collections.singletonList(actualThrowable);
|
||||
}
|
||||
|
||||
for (final Throwable error : errors) {
|
||||
if (isThrowableIgnored(error)) return;
|
||||
if (isThrowableIgnored(error)) {
|
||||
return;
|
||||
}
|
||||
if (isThrowableCritical(error)) {
|
||||
reportException(error);
|
||||
return;
|
||||
@@ -150,22 +168,24 @@ public class App extends Application {
|
||||
// Out-of-lifecycle exceptions should only be reported if a debug user wishes so,
|
||||
// When exception is not reported, log it
|
||||
if (isDisposedRxExceptionsReported()) {
|
||||
reportException(throwable);
|
||||
reportException(actualThrowable);
|
||||
} else {
|
||||
Log.e(TAG, "RxJavaPlugin: Undeliverable Exception received: ", throwable);
|
||||
Log.e(TAG, "RxJavaPlugin: Undeliverable Exception received: ", actualThrowable);
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isThrowableIgnored(@NonNull final Throwable throwable) {
|
||||
// Don't crash the application over a simple network problem
|
||||
return ExtractorHelper.hasAssignableCauseThrowable(throwable,
|
||||
IOException.class, SocketException.class, // network api cancellation
|
||||
InterruptedException.class, InterruptedIOException.class); // blocking code disposed
|
||||
return ExceptionUtils.hasAssignableCause(throwable,
|
||||
// network api cancellation
|
||||
IOException.class, SocketException.class,
|
||||
// blocking code disposed
|
||||
InterruptedException.class, InterruptedIOException.class);
|
||||
}
|
||||
|
||||
private boolean isThrowableCritical(@NonNull final Throwable throwable) {
|
||||
// Though these exceptions cannot be ignored
|
||||
return ExtractorHelper.hasAssignableCauseThrowable(throwable,
|
||||
return ExceptionUtils.hasAssignableCause(throwable,
|
||||
NullPointerException.class, IllegalArgumentException.class, // bug in app
|
||||
OnErrorNotImplementedException.class, MissingBackpressureException.class,
|
||||
IllegalStateException.class); // bug in operator
|
||||
@@ -188,84 +208,59 @@ public class App extends Application {
|
||||
.build();
|
||||
}
|
||||
|
||||
private void initACRA() {
|
||||
/**
|
||||
* Called in {@link #attachBaseContext(Context)} after calling the {@code super} method.
|
||||
* Should be overridden if MultiDex is enabled, since it has to be initialized before ACRA.
|
||||
*/
|
||||
protected void initACRA() {
|
||||
if (ACRA.isACRASenderServiceProcess()) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
final ACRAConfiguration acraConfig = new ConfigurationBuilder(this)
|
||||
.setReportSenderFactoryClasses(reportSenderFactoryClasses)
|
||||
final CoreConfiguration acraConfig = new CoreConfigurationBuilder(this)
|
||||
.setBuildConfigClass(BuildConfig.class)
|
||||
.build();
|
||||
ACRA.init(this, acraConfig);
|
||||
} catch (ACRAConfigurationException ace) {
|
||||
} catch (final ACRAConfigurationException ace) {
|
||||
ace.printStackTrace();
|
||||
ErrorActivity.reportError(this,
|
||||
ace,
|
||||
null,
|
||||
null,
|
||||
ErrorActivity.ErrorInfo.make(UserAction.SOMETHING_ELSE, "none",
|
||||
"Could not initialize ACRA crash report", R.string.app_ui_crash));
|
||||
ErrorInfo.make(UserAction.SOMETHING_ELSE, "none",
|
||||
"Could not initialize ACRA crash report", R.string.app_ui_crash));
|
||||
}
|
||||
}
|
||||
|
||||
public void initNotificationChannel() {
|
||||
if (Build.VERSION.SDK_INT < android.os.Build.VERSION_CODES.O) {
|
||||
private void initNotificationChannels() {
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
|
||||
return;
|
||||
}
|
||||
|
||||
final String id = getString(R.string.notification_channel_id);
|
||||
final CharSequence name = getString(R.string.notification_channel_name);
|
||||
final String description = getString(R.string.notification_channel_description);
|
||||
String id = getString(R.string.notification_channel_id);
|
||||
String name = getString(R.string.notification_channel_name);
|
||||
String description = getString(R.string.notification_channel_description);
|
||||
|
||||
// Keep this below DEFAULT to avoid making noise on every notification update
|
||||
final int importance = NotificationManager.IMPORTANCE_LOW;
|
||||
|
||||
NotificationChannel mChannel = new NotificationChannel(id, name, importance);
|
||||
mChannel.setDescription(description);
|
||||
final NotificationChannel mainChannel = new NotificationChannel(id, name, importance);
|
||||
mainChannel.setDescription(description);
|
||||
|
||||
NotificationManager mNotificationManager =
|
||||
(NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
|
||||
mNotificationManager.createNotificationChannel(mChannel);
|
||||
id = getString(R.string.app_update_notification_channel_id);
|
||||
name = getString(R.string.app_update_notification_channel_name);
|
||||
description = getString(R.string.app_update_notification_channel_description);
|
||||
|
||||
setUpUpdateNotificationChannel(importance);
|
||||
}
|
||||
final NotificationChannel appUpdateChannel = new NotificationChannel(id, name, importance);
|
||||
appUpdateChannel.setDescription(description);
|
||||
|
||||
/**
|
||||
* Set up notification channel for app update.
|
||||
* @param importance
|
||||
*/
|
||||
@TargetApi(Build.VERSION_CODES.O)
|
||||
private void setUpUpdateNotificationChannel(int importance) {
|
||||
|
||||
final String appUpdateId
|
||||
= getString(R.string.app_update_notification_channel_id);
|
||||
final CharSequence appUpdateName
|
||||
= getString(R.string.app_update_notification_channel_name);
|
||||
final String appUpdateDescription
|
||||
= getString(R.string.app_update_notification_channel_description);
|
||||
|
||||
NotificationChannel appUpdateChannel
|
||||
= new NotificationChannel(appUpdateId, appUpdateName, importance);
|
||||
appUpdateChannel.setDescription(appUpdateDescription);
|
||||
|
||||
NotificationManager appUpdateNotificationManager
|
||||
= (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
|
||||
appUpdateNotificationManager.createNotificationChannel(appUpdateChannel);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public static RefWatcher getRefWatcher(Context context) {
|
||||
final App application = (App) context.getApplicationContext();
|
||||
return application.refWatcher;
|
||||
}
|
||||
|
||||
protected RefWatcher installLeakCanary() {
|
||||
return RefWatcher.DISABLED;
|
||||
final NotificationManager notificationManager = getSystemService(NotificationManager.class);
|
||||
notificationManager.createNotificationChannels(Arrays.asList(mainChannel,
|
||||
appUpdateChannel));
|
||||
}
|
||||
|
||||
protected boolean isDisposedRxExceptionsReported() {
|
||||
return false;
|
||||
}
|
||||
|
||||
public static App getApp() {
|
||||
return app;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,32 +2,31 @@ package org.schabi.newpipe;
|
||||
|
||||
import android.content.Context;
|
||||
import android.os.Bundle;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.fragment.app.FragmentManager;
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
import android.util.Log;
|
||||
import android.view.View;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.fragment.app.FragmentManager;
|
||||
|
||||
import com.nostra13.universalimageloader.core.ImageLoader;
|
||||
import com.squareup.leakcanary.RefWatcher;
|
||||
|
||||
import icepick.Icepick;
|
||||
import icepick.State;
|
||||
import leakcanary.AppWatcher;
|
||||
|
||||
public abstract class BaseFragment extends Fragment {
|
||||
public static final ImageLoader IMAGE_LOADER = ImageLoader.getInstance();
|
||||
protected final String TAG = getClass().getSimpleName() + "@" + Integer.toHexString(hashCode());
|
||||
protected final boolean DEBUG = MainActivity.DEBUG;
|
||||
|
||||
protected AppCompatActivity activity;
|
||||
public static final ImageLoader imageLoader = ImageLoader.getInstance();
|
||||
|
||||
//These values are used for controlling framgents when they are part of the frontpage
|
||||
//These values are used for controlling fragments when they are part of the frontpage
|
||||
@State
|
||||
protected boolean useAsFrontPage = false;
|
||||
protected boolean mIsVisibleToUser = false;
|
||||
private boolean mIsVisibleToUser = false;
|
||||
|
||||
public void useAsFrontPage(boolean value) {
|
||||
public void useAsFrontPage(final boolean value) {
|
||||
useAsFrontPage = value;
|
||||
}
|
||||
|
||||
@@ -36,7 +35,7 @@ public abstract class BaseFragment extends Fragment {
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
@Override
|
||||
public void onAttach(Context context) {
|
||||
public void onAttach(final Context context) {
|
||||
super.onAttach(context);
|
||||
activity = (AppCompatActivity) context;
|
||||
}
|
||||
@@ -48,43 +47,49 @@ public abstract class BaseFragment extends Fragment {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState) {
|
||||
if (DEBUG) Log.d(TAG, "onCreate() called with: savedInstanceState = [" + savedInstanceState + "]");
|
||||
public void onCreate(final Bundle savedInstanceState) {
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "onCreate() called with: "
|
||||
+ "savedInstanceState = [" + savedInstanceState + "]");
|
||||
}
|
||||
super.onCreate(savedInstanceState);
|
||||
Icepick.restoreInstanceState(this, savedInstanceState);
|
||||
if (savedInstanceState != null) onRestoreInstanceState(savedInstanceState);
|
||||
if (savedInstanceState != null) {
|
||||
onRestoreInstanceState(savedInstanceState);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public void onViewCreated(View rootView, Bundle savedInstanceState) {
|
||||
public void onViewCreated(final View rootView, final Bundle savedInstanceState) {
|
||||
super.onViewCreated(rootView, savedInstanceState);
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "onViewCreated() called with: rootView = [" + rootView + "], savedInstanceState = [" + savedInstanceState + "]");
|
||||
Log.d(TAG, "onViewCreated() called with: "
|
||||
+ "rootView = [" + rootView + "], "
|
||||
+ "savedInstanceState = [" + savedInstanceState + "]");
|
||||
}
|
||||
initViews(rootView, savedInstanceState);
|
||||
initListeners();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSaveInstanceState(Bundle outState) {
|
||||
public void onSaveInstanceState(final Bundle outState) {
|
||||
super.onSaveInstanceState(outState);
|
||||
Icepick.saveInstanceState(this, outState);
|
||||
}
|
||||
|
||||
protected void onRestoreInstanceState(@NonNull Bundle savedInstanceState) {
|
||||
protected void onRestoreInstanceState(@NonNull final Bundle savedInstanceState) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroy() {
|
||||
super.onDestroy();
|
||||
|
||||
RefWatcher refWatcher = App.getRefWatcher(getActivity());
|
||||
if (refWatcher != null) refWatcher.watch(this);
|
||||
AppWatcher.INSTANCE.getObjectWatcher().watch(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setUserVisibleHint(boolean isVisibleToUser) {
|
||||
public void setUserVisibleHint(final boolean isVisibleToUser) {
|
||||
super.setUserVisibleHint(isVisibleToUser);
|
||||
mIsVisibleToUser = isVisibleToUser;
|
||||
}
|
||||
@@ -93,7 +98,7 @@ public abstract class BaseFragment extends Fragment {
|
||||
// Init
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
protected void initViews(View rootView, Bundle savedInstanceState) {
|
||||
protected void initViews(final View rootView, final Bundle savedInstanceState) {
|
||||
}
|
||||
|
||||
protected void initListeners() {
|
||||
@@ -103,10 +108,12 @@ public abstract class BaseFragment extends Fragment {
|
||||
// Utils
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
public void setTitle(String title) {
|
||||
if (DEBUG) Log.d(TAG, "setTitle() called with: title = [" + title + "]");
|
||||
if((!useAsFrontPage || mIsVisibleToUser)
|
||||
&& (activity != null && activity.getSupportActionBar() != null)) {
|
||||
public void setTitle(final String title) {
|
||||
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);
|
||||
}
|
||||
|
||||
222
app/src/main/java/org/schabi/newpipe/CheckForNewAppVersion.java
Normal file
222
app/src/main/java/org/schabi/newpipe/CheckForNewAppVersion.java
Normal file
@@ -0,0 +1,222 @@
|
||||
package org.schabi.newpipe;
|
||||
|
||||
import android.app.Application;
|
||||
import android.app.PendingIntent;
|
||||
import android.content.Intent;
|
||||
import android.content.SharedPreferences;
|
||||
import android.content.pm.PackageInfo;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.content.pm.Signature;
|
||||
import android.net.ConnectivityManager;
|
||||
import android.net.Uri;
|
||||
import android.util.Log;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.core.app.NotificationCompat;
|
||||
import androidx.core.app.NotificationManagerCompat;
|
||||
import androidx.core.content.ContextCompat;
|
||||
import androidx.preference.PreferenceManager;
|
||||
|
||||
import com.grack.nanojson.JsonObject;
|
||||
import com.grack.nanojson.JsonParser;
|
||||
import com.grack.nanojson.JsonParserException;
|
||||
|
||||
import org.schabi.newpipe.report.ErrorActivity;
|
||||
import org.schabi.newpipe.report.ErrorInfo;
|
||||
import org.schabi.newpipe.report.UserAction;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.InputStream;
|
||||
import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.security.cert.CertificateEncodingException;
|
||||
import java.security.cert.CertificateException;
|
||||
import java.security.cert.CertificateFactory;
|
||||
import java.security.cert.X509Certificate;
|
||||
|
||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
|
||||
import io.reactivex.rxjava3.core.Maybe;
|
||||
import io.reactivex.rxjava3.disposables.Disposable;
|
||||
import io.reactivex.rxjava3.schedulers.Schedulers;
|
||||
|
||||
public final class CheckForNewAppVersion {
|
||||
private CheckForNewAppVersion() { }
|
||||
|
||||
private static final boolean DEBUG = MainActivity.DEBUG;
|
||||
private static final String TAG = CheckForNewAppVersion.class.getSimpleName();
|
||||
|
||||
private static final String GITHUB_APK_SHA1
|
||||
= "B0:2E:90:7C:1C:D6:FC:57:C3:35:F0:88:D0:8F:50:5F:94:E4:D2:15";
|
||||
private static final String NEWPIPE_API_URL = "https://newpipe.schabi.org/api/data.json";
|
||||
|
||||
/**
|
||||
* Method to get the APK's SHA1 key. See https://stackoverflow.com/questions/9293019/#22506133.
|
||||
*
|
||||
* @param application The application
|
||||
* @return String with the APK's SHA1 fingerprint in hexadecimal
|
||||
*/
|
||||
@NonNull
|
||||
private static String getCertificateSHA1Fingerprint(@NonNull final Application application) {
|
||||
final PackageInfo packageInfo;
|
||||
try {
|
||||
packageInfo = application.getPackageManager().getPackageInfo(
|
||||
application.getPackageName(), PackageManager.GET_SIGNATURES);
|
||||
} catch (final PackageManager.NameNotFoundException e) {
|
||||
ErrorActivity.reportError(application, e, null, null,
|
||||
ErrorInfo.make(UserAction.SOMETHING_ELSE, "none",
|
||||
"Could not find package info", R.string.app_ui_crash));
|
||||
return "";
|
||||
}
|
||||
|
||||
final X509Certificate c;
|
||||
try {
|
||||
final Signature[] signatures = packageInfo.signatures;
|
||||
final byte[] cert = signatures[0].toByteArray();
|
||||
final InputStream input = new ByteArrayInputStream(cert);
|
||||
final CertificateFactory cf = CertificateFactory.getInstance("X509");
|
||||
c = (X509Certificate) cf.generateCertificate(input);
|
||||
} catch (final CertificateException e) {
|
||||
ErrorActivity.reportError(application, e, null, null,
|
||||
ErrorInfo.make(UserAction.SOMETHING_ELSE, "none",
|
||||
"Certificate error", R.string.app_ui_crash));
|
||||
return "";
|
||||
}
|
||||
|
||||
try {
|
||||
final MessageDigest md = MessageDigest.getInstance("SHA1");
|
||||
final byte[] publicKey = md.digest(c.getEncoded());
|
||||
return byte2HexFormatted(publicKey);
|
||||
} catch (NoSuchAlgorithmException | CertificateEncodingException e) {
|
||||
ErrorActivity.reportError(application, e, null, null,
|
||||
ErrorInfo.make(UserAction.SOMETHING_ELSE, "none",
|
||||
"Could not retrieve SHA1 key", R.string.app_ui_crash));
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
private static String byte2HexFormatted(final byte[] arr) {
|
||||
final StringBuilder str = new StringBuilder(arr.length * 2);
|
||||
|
||||
for (int i = 0; i < arr.length; i++) {
|
||||
String h = Integer.toHexString(arr[i]);
|
||||
final int l = h.length();
|
||||
if (l == 1) {
|
||||
h = "0" + h;
|
||||
}
|
||||
if (l > 2) {
|
||||
h = h.substring(l - 2, l);
|
||||
}
|
||||
str.append(h.toUpperCase());
|
||||
if (i < (arr.length - 1)) {
|
||||
str.append(':');
|
||||
}
|
||||
}
|
||||
return str.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Method to compare the current and latest available app version.
|
||||
* If a newer version is available, we show the update notification.
|
||||
*
|
||||
* @param application The application
|
||||
* @param versionName Name of new version
|
||||
* @param apkLocationUrl Url with the new apk
|
||||
* @param versionCode Code of new version
|
||||
*/
|
||||
private static void compareAppVersionAndShowNotification(@NonNull final Application application,
|
||||
final String versionName,
|
||||
final String apkLocationUrl,
|
||||
final int versionCode) {
|
||||
final int notificationId = 2000;
|
||||
|
||||
if (BuildConfig.VERSION_CODE < versionCode) {
|
||||
// A pending intent to open the apk location url in the browser.
|
||||
final Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(apkLocationUrl));
|
||||
final PendingIntent pendingIntent
|
||||
= PendingIntent.getActivity(application, 0, intent, 0);
|
||||
|
||||
final String channelId = application
|
||||
.getString(R.string.app_update_notification_channel_id);
|
||||
final NotificationCompat.Builder notificationBuilder
|
||||
= new NotificationCompat.Builder(application, channelId)
|
||||
.setSmallIcon(R.drawable.ic_newpipe_update)
|
||||
.setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
|
||||
.setContentIntent(pendingIntent)
|
||||
.setAutoCancel(true)
|
||||
.setContentTitle(application
|
||||
.getString(R.string.app_update_notification_content_title))
|
||||
.setContentText(application
|
||||
.getString(R.string.app_update_notification_content_text)
|
||||
+ " " + versionName);
|
||||
|
||||
final NotificationManagerCompat notificationManager
|
||||
= NotificationManagerCompat.from(application);
|
||||
notificationManager.notify(notificationId, notificationBuilder.build());
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean isConnected(@NonNull final App app) {
|
||||
final ConnectivityManager connectivityManager =
|
||||
ContextCompat.getSystemService(app, ConnectivityManager.class);
|
||||
return connectivityManager != null && connectivityManager.getActiveNetworkInfo() != null
|
||||
&& connectivityManager.getActiveNetworkInfo().isConnected();
|
||||
}
|
||||
|
||||
public static boolean isGithubApk(@NonNull final App app) {
|
||||
return getCertificateSHA1Fingerprint(app).equals(GITHUB_APK_SHA1);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public static Disposable checkNewVersion(@NonNull final App app) {
|
||||
final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(app);
|
||||
|
||||
// Check if user has enabled/disabled update checking
|
||||
// and if the current apk is a github one or not.
|
||||
if (!prefs.getBoolean(app.getString(R.string.update_app_key), true) || !isGithubApk(app)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return Maybe
|
||||
.fromCallable(() -> {
|
||||
if (!isConnected(app)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Make a network request to get latest NewPipe data.
|
||||
return DownloaderImpl.getInstance().get(NEWPIPE_API_URL).responseBody();
|
||||
})
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(
|
||||
response -> {
|
||||
// Parse the json from the response.
|
||||
try {
|
||||
final JsonObject githubStableObject = JsonParser.object()
|
||||
.from(response).getObject("flavors").getObject("github")
|
||||
.getObject("stable");
|
||||
|
||||
final String versionName = githubStableObject
|
||||
.getString("version");
|
||||
final int versionCode = githubStableObject
|
||||
.getInt("version_code");
|
||||
final String apkLocationUrl = githubStableObject
|
||||
.getString("apk");
|
||||
|
||||
compareAppVersionAndShowNotification(app, versionName,
|
||||
apkLocationUrl, versionCode);
|
||||
} catch (final JsonParserException e) {
|
||||
// connectivity problems, do not alarm user and fail silently
|
||||
if (DEBUG) {
|
||||
Log.w(TAG, "Could not get NewPipe API: invalid json", e);
|
||||
}
|
||||
}
|
||||
},
|
||||
e -> {
|
||||
// connectivity problems, do not alarm user and fail silently
|
||||
if (DEBUG) {
|
||||
Log.w(TAG, "Could not get NewPipe API: network problem", e);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,238 +0,0 @@
|
||||
package org.schabi.newpipe;
|
||||
|
||||
import android.app.Application;
|
||||
import android.app.PendingIntent;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.SharedPreferences;
|
||||
import android.content.pm.PackageInfo;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.content.pm.Signature;
|
||||
import android.net.ConnectivityManager;
|
||||
import android.net.Uri;
|
||||
import android.os.AsyncTask;
|
||||
import android.preference.PreferenceManager;
|
||||
import androidx.core.app.NotificationCompat;
|
||||
import androidx.core.app.NotificationManagerCompat;
|
||||
import android.util.Log;
|
||||
|
||||
import org.json.JSONException;
|
||||
import org.json.JSONObject;
|
||||
import org.schabi.newpipe.report.ErrorActivity;
|
||||
import org.schabi.newpipe.report.UserAction;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.security.cert.CertificateEncodingException;
|
||||
import java.security.cert.CertificateException;
|
||||
import java.security.cert.CertificateFactory;
|
||||
import java.security.cert.X509Certificate;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import okhttp3.OkHttpClient;
|
||||
import okhttp3.Request;
|
||||
import okhttp3.Response;
|
||||
|
||||
/**
|
||||
* AsyncTask to check if there is a newer version of the NewPipe github apk available or not.
|
||||
* If there is a newer version we show a notification, informing the user. On tapping
|
||||
* the notification, the user will be directed to the download link.
|
||||
*/
|
||||
public class CheckForNewAppVersionTask extends AsyncTask<Void, Void, String> {
|
||||
|
||||
private static final boolean DEBUG = MainActivity.DEBUG;
|
||||
private static final String TAG = CheckForNewAppVersionTask.class.getSimpleName();
|
||||
private static final Application app = App.getApp();
|
||||
private static final String GITHUB_APK_SHA1 = "B0:2E:90:7C:1C:D6:FC:57:C3:35:F0:88:D0:8F:50:5F:94:E4:D2:15";
|
||||
private static final String newPipeApiUrl = "https://newpipe.schabi.org/api/data.json";
|
||||
private static final int timeoutPeriod = 30;
|
||||
|
||||
private SharedPreferences mPrefs;
|
||||
private OkHttpClient client;
|
||||
|
||||
@Override
|
||||
protected void onPreExecute() {
|
||||
|
||||
mPrefs = PreferenceManager.getDefaultSharedPreferences(app);
|
||||
|
||||
// Check if user has enabled/ disabled update checking
|
||||
// and if the current apk is a github one or not.
|
||||
if (!mPrefs.getBoolean(app.getString(R.string.update_app_key), true)
|
||||
|| !isGithubApk()) {
|
||||
this.cancel(true);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String doInBackground(Void... voids) {
|
||||
|
||||
if(isCancelled() || !isConnected()) return null;
|
||||
|
||||
// Make a network request to get latest NewPipe data.
|
||||
if (client == null) {
|
||||
|
||||
client = new OkHttpClient
|
||||
.Builder()
|
||||
.readTimeout(timeoutPeriod, TimeUnit.SECONDS)
|
||||
.build();
|
||||
}
|
||||
|
||||
Request request = new Request.Builder()
|
||||
.url(newPipeApiUrl)
|
||||
.build();
|
||||
|
||||
try {
|
||||
Response response = client.newCall(request).execute();
|
||||
return response.body().string();
|
||||
} catch (IOException ex) {
|
||||
// connectivity problems, do not alarm user and fail silently
|
||||
if (DEBUG) Log.w(TAG, Log.getStackTraceString(ex));
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPostExecute(String response) {
|
||||
|
||||
// Parse the json from the response.
|
||||
if (response != null) {
|
||||
|
||||
try {
|
||||
JSONObject mainObject = new JSONObject(response);
|
||||
JSONObject flavoursObject = mainObject.getJSONObject("flavors");
|
||||
JSONObject githubObject = flavoursObject.getJSONObject("github");
|
||||
JSONObject githubStableObject = githubObject.getJSONObject("stable");
|
||||
|
||||
String versionName = githubStableObject.getString("version");
|
||||
String versionCode = githubStableObject.getString("version_code");
|
||||
String apkLocationUrl = githubStableObject.getString("apk");
|
||||
|
||||
compareAppVersionAndShowNotification(versionName, apkLocationUrl, versionCode);
|
||||
|
||||
} catch (JSONException ex) {
|
||||
// connectivity problems, do not alarm user and fail silently
|
||||
if (DEBUG) Log.w(TAG, Log.getStackTraceString(ex));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Method to compare the current and latest available app version.
|
||||
* If a newer version is available, we show the update notification.
|
||||
* @param versionName
|
||||
* @param apkLocationUrl
|
||||
*/
|
||||
private void compareAppVersionAndShowNotification(String versionName,
|
||||
String apkLocationUrl,
|
||||
String versionCode) {
|
||||
|
||||
int NOTIFICATION_ID = 2000;
|
||||
|
||||
if (BuildConfig.VERSION_CODE < Integer.valueOf(versionCode)) {
|
||||
|
||||
// A pending intent to open the apk location url in the browser.
|
||||
Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(apkLocationUrl));
|
||||
PendingIntent pendingIntent
|
||||
= PendingIntent.getActivity(app, 0, intent, 0);
|
||||
|
||||
NotificationCompat.Builder notificationBuilder = new NotificationCompat
|
||||
.Builder(app, app.getString(R.string.app_update_notification_channel_id))
|
||||
.setSmallIcon(R.drawable.ic_newpipe_update)
|
||||
.setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
|
||||
.setContentIntent(pendingIntent)
|
||||
.setAutoCancel(true)
|
||||
.setContentTitle(app.getString(R.string.app_update_notification_content_title))
|
||||
.setContentText(app.getString(R.string.app_update_notification_content_text)
|
||||
+ " " + versionName);
|
||||
|
||||
NotificationManagerCompat notificationManager = NotificationManagerCompat.from(app);
|
||||
notificationManager.notify(NOTIFICATION_ID, notificationBuilder.build());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Method to get the apk's SHA1 key.
|
||||
* https://stackoverflow.com/questions/9293019/get-certificate-fingerprint-from-android-app#22506133
|
||||
*/
|
||||
private static String getCertificateSHA1Fingerprint() {
|
||||
|
||||
PackageManager pm = app.getPackageManager();
|
||||
String packageName = app.getPackageName();
|
||||
int flags = PackageManager.GET_SIGNATURES;
|
||||
PackageInfo packageInfo = null;
|
||||
|
||||
try {
|
||||
packageInfo = pm.getPackageInfo(packageName, flags);
|
||||
} catch (PackageManager.NameNotFoundException ex) {
|
||||
ErrorActivity.reportError(app, ex, null, null,
|
||||
ErrorActivity.ErrorInfo.make(UserAction.SOMETHING_ELSE, "none",
|
||||
"Could not find package info", R.string.app_ui_crash));
|
||||
}
|
||||
|
||||
Signature[] signatures = packageInfo.signatures;
|
||||
byte[] cert = signatures[0].toByteArray();
|
||||
InputStream input = new ByteArrayInputStream(cert);
|
||||
|
||||
CertificateFactory cf = null;
|
||||
X509Certificate c = null;
|
||||
|
||||
try {
|
||||
cf = CertificateFactory.getInstance("X509");
|
||||
c = (X509Certificate) cf.generateCertificate(input);
|
||||
} catch (CertificateException ex) {
|
||||
ErrorActivity.reportError(app, ex, null, null,
|
||||
ErrorActivity.ErrorInfo.make(UserAction.SOMETHING_ELSE, "none",
|
||||
"Certificate error", R.string.app_ui_crash));
|
||||
}
|
||||
|
||||
String hexString = null;
|
||||
|
||||
try {
|
||||
MessageDigest md = MessageDigest.getInstance("SHA1");
|
||||
byte[] publicKey = md.digest(c.getEncoded());
|
||||
hexString = byte2HexFormatted(publicKey);
|
||||
} catch (NoSuchAlgorithmException ex1) {
|
||||
ErrorActivity.reportError(app, ex1, null, null,
|
||||
ErrorActivity.ErrorInfo.make(UserAction.SOMETHING_ELSE, "none",
|
||||
"Could not retrieve SHA1 key", R.string.app_ui_crash));
|
||||
} catch (CertificateEncodingException ex2) {
|
||||
ErrorActivity.reportError(app, ex2, null, null,
|
||||
ErrorActivity.ErrorInfo.make(UserAction.SOMETHING_ELSE, "none",
|
||||
"Could not retrieve SHA1 key", R.string.app_ui_crash));
|
||||
}
|
||||
|
||||
return hexString;
|
||||
}
|
||||
|
||||
private static String byte2HexFormatted(byte[] arr) {
|
||||
|
||||
StringBuilder str = new StringBuilder(arr.length * 2);
|
||||
|
||||
for (int i = 0; i < arr.length; i++) {
|
||||
String h = Integer.toHexString(arr[i]);
|
||||
int l = h.length();
|
||||
if (l == 1) h = "0" + h;
|
||||
if (l > 2) h = h.substring(l - 2, l);
|
||||
str.append(h.toUpperCase());
|
||||
if (i < (arr.length - 1)) str.append(':');
|
||||
}
|
||||
return str.toString();
|
||||
}
|
||||
|
||||
public static boolean isGithubApk() {
|
||||
|
||||
return getCertificateSHA1Fingerprint().equals(GITHUB_APK_SHA1);
|
||||
}
|
||||
|
||||
private boolean isConnected() {
|
||||
|
||||
ConnectivityManager cm =
|
||||
(ConnectivityManager) app.getSystemService(Context.CONNECTIVITY_SERVICE);
|
||||
return cm.getActiveNetworkInfo() != null
|
||||
&& cm.getActiveNetworkInfo().isConnected();
|
||||
}
|
||||
}
|
||||
@@ -1,12 +1,18 @@
|
||||
package org.schabi.newpipe;
|
||||
|
||||
import android.content.Context;
|
||||
import android.os.Build;
|
||||
import android.text.TextUtils;
|
||||
import androidx.preference.PreferenceManager;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
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.CookieUtils;
|
||||
import org.schabi.newpipe.util.InfoCache;
|
||||
import org.schabi.newpipe.util.TLSSocketFactoryCompat;
|
||||
|
||||
import java.io.IOException;
|
||||
@@ -17,6 +23,7 @@ import java.security.KeyStoreException;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
@@ -26,9 +33,6 @@ 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;
|
||||
@@ -37,42 +41,139 @@ 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";
|
||||
public final 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";
|
||||
public static final String YOUTUBE_RESTRICTED_MODE_COOKIE_KEY
|
||||
= "youtube_restricted_mode_key";
|
||||
public static final String YOUTUBE_RESTRICTED_MODE_COOKIE = "PREF=f2=8000000";
|
||||
public static final String YOUTUBE_DOMAIN = "youtube.com";
|
||||
|
||||
private static DownloaderImpl instance;
|
||||
private String mCookies;
|
||||
private OkHttpClient client;
|
||||
private final Map<String, String> mCookies;
|
||||
private final OkHttpClient client;
|
||||
|
||||
private DownloaderImpl(OkHttpClient.Builder builder) {
|
||||
private DownloaderImpl(final 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))
|
||||
// .cache(new Cache(new File(context.getExternalCacheDir(), "okhttp"),
|
||||
// 16 * 1024 * 1024))
|
||||
.build();
|
||||
this.mCookies = new HashMap<>();
|
||||
}
|
||||
|
||||
/**
|
||||
* It's recommended to call exactly once in the entire lifetime of the application.
|
||||
*
|
||||
* @param builder if null, default builder will be used
|
||||
* @return a new instance of {@link DownloaderImpl}
|
||||
*/
|
||||
public static DownloaderImpl init(@Nullable OkHttpClient.Builder builder) {
|
||||
return instance = new DownloaderImpl(builder != null ? builder : new OkHttpClient.Builder());
|
||||
public static DownloaderImpl init(@Nullable final OkHttpClient.Builder builder) {
|
||||
instance = new DownloaderImpl(
|
||||
builder != null ? builder : new OkHttpClient.Builder());
|
||||
return instance;
|
||||
}
|
||||
|
||||
public static DownloaderImpl getInstance() {
|
||||
return instance;
|
||||
}
|
||||
|
||||
public String getCookies() {
|
||||
return mCookies;
|
||||
/**
|
||||
* 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.
|
||||
* </p>
|
||||
*
|
||||
* @param builder The HTTPClient Builder on which TLS is enabled on (will be modified in-place)
|
||||
*/
|
||||
private static void enableModernTLS(final OkHttpClient.Builder builder) {
|
||||
try {
|
||||
// get the default TrustManager
|
||||
final TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance(
|
||||
TrustManagerFactory.getDefaultAlgorithm());
|
||||
trustManagerFactory.init((KeyStore) null);
|
||||
final TrustManager[] trustManagers = trustManagerFactory.getTrustManagers();
|
||||
if (trustManagers.length != 1 || !(trustManagers[0] instanceof X509TrustManager)) {
|
||||
throw new IllegalStateException("Unexpected default trust managers:"
|
||||
+ Arrays.toString(trustManagers));
|
||||
}
|
||||
final X509TrustManager trustManager = (X509TrustManager) trustManagers[0];
|
||||
|
||||
// insert our own TLSSocketFactory
|
||||
final 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
|
||||
final List<CipherSuite> cipherSuites =
|
||||
new ArrayList<>(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);
|
||||
final ConnectionSpec legacyTLS = new ConnectionSpec.Builder(ConnectionSpec.MODERN_TLS)
|
||||
.cipherSuites(cipherSuites.toArray(new CipherSuite[0]))
|
||||
.build();
|
||||
|
||||
builder.connectionSpecs(Arrays.asList(legacyTLS, ConnectionSpec.CLEARTEXT));
|
||||
} catch (final KeyManagementException | NoSuchAlgorithmException | KeyStoreException e) {
|
||||
if (DEBUG) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void setCookies(String cookies) {
|
||||
mCookies = cookies;
|
||||
public String getCookies(final String url) {
|
||||
final List<String> resultCookies = new ArrayList<>();
|
||||
if (url.contains(YOUTUBE_DOMAIN)) {
|
||||
final String youtubeCookie = getCookie(YOUTUBE_RESTRICTED_MODE_COOKIE_KEY);
|
||||
if (youtubeCookie != null) {
|
||||
resultCookies.add(youtubeCookie);
|
||||
}
|
||||
}
|
||||
// Recaptcha cookie is always added TODO: not sure if this is necessary
|
||||
final String recaptchaCookie = getCookie(ReCaptchaActivity.RECAPTCHA_COOKIES_KEY);
|
||||
if (recaptchaCookie != null) {
|
||||
resultCookies.add(recaptchaCookie);
|
||||
}
|
||||
return CookieUtils.concatCookies(resultCookies);
|
||||
}
|
||||
|
||||
public String getCookie(final String key) {
|
||||
return mCookies.get(key);
|
||||
}
|
||||
|
||||
public void setCookie(final String key, final String cookie) {
|
||||
mCookies.put(key, cookie);
|
||||
}
|
||||
|
||||
public void removeCookie(final String key) {
|
||||
mCookies.remove(key);
|
||||
}
|
||||
|
||||
public void updateYoutubeRestrictedModeCookies(final Context context) {
|
||||
final String restrictedModeEnabledKey =
|
||||
context.getString(R.string.youtube_restricted_mode_enabled);
|
||||
final boolean restrictedModeEnabled = PreferenceManager.getDefaultSharedPreferences(context)
|
||||
.getBoolean(restrictedModeEnabledKey, false);
|
||||
updateYoutubeRestrictedModeCookies(restrictedModeEnabled);
|
||||
}
|
||||
|
||||
public void updateYoutubeRestrictedModeCookies(final boolean youtubeRestrictedModeEnabled) {
|
||||
if (youtubeRestrictedModeEnabled) {
|
||||
setCookie(YOUTUBE_RESTRICTED_MODE_COOKIE_KEY,
|
||||
YOUTUBE_RESTRICTED_MODE_COOKIE);
|
||||
} else {
|
||||
removeCookie(YOUTUBE_RESTRICTED_MODE_COOKIE_KEY);
|
||||
}
|
||||
InfoCache.getInstance().clearCache();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -81,25 +182,26 @@ public class DownloaderImpl extends Downloader {
|
||||
* @param url an url pointing to the content
|
||||
* @return the size of the content, in bytes
|
||||
*/
|
||||
public long getContentLength(String url) throws IOException {
|
||||
public long getContentLength(final String url) throws IOException {
|
||||
try {
|
||||
final Response response = head(url);
|
||||
return Long.parseLong(response.getHeader("Content-Length"));
|
||||
} catch (NumberFormatException e) {
|
||||
} catch (final NumberFormatException e) {
|
||||
throw new IOException("Invalid content length", e);
|
||||
} catch (ReCaptchaException e) {
|
||||
} catch (final ReCaptchaException e) {
|
||||
throw new IOException(e);
|
||||
}
|
||||
}
|
||||
|
||||
public InputStream stream(String siteUrl) throws IOException {
|
||||
public InputStream stream(final 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 String cookies = getCookies(siteUrl);
|
||||
if (!cookies.isEmpty()) {
|
||||
requestBuilder.addHeader("Cookie", cookies);
|
||||
}
|
||||
|
||||
final okhttp3.Request request = requestBuilder.build();
|
||||
@@ -116,13 +218,14 @@ public class DownloaderImpl extends Downloader {
|
||||
}
|
||||
|
||||
return body.byteStream();
|
||||
} catch (ReCaptchaException e) {
|
||||
} catch (final ReCaptchaException e) {
|
||||
throw new IOException(e.getMessage(), e.getCause());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Response execute(@NonNull Request request) throws IOException, ReCaptchaException {
|
||||
public Response execute(@NonNull final Request request)
|
||||
throws IOException, ReCaptchaException {
|
||||
final String httpMethod = request.httpMethod();
|
||||
final String url = request.url();
|
||||
final Map<String, List<String>> headers = request.headers();
|
||||
@@ -137,17 +240,18 @@ public class DownloaderImpl extends Downloader {
|
||||
.method(httpMethod, requestBody).url(url)
|
||||
.addHeader("User-Agent", USER_AGENT);
|
||||
|
||||
if (!TextUtils.isEmpty(mCookies)) {
|
||||
requestBuilder.addHeader("Cookie", mCookies);
|
||||
final String cookies = getCookies(url);
|
||||
if (!cookies.isEmpty()) {
|
||||
requestBuilder.addHeader("Cookie", cookies);
|
||||
}
|
||||
|
||||
for (Map.Entry<String, List<String>> pair : headers.entrySet()) {
|
||||
for (final 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) {
|
||||
for (final String headerValue : headerValueList) {
|
||||
requestBuilder.addHeader(headerName, headerValue);
|
||||
}
|
||||
} else if (headerValueList.size() == 1) {
|
||||
@@ -172,49 +276,7 @@ public class DownloaderImpl extends Downloader {
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
return new Response(response.code(), response.message(), response.headers().toMultimap(),
|
||||
responseBodyToReturn, latestUrl);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
|
||||
package org.schabi.newpipe;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
@@ -27,22 +26,8 @@ import android.os.Bundle;
|
||||
|
||||
public class ExitActivity extends Activity {
|
||||
|
||||
@SuppressLint("NewApi")
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
if (Build.VERSION.SDK_INT >= 21) {
|
||||
finishAndRemoveTask();
|
||||
} else {
|
||||
finish();
|
||||
}
|
||||
|
||||
System.exit(0);
|
||||
}
|
||||
|
||||
public static void exitAndRemoveFromRecentApps(Activity activity) {
|
||||
Intent intent = new Intent(activity, ExitActivity.class);
|
||||
public static void exitAndRemoveFromRecentApps(final Activity activity) {
|
||||
final Intent intent = new Intent(activity, ExitActivity.class);
|
||||
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK
|
||||
| Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS
|
||||
@@ -51,4 +36,18 @@ public class ExitActivity extends Activity {
|
||||
|
||||
activity.startActivity(intent);
|
||||
}
|
||||
|
||||
@SuppressLint("NewApi")
|
||||
@Override
|
||||
protected void onCreate(final Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
||||
finishAndRemoveTask();
|
||||
} else {
|
||||
finish();
|
||||
}
|
||||
|
||||
System.exit(0);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ import android.annotation.SuppressLint;
|
||||
import android.content.Context;
|
||||
import android.content.SharedPreferences;
|
||||
import android.content.res.Resources;
|
||||
import android.preference.PreferenceManager;
|
||||
import androidx.preference.PreferenceManager;
|
||||
|
||||
import com.nostra13.universalimageloader.core.download.BaseImageDownloader;
|
||||
|
||||
@@ -18,7 +18,7 @@ public class ImageDownloader extends BaseImageDownloader {
|
||||
private final SharedPreferences preferences;
|
||||
private final String downloadThumbnailKey;
|
||||
|
||||
public ImageDownloader(Context context) {
|
||||
public ImageDownloader(final Context context) {
|
||||
super(context);
|
||||
this.resources = context.getResources();
|
||||
this.preferences = PreferenceManager.getDefaultSharedPreferences(context);
|
||||
@@ -31,7 +31,7 @@ public class ImageDownloader extends BaseImageDownloader {
|
||||
|
||||
@SuppressLint("ResourceType")
|
||||
@Override
|
||||
public InputStream getStream(String imageUri, Object extra) throws IOException {
|
||||
public InputStream getStream(final String imageUri, final Object extra) throws IOException {
|
||||
if (isDownloadingThumbnail()) {
|
||||
return super.getStream(imageUri, extra);
|
||||
} else {
|
||||
@@ -39,7 +39,8 @@ public class ImageDownloader extends BaseImageDownloader {
|
||||
}
|
||||
}
|
||||
|
||||
protected InputStream getStreamFromNetwork(String imageUri, Object extra) throws IOException {
|
||||
protected InputStream getStreamFromNetwork(final String imageUri, final Object extra)
|
||||
throws IOException {
|
||||
final DownloaderImpl downloader = (DownloaderImpl) NewPipe.getDownloader();
|
||||
return downloader.stream(imageUri);
|
||||
}
|
||||
|
||||
@@ -20,24 +20,27 @@
|
||||
|
||||
package org.schabi.newpipe;
|
||||
|
||||
import android.content.BroadcastReceiver;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.IntentFilter;
|
||||
import android.content.SharedPreferences;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.os.Handler;
|
||||
import android.os.Looper;
|
||||
import android.preference.PreferenceManager;
|
||||
import android.util.Log;
|
||||
import android.view.KeyEvent;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
import android.view.Window;
|
||||
import android.view.WindowManager;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.AdapterView;
|
||||
import android.widget.ArrayAdapter;
|
||||
import android.widget.Button;
|
||||
import android.widget.FrameLayout;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.Spinner;
|
||||
import android.widget.TextView;
|
||||
@@ -51,7 +54,9 @@ import androidx.core.view.GravityCompat;
|
||||
import androidx.drawerlayout.widget.DrawerLayout;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.fragment.app.FragmentManager;
|
||||
import androidx.preference.PreferenceManager;
|
||||
|
||||
import com.google.android.material.bottomsheet.BottomSheetBehavior;
|
||||
import com.google.android.material.navigation.NavigationView;
|
||||
|
||||
import org.schabi.newpipe.extractor.NewPipe;
|
||||
@@ -62,20 +67,28 @@ import org.schabi.newpipe.fragments.BackPressable;
|
||||
import org.schabi.newpipe.fragments.MainFragment;
|
||||
import org.schabi.newpipe.fragments.detail.VideoDetailFragment;
|
||||
import org.schabi.newpipe.fragments.list.search.SearchFragment;
|
||||
import org.schabi.newpipe.player.VideoPlayer;
|
||||
import org.schabi.newpipe.player.event.OnKeyDownListener;
|
||||
import org.schabi.newpipe.player.helper.PlayerHolder;
|
||||
import org.schabi.newpipe.player.playqueue.PlayQueue;
|
||||
import org.schabi.newpipe.report.ErrorActivity;
|
||||
import org.schabi.newpipe.util.Constants;
|
||||
import org.schabi.newpipe.util.DeviceUtils;
|
||||
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.SerializedCache;
|
||||
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 org.schabi.newpipe.views.FocusOverlayView;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
import static org.schabi.newpipe.util.Localization.assureCorrectAppLanguage;
|
||||
|
||||
@@ -83,20 +96,23 @@ public class MainActivity extends AppCompatActivity {
|
||||
private static final String TAG = "MainActivity";
|
||||
public static final boolean DEBUG = !BuildConfig.BUILD_TYPE.equals("release");
|
||||
|
||||
private ActionBarDrawerToggle toggle = null;
|
||||
private DrawerLayout drawer = null;
|
||||
private NavigationView drawerItems = null;
|
||||
private TextView headerServiceView = null;
|
||||
private Button toggleServiceButton = null;
|
||||
private ActionBarDrawerToggle toggle;
|
||||
private DrawerLayout drawer;
|
||||
private NavigationView drawerItems;
|
||||
private ImageView headerServiceIcon;
|
||||
private TextView headerServiceView;
|
||||
private Button toggleServiceButton;
|
||||
|
||||
private boolean servicesShown = false;
|
||||
private ImageView serviceArrow;
|
||||
|
||||
private static final int ITEM_ID_SUBSCRIPTIONS = - 1;
|
||||
private static final int ITEM_ID_FEED = - 2;
|
||||
private static final int ITEM_ID_BOOKMARKS = - 3;
|
||||
private static final int ITEM_ID_DOWNLOADS = - 4;
|
||||
private static final int ITEM_ID_HISTORY = - 5;
|
||||
private BroadcastReceiver broadcastReceiver;
|
||||
|
||||
private static final int ITEM_ID_SUBSCRIPTIONS = -1;
|
||||
private static final int ITEM_ID_FEED = -2;
|
||||
private static final int ITEM_ID_BOOKMARKS = -3;
|
||||
private static final int ITEM_ID_DOWNLOADS = -4;
|
||||
private static final int ITEM_ID_HISTORY = -5;
|
||||
private static final int ITEM_ID_SETTINGS = 0;
|
||||
private static final int ITEM_ID_ABOUT = 1;
|
||||
|
||||
@@ -107,8 +123,11 @@ public class MainActivity extends AppCompatActivity {
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
if (DEBUG) Log.d(TAG, "onCreate() called with: savedInstanceState = [" + savedInstanceState + "]");
|
||||
protected void onCreate(final 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) {
|
||||
@@ -120,21 +139,21 @@ public class MainActivity extends AppCompatActivity {
|
||||
super.onCreate(savedInstanceState);
|
||||
setContentView(R.layout.activity_main);
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
||||
Window w = getWindow();
|
||||
w.setFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS, WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS);
|
||||
}
|
||||
|
||||
if (getSupportFragmentManager() != null && getSupportFragmentManager().getBackStackEntryCount() == 0) {
|
||||
if (getSupportFragmentManager().getBackStackEntryCount() == 0) {
|
||||
initFragments();
|
||||
}
|
||||
|
||||
setSupportActionBar(findViewById(R.id.toolbar));
|
||||
try {
|
||||
setupDrawer();
|
||||
} catch (Exception e) {
|
||||
} catch (final Exception e) {
|
||||
ErrorActivity.reportUiError(this, e);
|
||||
}
|
||||
|
||||
if (DeviceUtils.isTv(this)) {
|
||||
FocusOverlayView.setupFocusObserver(this);
|
||||
}
|
||||
openMiniPlayerUponPlayerStarted();
|
||||
}
|
||||
|
||||
private void setupDrawer() throws Exception {
|
||||
@@ -143,56 +162,59 @@ public class MainActivity extends AppCompatActivity {
|
||||
drawerItems = findViewById(R.id.navigation);
|
||||
|
||||
//Tabs
|
||||
int currentServiceId = ServiceHelper.getSelectedServiceId(this);
|
||||
StreamingService service = NewPipe.getService(currentServiceId);
|
||||
final int currentServiceId = ServiceHelper.getSelectedServiceId(this);
|
||||
final StreamingService service = NewPipe.getService(currentServiceId);
|
||||
|
||||
int kioskId = 0;
|
||||
|
||||
for (final String ks : service.getKioskList().getAvailableKiosks()) {
|
||||
drawerItems.getMenu()
|
||||
.add(R.id.menu_tabs_group, kioskId, 0, KioskTranslator.getTranslatedKioskName(ks, this))
|
||||
.setIcon(KioskTranslator.getKioskIcons(ks, this));
|
||||
kioskId ++;
|
||||
.add(R.id.menu_tabs_group, kioskId, 0, KioskTranslator
|
||||
.getTranslatedKioskName(ks, this))
|
||||
.setIcon(KioskTranslator.getKioskIcon(ks, this));
|
||||
kioskId++;
|
||||
}
|
||||
|
||||
drawerItems.getMenu()
|
||||
.add(R.id.menu_tabs_group, ITEM_ID_SUBSCRIPTIONS, ORDER, R.string.tab_subscriptions)
|
||||
.add(R.id.menu_tabs_group, ITEM_ID_SUBSCRIPTIONS, ORDER,
|
||||
R.string.tab_subscriptions)
|
||||
.setIcon(ThemeHelper.resolveResourceIdFromAttr(this, R.attr.ic_channel));
|
||||
drawerItems.getMenu()
|
||||
.add(R.id.menu_tabs_group, ITEM_ID_FEED, ORDER, R.string.fragment_feed_title)
|
||||
.setIcon(ThemeHelper.resolveResourceIdFromAttr(this, R.attr.rss));
|
||||
.setIcon(ThemeHelper.resolveResourceIdFromAttr(this, R.attr.ic_rss));
|
||||
drawerItems.getMenu()
|
||||
.add(R.id.menu_tabs_group, ITEM_ID_BOOKMARKS, ORDER, R.string.tab_bookmarks)
|
||||
.setIcon(ThemeHelper.resolveResourceIdFromAttr(this, R.attr.ic_bookmark));
|
||||
drawerItems.getMenu()
|
||||
.add(R.id.menu_tabs_group, ITEM_ID_DOWNLOADS, ORDER, R.string.downloads)
|
||||
.setIcon(ThemeHelper.resolveResourceIdFromAttr(this, R.attr.download));
|
||||
.setIcon(ThemeHelper.resolveResourceIdFromAttr(this, R.attr.ic_file_download));
|
||||
drawerItems.getMenu()
|
||||
.add(R.id.menu_tabs_group, ITEM_ID_HISTORY, ORDER, R.string.action_history)
|
||||
.setIcon(ThemeHelper.resolveResourceIdFromAttr(this, R.attr.history));
|
||||
.setIcon(ThemeHelper.resolveResourceIdFromAttr(this, R.attr.ic_history));
|
||||
|
||||
//Settings and About
|
||||
drawerItems.getMenu()
|
||||
.add(R.id.menu_options_about_group, ITEM_ID_SETTINGS, ORDER, R.string.settings)
|
||||
.setIcon(ThemeHelper.resolveResourceIdFromAttr(this, R.attr.settings));
|
||||
.setIcon(ThemeHelper.resolveResourceIdFromAttr(this, R.attr.ic_settings));
|
||||
drawerItems.getMenu()
|
||||
.add(R.id.menu_options_about_group, ITEM_ID_ABOUT, ORDER, R.string.tab_about)
|
||||
.setIcon(ThemeHelper.resolveResourceIdFromAttr(this, R.attr.info));
|
||||
.setIcon(ThemeHelper.resolveResourceIdFromAttr(this, R.attr.ic_info_outline));
|
||||
|
||||
toggle = new ActionBarDrawerToggle(this, drawer, toolbar, R.string.drawer_open, R.string.drawer_close);
|
||||
toggle = new ActionBarDrawerToggle(this, drawer, toolbar, R.string.drawer_open,
|
||||
R.string.drawer_close);
|
||||
toggle.syncState();
|
||||
drawer.addDrawerListener(toggle);
|
||||
drawer.addDrawerListener(new DrawerLayout.SimpleDrawerListener() {
|
||||
private int lastService;
|
||||
|
||||
@Override
|
||||
public void onDrawerOpened(View drawerView) {
|
||||
public void onDrawerOpened(final View drawerView) {
|
||||
lastService = ServiceHelper.getSelectedServiceId(MainActivity.this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDrawerClosed(View drawerView) {
|
||||
if(servicesShown) {
|
||||
public void onDrawerClosed(final View drawerView) {
|
||||
if (servicesShown) {
|
||||
toggleServices();
|
||||
}
|
||||
if (lastService != ServiceHelper.getSelectedServiceId(MainActivity.this)) {
|
||||
@@ -205,7 +227,7 @@ public class MainActivity extends AppCompatActivity {
|
||||
setupDrawerHeader();
|
||||
}
|
||||
|
||||
private boolean drawerItemSelected(MenuItem item) {
|
||||
private boolean drawerItemSelected(final MenuItem item) {
|
||||
switch (item.getGroupId()) {
|
||||
case R.id.menu_services_group:
|
||||
changeService(item);
|
||||
@@ -213,7 +235,7 @@ public class MainActivity extends AppCompatActivity {
|
||||
case R.id.menu_tabs_group:
|
||||
try {
|
||||
tabSelected(item);
|
||||
} catch (Exception e) {
|
||||
} catch (final Exception e) {
|
||||
ErrorActivity.reportUiError(this, e);
|
||||
}
|
||||
break;
|
||||
@@ -228,14 +250,16 @@ public class MainActivity extends AppCompatActivity {
|
||||
return true;
|
||||
}
|
||||
|
||||
private void changeService(MenuItem item) {
|
||||
drawerItems.getMenu().getItem(ServiceHelper.getSelectedServiceId(this)).setChecked(false);
|
||||
private void changeService(final MenuItem item) {
|
||||
drawerItems.getMenu().getItem(ServiceHelper.getSelectedServiceId(this))
|
||||
.setChecked(false);
|
||||
ServiceHelper.setSelectedServiceId(this, item.getItemId());
|
||||
drawerItems.getMenu().getItem(ServiceHelper.getSelectedServiceId(this)).setChecked(true);
|
||||
drawerItems.getMenu().getItem(ServiceHelper.getSelectedServiceId(this))
|
||||
.setChecked(true);
|
||||
}
|
||||
|
||||
private void tabSelected(MenuItem item) throws ExtractionException {
|
||||
switch(item.getItemId()) {
|
||||
private void tabSelected(final MenuItem item) throws ExtractionException {
|
||||
switch (item.getItemId()) {
|
||||
case ITEM_ID_SUBSCRIPTIONS:
|
||||
NavigationHelper.openSubscriptionFragment(getSupportFragmentManager());
|
||||
break;
|
||||
@@ -252,25 +276,26 @@ public class MainActivity extends AppCompatActivity {
|
||||
NavigationHelper.openStatisticFragment(getSupportFragmentManager());
|
||||
break;
|
||||
default:
|
||||
int currentServiceId = ServiceHelper.getSelectedServiceId(this);
|
||||
StreamingService service = NewPipe.getService(currentServiceId);
|
||||
final int currentServiceId = ServiceHelper.getSelectedServiceId(this);
|
||||
final StreamingService service = NewPipe.getService(currentServiceId);
|
||||
String serviceName = "";
|
||||
|
||||
int kioskId = 0;
|
||||
for (final String ks : service.getKioskList().getAvailableKiosks()) {
|
||||
if(kioskId == item.getItemId()) {
|
||||
if (kioskId == item.getItemId()) {
|
||||
serviceName = ks;
|
||||
}
|
||||
kioskId ++;
|
||||
kioskId++;
|
||||
}
|
||||
|
||||
NavigationHelper.openKioskFragment(getSupportFragmentManager(), currentServiceId, serviceName);
|
||||
NavigationHelper.openKioskFragment(getSupportFragmentManager(), currentServiceId,
|
||||
serviceName);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private void optionsAboutSelected(MenuItem item) {
|
||||
switch(item.getItemId()) {
|
||||
private void optionsAboutSelected(final MenuItem item) {
|
||||
switch (item.getItemId()) {
|
||||
case ITEM_ID_SETTINGS:
|
||||
NavigationHelper.openSettings(this);
|
||||
break;
|
||||
@@ -281,15 +306,28 @@ public class MainActivity extends AppCompatActivity {
|
||||
}
|
||||
|
||||
private void setupDrawerHeader() {
|
||||
NavigationView navigationView = findViewById(R.id.navigation);
|
||||
View hView = navigationView.getHeaderView(0);
|
||||
final NavigationView navigationView = findViewById(R.id.navigation);
|
||||
final View hView = navigationView.getHeaderView(0);
|
||||
|
||||
serviceArrow = hView.findViewById(R.id.drawer_arrow);
|
||||
headerServiceIcon = hView.findViewById(R.id.drawer_header_service_icon);
|
||||
headerServiceView = hView.findViewById(R.id.drawer_header_service_view);
|
||||
toggleServiceButton = hView.findViewById(R.id.drawer_header_action_button);
|
||||
toggleServiceButton.setOnClickListener(view -> {
|
||||
toggleServices();
|
||||
});
|
||||
toggleServiceButton.setOnClickListener(view -> toggleServices());
|
||||
|
||||
// If the current app name is bigger than the default "NewPipe" (7 chars),
|
||||
// let the text view grow a little more as well.
|
||||
if (getString(R.string.app_name).length() > "NewPipe".length()) {
|
||||
final TextView headerTitle = hView.findViewById(R.id.drawer_header_newpipe_title);
|
||||
final ViewGroup.LayoutParams layoutParams = headerTitle.getLayoutParams();
|
||||
layoutParams.width = ViewGroup.LayoutParams.WRAP_CONTENT;
|
||||
headerTitle.setLayoutParams(layoutParams);
|
||||
headerTitle.setMaxLines(2);
|
||||
headerTitle.setMinWidth(getResources()
|
||||
.getDimensionPixelSize(R.dimen.drawer_header_newpipe_title_default_width));
|
||||
headerTitle.setMaxWidth(getResources()
|
||||
.getDimensionPixelSize(R.dimen.drawer_header_newpipe_title_max_width));
|
||||
}
|
||||
}
|
||||
|
||||
private void toggleServices() {
|
||||
@@ -299,70 +337,76 @@ public class MainActivity extends AppCompatActivity {
|
||||
drawerItems.getMenu().removeGroup(R.id.menu_tabs_group);
|
||||
drawerItems.getMenu().removeGroup(R.id.menu_options_about_group);
|
||||
|
||||
|
||||
if(servicesShown) {
|
||||
if (servicesShown) {
|
||||
showServices();
|
||||
} else {
|
||||
try {
|
||||
showTabs();
|
||||
} catch (Exception e) {
|
||||
} catch (final Exception e) {
|
||||
ErrorActivity.reportUiError(this, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void showServices() {
|
||||
serviceArrow.setImageResource(R.drawable.ic_arrow_up_white);
|
||||
serviceArrow.setImageResource(R.drawable.ic_arrow_drop_up_white_24dp);
|
||||
|
||||
for(StreamingService s : NewPipe.getServices()) {
|
||||
final String title = s.getServiceInfo().getName() +
|
||||
(ServiceHelper.isBeta(s) ? " (beta)" : "");
|
||||
for (final StreamingService s : NewPipe.getServices()) {
|
||||
final String title = s.getServiceInfo().getName()
|
||||
+ (ServiceHelper.isBeta(s) ? " (beta)" : "");
|
||||
|
||||
MenuItem menuItem = drawerItems.getMenu()
|
||||
final 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){
|
||||
if (s.getServiceId() == 3) {
|
||||
enhancePeertubeMenu(s, menuItem);
|
||||
}
|
||||
}
|
||||
drawerItems.getMenu().getItem(ServiceHelper.getSelectedServiceId(this)).setChecked(true);
|
||||
drawerItems.getMenu().getItem(ServiceHelper.getSelectedServiceId(this))
|
||||
.setChecked(true);
|
||||
}
|
||||
|
||||
private void enhancePeertubeMenu(StreamingService s, MenuItem menuItem) {
|
||||
PeertubeInstance currentInstace = PeertubeHelper.getCurrentInstance();
|
||||
private void enhancePeertubeMenu(final StreamingService s, final MenuItem menuItem) {
|
||||
final 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<>();
|
||||
final Spinner spinner = (Spinner) LayoutInflater.from(this)
|
||||
.inflate(R.layout.instance_spinner_layout, null);
|
||||
final List<PeertubeInstance> instances = PeertubeHelper.getInstanceList(this);
|
||||
final List<String> items = new ArrayList<>();
|
||||
int defaultSelect = 0;
|
||||
for(PeertubeInstance instance: instances){
|
||||
for (final PeertubeInstance instance : instances) {
|
||||
items.add(instance.getName());
|
||||
if(instance.getUrl().equals(currentInstace.getUrl())){
|
||||
defaultSelect = items.size()-1;
|
||||
if (instance.getUrl().equals(currentInstace.getUrl())) {
|
||||
defaultSelect = items.size() - 1;
|
||||
}
|
||||
}
|
||||
ArrayAdapter<String> adapter = new ArrayAdapter<>(this, R.layout.instance_spinner_item, items);
|
||||
final 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;
|
||||
public void onItemSelected(final AdapterView<?> parent, final View view,
|
||||
final int position, final long id) {
|
||||
final 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);
|
||||
getSupportFragmentManager().popBackStack(null,
|
||||
FragmentManager.POP_BACK_STACK_INCLUSIVE);
|
||||
recreate();
|
||||
}, 300);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onNothingSelected(AdapterView<?> parent) {
|
||||
public void onNothingSelected(final AdapterView<?> parent) {
|
||||
|
||||
}
|
||||
});
|
||||
@@ -370,19 +414,20 @@ public class MainActivity extends AppCompatActivity {
|
||||
}
|
||||
|
||||
private void showTabs() throws ExtractionException {
|
||||
serviceArrow.setImageResource(R.drawable.ic_arrow_down_white);
|
||||
serviceArrow.setImageResource(R.drawable.ic_arrow_drop_down_white_24dp);
|
||||
|
||||
//Tabs
|
||||
int currentServiceId = ServiceHelper.getSelectedServiceId(this);
|
||||
StreamingService service = NewPipe.getService(currentServiceId);
|
||||
final int currentServiceId = ServiceHelper.getSelectedServiceId(this);
|
||||
final StreamingService service = NewPipe.getService(currentServiceId);
|
||||
|
||||
int kioskId = 0;
|
||||
|
||||
for (final String ks : service.getKioskList().getAvailableKiosks()) {
|
||||
drawerItems.getMenu()
|
||||
.add(R.id.menu_tabs_group, kioskId, ORDER, KioskTranslator.getTranslatedKioskName(ks, this))
|
||||
.setIcon(KioskTranslator.getKioskIcons(ks, this));
|
||||
kioskId ++;
|
||||
.add(R.id.menu_tabs_group, kioskId, ORDER,
|
||||
KioskTranslator.getTranslatedKioskName(ks, this))
|
||||
.setIcon(KioskTranslator.getKioskIcon(ks, this));
|
||||
kioskId++;
|
||||
}
|
||||
|
||||
drawerItems.getMenu()
|
||||
@@ -390,24 +435,24 @@ public class MainActivity extends AppCompatActivity {
|
||||
.setIcon(ThemeHelper.resolveResourceIdFromAttr(this, R.attr.ic_channel));
|
||||
drawerItems.getMenu()
|
||||
.add(R.id.menu_tabs_group, ITEM_ID_FEED, ORDER, R.string.fragment_feed_title)
|
||||
.setIcon(ThemeHelper.resolveResourceIdFromAttr(this, R.attr.rss));
|
||||
.setIcon(ThemeHelper.resolveResourceIdFromAttr(this, R.attr.ic_rss));
|
||||
drawerItems.getMenu()
|
||||
.add(R.id.menu_tabs_group, ITEM_ID_BOOKMARKS, ORDER, R.string.tab_bookmarks)
|
||||
.setIcon(ThemeHelper.resolveResourceIdFromAttr(this, R.attr.ic_bookmark));
|
||||
drawerItems.getMenu()
|
||||
.add(R.id.menu_tabs_group, ITEM_ID_DOWNLOADS, ORDER, R.string.downloads)
|
||||
.setIcon(ThemeHelper.resolveResourceIdFromAttr(this, R.attr.download));
|
||||
.setIcon(ThemeHelper.resolveResourceIdFromAttr(this, R.attr.ic_file_download));
|
||||
drawerItems.getMenu()
|
||||
.add(R.id.menu_tabs_group, ITEM_ID_HISTORY, ORDER, R.string.action_history)
|
||||
.setIcon(ThemeHelper.resolveResourceIdFromAttr(this, R.attr.history));
|
||||
.setIcon(ThemeHelper.resolveResourceIdFromAttr(this, R.attr.ic_history));
|
||||
|
||||
//Settings and About
|
||||
drawerItems.getMenu()
|
||||
.add(R.id.menu_options_about_group, ITEM_ID_SETTINGS, ORDER, R.string.settings)
|
||||
.setIcon(ThemeHelper.resolveResourceIdFromAttr(this, R.attr.settings));
|
||||
.setIcon(ThemeHelper.resolveResourceIdFromAttr(this, R.attr.ic_settings));
|
||||
drawerItems.getMenu()
|
||||
.add(R.id.menu_options_about_group, ITEM_ID_ABOUT, ORDER, R.string.tab_about)
|
||||
.setIcon(ThemeHelper.resolveResourceIdFromAttr(this, R.attr.info));
|
||||
.setIcon(ThemeHelper.resolveResourceIdFromAttr(this, R.attr.ic_info_outline));
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -416,39 +461,52 @@ public class MainActivity extends AppCompatActivity {
|
||||
if (!isChangingConfigurations()) {
|
||||
StateSaver.clearStateFiles();
|
||||
}
|
||||
if (broadcastReceiver != null) {
|
||||
unregisterReceiver(broadcastReceiver);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onResume() {
|
||||
assureCorrectAppLanguage(this);
|
||||
Localization.init(getApplicationContext()); //change the date format to match the selected language on resume
|
||||
// Change the date format to match the selected language on resume
|
||||
Localization.init(getApplicationContext());
|
||||
super.onResume();
|
||||
|
||||
// close drawer on return, and don't show animation, so its looks like the drawer isn't open
|
||||
// when the user returns to MainActivity
|
||||
// Close drawer on return, and don't show animation,
|
||||
// so it looks like the drawer isn't open when the user returns to MainActivity
|
||||
drawer.closeDrawer(GravityCompat.START, false);
|
||||
try {
|
||||
String selectedServiceName = NewPipe.getService(
|
||||
ServiceHelper.getSelectedServiceId(this)).getServiceInfo().getName();
|
||||
final int selectedServiceId = ServiceHelper.getSelectedServiceId(this);
|
||||
final String selectedServiceName = NewPipe.getService(selectedServiceId)
|
||||
.getServiceInfo().getName();
|
||||
headerServiceView.setText(selectedServiceName);
|
||||
headerServiceIcon.setImageResource(ServiceHelper.getIcon(selectedServiceId));
|
||||
|
||||
headerServiceView.post(() -> headerServiceView.setSelected(true));
|
||||
toggleServiceButton.setContentDescription(
|
||||
getString(R.string.drawer_header_description) + selectedServiceName);
|
||||
} catch (Exception e) {
|
||||
} catch (final Exception e) {
|
||||
ErrorActivity.reportUiError(this, e);
|
||||
}
|
||||
|
||||
SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this);
|
||||
final SharedPreferences sharedPreferences
|
||||
= PreferenceManager.getDefaultSharedPreferences(this);
|
||||
if (sharedPreferences.getBoolean(Constants.KEY_THEME_CHANGE, false)) {
|
||||
if (DEBUG) Log.d(TAG, "Theme has changed, recreating activity...");
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "Theme has changed, recreating activity...");
|
||||
}
|
||||
sharedPreferences.edit().putBoolean(Constants.KEY_THEME_CHANGE, false).apply();
|
||||
// https://stackoverflow.com/questions/10844112/runtimeexception-performing-pause-of-activity-that-is-not-resumed
|
||||
// Briefly, let the activity resume properly posting the recreate call to end of the message queue
|
||||
// https://stackoverflow.com/questions/10844112/
|
||||
// Briefly, let the activity resume
|
||||
// properly posting the recreate call to end of the message queue
|
||||
new Handler(Looper.getMainLooper()).post(MainActivity.this::recreate);
|
||||
}
|
||||
|
||||
if (sharedPreferences.getBoolean(Constants.KEY_MAIN_PAGE_CHANGE, false)) {
|
||||
if (DEBUG) Log.d(TAG, "main page has changed, recreating main fragment...");
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "main page has changed, recreating main fragment...");
|
||||
}
|
||||
sharedPreferences.edit().putBoolean(Constants.KEY_MAIN_PAGE_CHANGE, false).apply();
|
||||
NavigationHelper.openMainActivity(this);
|
||||
}
|
||||
@@ -459,13 +517,18 @@ public class MainActivity extends AppCompatActivity {
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onNewIntent(Intent intent) {
|
||||
if (DEBUG) Log.d(TAG, "onNewIntent() called with: intent = [" + intent + "]");
|
||||
protected void onNewIntent(final Intent intent) {
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "onNewIntent() called with: intent = [" + intent + "]");
|
||||
}
|
||||
if (intent != null) {
|
||||
// Return if launched from a launcher (e.g. Nova Launcher, Pixel Launcher ...)
|
||||
// to not destroy the already created backstack
|
||||
String action = intent.getAction();
|
||||
if ((action != null && action.equals(Intent.ACTION_MAIN)) && intent.hasCategory(Intent.CATEGORY_LAUNCHER)) return;
|
||||
final String action = intent.getAction();
|
||||
if ((action != null && action.equals(Intent.ACTION_MAIN))
|
||||
&& intent.hasCategory(Intent.CATEGORY_LAUNCHER)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
super.onNewIntent(intent);
|
||||
@@ -474,25 +537,76 @@ public class MainActivity extends AppCompatActivity {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBackPressed() {
|
||||
if (DEBUG) Log.d(TAG, "onBackPressed() called");
|
||||
|
||||
Fragment fragment = getSupportFragmentManager().findFragmentById(R.id.fragment_holder);
|
||||
// If current fragment implements BackPressable (i.e. can/wanna handle back press) delegate the back press to it
|
||||
if (fragment instanceof BackPressable) {
|
||||
if (((BackPressable) fragment).onBackPressed()) return;
|
||||
public boolean onKeyDown(final int keyCode, final KeyEvent event) {
|
||||
final Fragment fragment = getSupportFragmentManager()
|
||||
.findFragmentById(R.id.fragment_player_holder);
|
||||
if (fragment instanceof OnKeyDownListener
|
||||
&& !bottomSheetHiddenOrCollapsed()) {
|
||||
// Provide keyDown event to fragment which then sends this event
|
||||
// to the main player service
|
||||
return ((OnKeyDownListener) fragment).onKeyDown(keyCode)
|
||||
|| super.onKeyDown(keyCode, event);
|
||||
}
|
||||
|
||||
|
||||
if (getSupportFragmentManager().getBackStackEntryCount() == 1) {
|
||||
finish();
|
||||
} else super.onBackPressed();
|
||||
return super.onKeyDown(keyCode, event);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
|
||||
for (int i: grantResults){
|
||||
if (i == PackageManager.PERMISSION_DENIED){
|
||||
public void onBackPressed() {
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "onBackPressed() called");
|
||||
}
|
||||
|
||||
if (DeviceUtils.isTv(this)) {
|
||||
final View drawerPanel = findViewById(R.id.navigation);
|
||||
if (drawer.isDrawerOpen(drawerPanel)) {
|
||||
drawer.closeDrawers();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// In case bottomSheet is not visible on the screen or collapsed we can assume that the user
|
||||
// interacts with a fragment inside fragment_holder so all back presses should be
|
||||
// handled by it
|
||||
if (bottomSheetHiddenOrCollapsed()) {
|
||||
final Fragment fragment = getSupportFragmentManager()
|
||||
.findFragmentById(R.id.fragment_holder);
|
||||
// If current fragment implements BackPressable (i.e. can/wanna handle back press)
|
||||
// delegate the back press to it
|
||||
if (fragment instanceof BackPressable) {
|
||||
if (((BackPressable) fragment).onBackPressed()) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
} else {
|
||||
final Fragment fragmentPlayer = getSupportFragmentManager()
|
||||
.findFragmentById(R.id.fragment_player_holder);
|
||||
// If current fragment implements BackPressable (i.e. can/wanna handle back press)
|
||||
// delegate the back press to it
|
||||
if (fragmentPlayer instanceof BackPressable) {
|
||||
if (!((BackPressable) fragmentPlayer).onBackPressed()) {
|
||||
final FrameLayout bottomSheetLayout =
|
||||
findViewById(R.id.fragment_player_holder);
|
||||
BottomSheetBehavior.from(bottomSheetLayout)
|
||||
.setState(BottomSheetBehavior.STATE_COLLAPSED);
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (getSupportFragmentManager().getBackStackEntryCount() == 1) {
|
||||
finish();
|
||||
} else {
|
||||
super.onBackPressed();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRequestPermissionsResult(final int requestCode,
|
||||
@NonNull final String[] permissions,
|
||||
@NonNull final int[] grantResults) {
|
||||
for (final int i : grantResults) {
|
||||
if (i == PackageManager.PERMISSION_DENIED) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -501,7 +615,8 @@ public class MainActivity extends AppCompatActivity {
|
||||
NavigationHelper.openDownloads(this);
|
||||
break;
|
||||
case PermissionHelper.DOWNLOAD_DIALOG_REQUEST_CODE:
|
||||
Fragment fragment = getSupportFragmentManager().findFragmentById(R.id.fragment_holder);
|
||||
final Fragment fragment = getSupportFragmentManager()
|
||||
.findFragmentById(R.id.fragment_player_holder);
|
||||
if (fragment instanceof VideoDetailFragment) {
|
||||
((VideoDetailFragment) fragment).openDownloadDialog();
|
||||
}
|
||||
@@ -546,21 +661,20 @@ public class MainActivity extends AppCompatActivity {
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
@Override
|
||||
public boolean onCreateOptionsMenu(Menu menu) {
|
||||
if (DEBUG) Log.d(TAG, "onCreateOptionsMenu() called with: menu = [" + menu + "]");
|
||||
public boolean onCreateOptionsMenu(final Menu menu) {
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "onCreateOptionsMenu() called with: menu = [" + menu + "]");
|
||||
}
|
||||
super.onCreateOptionsMenu(menu);
|
||||
|
||||
Fragment fragment = getSupportFragmentManager().findFragmentById(R.id.fragment_holder);
|
||||
if (!(fragment instanceof VideoDetailFragment)) {
|
||||
findViewById(R.id.toolbar).findViewById(R.id.toolbar_spinner).setVisibility(View.GONE);
|
||||
}
|
||||
|
||||
final Fragment fragment
|
||||
= getSupportFragmentManager().findFragmentById(R.id.fragment_holder);
|
||||
if (!(fragment instanceof SearchFragment)) {
|
||||
findViewById(R.id.toolbar).findViewById(R.id.toolbar_search_container).setVisibility(View.GONE);
|
||||
|
||||
findViewById(R.id.toolbar).findViewById(R.id.toolbar_search_container)
|
||||
.setVisibility(View.GONE);
|
||||
}
|
||||
|
||||
ActionBar actionBar = getSupportActionBar();
|
||||
final ActionBar actionBar = getSupportActionBar();
|
||||
if (actionBar != null) {
|
||||
actionBar.setDisplayHomeAsUpEnabled(false);
|
||||
}
|
||||
@@ -571,9 +685,11 @@ public class MainActivity extends AppCompatActivity {
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(MenuItem item) {
|
||||
if (DEBUG) Log.d(TAG, "onOptionsItemSelected() called with: item = [" + item + "]");
|
||||
int id = item.getItemId();
|
||||
public boolean onOptionsItemSelected(final MenuItem item) {
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "onOptionsItemSelected() called with: item = [" + item + "]");
|
||||
}
|
||||
final int id = item.getItemId();
|
||||
|
||||
switch (id) {
|
||||
case android.R.id.home:
|
||||
@@ -589,11 +705,22 @@ public class MainActivity extends AppCompatActivity {
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
private void initFragments() {
|
||||
if (DEBUG) Log.d(TAG, "initFragments() called");
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "initFragments() called");
|
||||
}
|
||||
StateSaver.clearStateFiles();
|
||||
if (getIntent() != null && getIntent().hasExtra(Constants.KEY_LINK_TYPE)) {
|
||||
// When user watch a video inside popup and then tries to open the video in main player
|
||||
// while the app is closed he will see a blank fragment on place of kiosk.
|
||||
// Let's open it first
|
||||
if (getSupportFragmentManager().getBackStackEntryCount() == 0) {
|
||||
NavigationHelper.openMainFragment(getSupportFragmentManager());
|
||||
}
|
||||
|
||||
handleIntent(getIntent());
|
||||
} else NavigationHelper.gotoMainFragment(getSupportFragmentManager());
|
||||
} else {
|
||||
NavigationHelper.gotoMainFragment(getSupportFragmentManager());
|
||||
}
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
@@ -601,12 +728,14 @@ public class MainActivity extends AppCompatActivity {
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
private void updateDrawerNavigation() {
|
||||
if (getSupportActionBar() == null) return;
|
||||
if (getSupportActionBar() == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
final Toolbar toolbar = findViewById(R.id.toolbar);
|
||||
final DrawerLayout drawer = findViewById(R.id.drawer_layout);
|
||||
|
||||
final Fragment fragment = getSupportFragmentManager().findFragmentById(R.id.fragment_holder);
|
||||
final Fragment fragment = getSupportFragmentManager()
|
||||
.findFragmentById(R.id.fragment_holder);
|
||||
if (fragment instanceof MainFragment) {
|
||||
getSupportActionBar().setDisplayHomeAsUpEnabled(false);
|
||||
if (toggle != null) {
|
||||
@@ -621,44 +750,53 @@ public class MainActivity extends AppCompatActivity {
|
||||
}
|
||||
}
|
||||
|
||||
private void updateDrawerHeaderString(String content) {
|
||||
NavigationView navigationView = findViewById(R.id.navigation);
|
||||
View hView = navigationView.getHeaderView(0);
|
||||
Button action = hView.findViewById(R.id.drawer_header_action_button);
|
||||
|
||||
action.setContentDescription(content);
|
||||
}
|
||||
|
||||
private void handleIntent(Intent intent) {
|
||||
private void handleIntent(final Intent intent) {
|
||||
try {
|
||||
if (DEBUG) Log.d(TAG, "handleIntent() called with: intent = [" + intent + "]");
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "handleIntent() called with: intent = [" + intent + "]");
|
||||
}
|
||||
|
||||
if (intent.hasExtra(Constants.KEY_LINK_TYPE)) {
|
||||
String url = intent.getStringExtra(Constants.KEY_URL);
|
||||
int serviceId = intent.getIntExtra(Constants.KEY_SERVICE_ID, 0);
|
||||
final String url = intent.getStringExtra(Constants.KEY_URL);
|
||||
final int serviceId = intent.getIntExtra(Constants.KEY_SERVICE_ID, 0);
|
||||
String title = intent.getStringExtra(Constants.KEY_TITLE);
|
||||
switch (((StreamingService.LinkType) intent.getSerializableExtra(Constants.KEY_LINK_TYPE))) {
|
||||
if (title == null) {
|
||||
title = "";
|
||||
}
|
||||
|
||||
final StreamingService.LinkType linkType = ((StreamingService.LinkType) intent
|
||||
.getSerializableExtra(Constants.KEY_LINK_TYPE));
|
||||
assert linkType != null;
|
||||
switch (linkType) {
|
||||
case STREAM:
|
||||
boolean autoPlay = intent.getBooleanExtra(VideoDetailFragment.AUTO_PLAY, false);
|
||||
NavigationHelper.openVideoDetailFragment(getSupportFragmentManager(), serviceId, url, title, autoPlay);
|
||||
final String intentCacheKey = intent.getStringExtra(
|
||||
VideoPlayer.PLAY_QUEUE_KEY);
|
||||
final PlayQueue playQueue = intentCacheKey != null
|
||||
? SerializedCache.getInstance()
|
||||
.take(intentCacheKey, PlayQueue.class)
|
||||
: null;
|
||||
|
||||
final boolean switchingPlayers = intent.getBooleanExtra(
|
||||
VideoDetailFragment.KEY_SWITCHING_PLAYERS, false);
|
||||
NavigationHelper.openVideoDetailFragment(
|
||||
getApplicationContext(), getSupportFragmentManager(),
|
||||
serviceId, url, title, playQueue, switchingPlayers);
|
||||
break;
|
||||
case CHANNEL:
|
||||
NavigationHelper.openChannelFragment(getSupportFragmentManager(),
|
||||
serviceId,
|
||||
url,
|
||||
title);
|
||||
serviceId, url, title);
|
||||
break;
|
||||
case PLAYLIST:
|
||||
NavigationHelper.openPlaylistFragment(getSupportFragmentManager(),
|
||||
serviceId,
|
||||
url,
|
||||
title);
|
||||
serviceId, url, title);
|
||||
break;
|
||||
}
|
||||
} else if (intent.hasExtra(Constants.KEY_OPEN_SEARCH)) {
|
||||
String searchString = intent.getStringExtra(Constants.KEY_SEARCH_STRING);
|
||||
if (searchString == null) searchString = "";
|
||||
int serviceId = intent.getIntExtra(Constants.KEY_SERVICE_ID, 0);
|
||||
if (searchString == null) {
|
||||
searchString = "";
|
||||
}
|
||||
final int serviceId = intent.getIntExtra(Constants.KEY_SERVICE_ID, 0);
|
||||
NavigationHelper.openSearchFragment(
|
||||
getSupportFragmentManager(),
|
||||
serviceId,
|
||||
@@ -667,8 +805,61 @@ public class MainActivity extends AppCompatActivity {
|
||||
} else {
|
||||
NavigationHelper.gotoMainFragment(getSupportFragmentManager());
|
||||
}
|
||||
} catch (Exception e) {
|
||||
} catch (final Exception e) {
|
||||
ErrorActivity.reportUiError(this, e);
|
||||
}
|
||||
}
|
||||
|
||||
private void openMiniPlayerIfMissing() {
|
||||
final Fragment fragmentPlayer = getSupportFragmentManager()
|
||||
.findFragmentById(R.id.fragment_player_holder);
|
||||
if (fragmentPlayer == null) {
|
||||
// We still don't have a fragment attached to the activity. It can happen when a user
|
||||
// started popup or background players without opening a stream inside the fragment.
|
||||
// Adding it in a collapsed state (only mini player will be visible).
|
||||
NavigationHelper.showMiniPlayer(getSupportFragmentManager());
|
||||
}
|
||||
}
|
||||
|
||||
private void openMiniPlayerUponPlayerStarted() {
|
||||
if (getIntent().getSerializableExtra(Constants.KEY_LINK_TYPE)
|
||||
== StreamingService.LinkType.STREAM) {
|
||||
// handleIntent() already takes care of opening video detail fragment
|
||||
// due to an intent containing a STREAM link
|
||||
return;
|
||||
}
|
||||
|
||||
if (PlayerHolder.isPlayerOpen()) {
|
||||
// if the player is already open, no need for a broadcast receiver
|
||||
openMiniPlayerIfMissing();
|
||||
} else {
|
||||
// listen for player start intent being sent around
|
||||
broadcastReceiver = new BroadcastReceiver() {
|
||||
@Override
|
||||
public void onReceive(final Context context, final Intent intent) {
|
||||
if (Objects.equals(intent.getAction(),
|
||||
VideoDetailFragment.ACTION_PLAYER_STARTED)) {
|
||||
openMiniPlayerIfMissing();
|
||||
// At this point the player is added 100%, we can unregister. Other actions
|
||||
// are useless since the fragment will not be removed after that.
|
||||
unregisterReceiver(broadcastReceiver);
|
||||
broadcastReceiver = null;
|
||||
}
|
||||
}
|
||||
};
|
||||
final IntentFilter intentFilter = new IntentFilter();
|
||||
intentFilter.addAction(VideoDetailFragment.ACTION_PLAYER_STARTED);
|
||||
registerReceiver(broadcastReceiver, intentFilter);
|
||||
}
|
||||
}
|
||||
|
||||
private boolean bottomSheetHiddenOrCollapsed() {
|
||||
final FrameLayout bottomSheetLayout = findViewById(R.id.fragment_player_holder);
|
||||
final BottomSheetBehavior<FrameLayout> bottomSheetBehavior =
|
||||
BottomSheetBehavior.from(bottomSheetLayout);
|
||||
|
||||
final int sheetState = bottomSheetBehavior.getState();
|
||||
return sheetState == BottomSheetBehavior.STATE_HIDDEN
|
||||
|| sheetState == BottomSheetBehavior.STATE_COLLAPSED;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,14 +13,13 @@ import static org.schabi.newpipe.database.Migrations.MIGRATION_1_2;
|
||||
import static org.schabi.newpipe.database.Migrations.MIGRATION_2_3;
|
||||
|
||||
public final class NewPipeDatabase {
|
||||
|
||||
private static volatile AppDatabase databaseInstance;
|
||||
|
||||
private NewPipeDatabase() {
|
||||
//no instance
|
||||
}
|
||||
|
||||
private static AppDatabase getDatabase(Context context) {
|
||||
private static AppDatabase getDatabase(final Context context) {
|
||||
return Room
|
||||
.databaseBuilder(context.getApplicationContext(), AppDatabase.class, DATABASE_NAME)
|
||||
.addMigrations(MIGRATION_1_2, MIGRATION_2_3)
|
||||
@@ -28,13 +27,14 @@ public final class NewPipeDatabase {
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public static AppDatabase getInstance(@NonNull Context context) {
|
||||
public static AppDatabase getInstance(@NonNull final Context context) {
|
||||
AppDatabase result = databaseInstance;
|
||||
if (result == null) {
|
||||
synchronized (NewPipeDatabase.class) {
|
||||
result = databaseInstance;
|
||||
if (result == null) {
|
||||
databaseInstance = (result = getDatabase(context));
|
||||
databaseInstance = getDatabase(context);
|
||||
result = databaseInstance;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -46,7 +46,7 @@ public final class NewPipeDatabase {
|
||||
if (databaseInstance == null) {
|
||||
throw new IllegalStateException("database is not initialized");
|
||||
}
|
||||
Cursor c = databaseInstance.query("pragma wal_checkpoint(full)", null);
|
||||
final Cursor c = databaseInstance.query("pragma wal_checkpoint(full)", null);
|
||||
if (c.moveToFirst() && c.getInt(0) == 1) {
|
||||
throw new RuntimeException("Checkpoint was blocked from completing");
|
||||
}
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
|
||||
package org.schabi.newpipe;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
@@ -26,21 +25,22 @@ import android.os.Bundle;
|
||||
*/
|
||||
|
||||
public class PanicResponderActivity extends Activity {
|
||||
|
||||
public static final String PANIC_TRIGGER_ACTION = "info.guardianproject.panic.action.TRIGGER";
|
||||
|
||||
@SuppressLint("NewApi")
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
protected void onCreate(final Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
Intent intent = getIntent();
|
||||
final Intent intent = getIntent();
|
||||
if (intent != null && PANIC_TRIGGER_ACTION.equals(intent.getAction())) {
|
||||
// TODO explicitly clear the search results once they are restored when the app restarts
|
||||
// or if the app reloads the current video after being killed, that should be cleared also
|
||||
// TODO: Explicitly clear the search results
|
||||
// once they are restored when the app restarts
|
||||
// or if the app reloads the current video after being killed,
|
||||
// that should be cleared also
|
||||
ExitActivity.exitAndRemoveFromRecentApps(this);
|
||||
}
|
||||
|
||||
if (Build.VERSION.SDK_INT >= 21) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
||||
finishAndRemoveTask();
|
||||
} else {
|
||||
finish();
|
||||
|
||||
@@ -1,24 +1,31 @@
|
||||
package org.schabi.newpipe;
|
||||
|
||||
import android.content.Intent;
|
||||
import android.content.SharedPreferences;
|
||||
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.WebResourceRequest;
|
||||
import android.webkit.WebSettings;
|
||||
import android.webkit.WebView;
|
||||
import android.webkit.WebViewClient;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.RequiresApi;
|
||||
import androidx.appcompat.app.ActionBar;
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
import androidx.appcompat.widget.Toolbar;
|
||||
import androidx.core.app.NavUtils;
|
||||
import androidx.preference.PreferenceManager;
|
||||
|
||||
import org.schabi.newpipe.util.ThemeHelper;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import java.io.UnsupportedEncodingException;
|
||||
import java.net.URLDecoder;
|
||||
|
||||
/*
|
||||
* Created by beneth <bmauduit@beneth.fr> on 06.12.16.
|
||||
@@ -44,16 +51,17 @@ public class ReCaptchaActivity extends AppCompatActivity {
|
||||
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";
|
||||
public static final String RECAPTCHA_COOKIES_KEY = "recaptcha_cookies";
|
||||
|
||||
private WebView webView;
|
||||
private String foundCookies = "";
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
protected void onCreate(final Bundle savedInstanceState) {
|
||||
ThemeHelper.setTheme(this);
|
||||
super.onCreate(savedInstanceState);
|
||||
setContentView(R.layout.activity_recaptcha);
|
||||
Toolbar toolbar = findViewById(R.id.toolbar);
|
||||
final Toolbar toolbar = findViewById(R.id.toolbar);
|
||||
setSupportActionBar(toolbar);
|
||||
|
||||
String url = getIntent().getStringExtra(RECAPTCHA_URL_EXTRA);
|
||||
@@ -68,23 +76,47 @@ public class ReCaptchaActivity extends AppCompatActivity {
|
||||
webView = findViewById(R.id.reCaptchaWebView);
|
||||
|
||||
// enable Javascript
|
||||
WebSettings webSettings = webView.getSettings();
|
||||
final WebSettings webSettings = webView.getSettings();
|
||||
webSettings.setJavaScriptEnabled(true);
|
||||
|
||||
webView.setWebViewClient(new WebViewClient() {
|
||||
@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
|
||||
@Override
|
||||
public void onPageFinished(WebView view, String url) {
|
||||
public boolean shouldOverrideUrlLoading(final WebView view,
|
||||
final WebResourceRequest request) {
|
||||
final String url = request.getUrl().toString();
|
||||
if (MainActivity.DEBUG) {
|
||||
Log.d(TAG, "shouldOverrideUrlLoading: request.url=" + url);
|
||||
}
|
||||
|
||||
handleCookiesFromUrl(url);
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean shouldOverrideUrlLoading(final WebView view, final String url) {
|
||||
if (MainActivity.DEBUG) {
|
||||
Log.d(TAG, "shouldOverrideUrlLoading: url=" + url);
|
||||
}
|
||||
|
||||
handleCookiesFromUrl(url);
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPageFinished(final WebView view, final String url) {
|
||||
super.onPageFinished(view, url);
|
||||
handleCookies(url);
|
||||
handleCookiesFromUrl(url);
|
||||
}
|
||||
});
|
||||
|
||||
// cleaning cache, history and cookies from webView
|
||||
webView.clearCache(true);
|
||||
webView.clearHistory();
|
||||
android.webkit.CookieManager cookieManager = CookieManager.getInstance();
|
||||
final android.webkit.CookieManager cookieManager = CookieManager.getInstance();
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
||||
cookieManager.removeAllCookies(aBoolean -> {});
|
||||
cookieManager.removeAllCookies(aBoolean -> {
|
||||
});
|
||||
} else {
|
||||
cookieManager.removeAllCookie();
|
||||
}
|
||||
@@ -93,10 +125,10 @@ public class ReCaptchaActivity extends AppCompatActivity {
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onCreateOptionsMenu(Menu menu) {
|
||||
public boolean onCreateOptionsMenu(final Menu menu) {
|
||||
getMenuInflater().inflate(R.menu.menu_recaptcha, menu);
|
||||
|
||||
ActionBar actionBar = getSupportActionBar();
|
||||
final ActionBar actionBar = getSupportActionBar();
|
||||
if (actionBar != null) {
|
||||
actionBar.setDisplayHomeAsUpEnabled(false);
|
||||
actionBar.setTitle(R.string.title_activity_recaptcha);
|
||||
@@ -112,8 +144,8 @@ public class ReCaptchaActivity extends AppCompatActivity {
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(MenuItem item) {
|
||||
int id = item.getItemId();
|
||||
public boolean onOptionsItemSelected(final MenuItem item) {
|
||||
final int id = item.getItemId();
|
||||
switch (id) {
|
||||
case R.id.menu_item_done:
|
||||
saveCookiesAndFinish();
|
||||
@@ -124,37 +156,83 @@ public class ReCaptchaActivity extends AppCompatActivity {
|
||||
}
|
||||
|
||||
private void saveCookiesAndFinish() {
|
||||
handleCookies(webView.getUrl()); // try to get cookies of unclosed page
|
||||
handleCookiesFromUrl(webView.getUrl()); // try to get cookies of unclosed page
|
||||
if (MainActivity.DEBUG) {
|
||||
Log.d(TAG, "saveCookiesAndFinish: foundCookies=" + foundCookies);
|
||||
}
|
||||
|
||||
if (!foundCookies.isEmpty()) {
|
||||
// save cookies to preferences
|
||||
final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(
|
||||
getApplicationContext());
|
||||
final String key = getApplicationContext().getString(R.string.recaptcha_cookies_key);
|
||||
prefs.edit().putString(key, foundCookies).apply();
|
||||
|
||||
// give cookies to Downloader class
|
||||
DownloaderImpl.getInstance().setCookies(foundCookies);
|
||||
DownloaderImpl.getInstance().setCookie(RECAPTCHA_COOKIES_KEY, foundCookies);
|
||||
setResult(RESULT_OK);
|
||||
}
|
||||
|
||||
Intent intent = new Intent(this, org.schabi.newpipe.MainActivity.class);
|
||||
final Intent intent = new Intent(this, org.schabi.newpipe.MainActivity.class);
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
|
||||
NavUtils.navigateUpTo(this, intent);
|
||||
}
|
||||
|
||||
|
||||
private void handleCookiesFromUrl(@Nullable final String url) {
|
||||
if (MainActivity.DEBUG) {
|
||||
Log.d(TAG, "handleCookiesFromUrl: url=" + (url == null ? "null" : url));
|
||||
}
|
||||
|
||||
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;
|
||||
if (url == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
addYoutubeCookies(cookies);
|
||||
// add other methods to extract cookies here
|
||||
final String cookies = CookieManager.getInstance().getCookie(url);
|
||||
handleCookies(cookies);
|
||||
|
||||
// sometimes cookies are inside the url
|
||||
final int abuseStart = url.indexOf("google_abuse=");
|
||||
if (abuseStart != -1) {
|
||||
final int abuseEnd = url.indexOf("+path");
|
||||
|
||||
try {
|
||||
String abuseCookie = url.substring(abuseStart + 13, abuseEnd);
|
||||
abuseCookie = URLDecoder.decode(abuseCookie, "UTF-8");
|
||||
handleCookies(abuseCookie);
|
||||
} catch (UnsupportedEncodingException | StringIndexOutOfBoundsException e) {
|
||||
if (MainActivity.DEBUG) {
|
||||
e.printStackTrace();
|
||||
Log.d(TAG, "handleCookiesFromUrl: invalid google abuse starting at "
|
||||
+ abuseStart + " and ending at " + abuseEnd + " for url " + url);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void addYoutubeCookies(@NonNull String cookies) {
|
||||
if (cookies.contains("s_gl=") || cookies.contains("goojf=") || cookies.contains("VISITOR_INFO1_LIVE=")) {
|
||||
private void handleCookies(@Nullable final String cookies) {
|
||||
if (MainActivity.DEBUG) {
|
||||
Log.d(TAG, "handleCookies: cookies=" + (cookies == null ? "null" : cookies));
|
||||
}
|
||||
|
||||
if (cookies == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
addYoutubeCookies(cookies);
|
||||
// add here methods to extract cookies for other services
|
||||
}
|
||||
|
||||
private void addYoutubeCookies(@NonNull final String cookies) {
|
||||
if (cookies.contains("s_gl=") || cookies.contains("goojf=")
|
||||
|| cookies.contains("VISITOR_INFO1_LIVE=")
|
||||
|| cookies.contains("GOOGLE_ABUSE_EXEMPTION=")) {
|
||||
// youtube seems to also need the other cookies:
|
||||
addCookie(cookies);
|
||||
}
|
||||
}
|
||||
|
||||
private void addCookie(String cookie) {
|
||||
private void addCookie(final String cookie) {
|
||||
if (foundCookies.contains(cookie)) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -8,7 +8,6 @@ import android.content.Intent;
|
||||
import android.content.SharedPreferences;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.os.Bundle;
|
||||
import android.preference.PreferenceManager;
|
||||
import android.text.TextUtils;
|
||||
import android.view.ContextThemeWrapper;
|
||||
import android.view.LayoutInflater;
|
||||
@@ -25,8 +24,11 @@ import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
import androidx.appcompat.content.res.AppCompatResources;
|
||||
import androidx.core.app.NotificationCompat;
|
||||
import androidx.core.widget.TextViewCompat;
|
||||
import androidx.fragment.app.FragmentManager;
|
||||
import androidx.preference.PreferenceManager;
|
||||
|
||||
import org.schabi.newpipe.download.DownloadDialog;
|
||||
import org.schabi.newpipe.extractor.Info;
|
||||
@@ -38,18 +40,24 @@ import org.schabi.newpipe.extractor.exceptions.ExtractionException;
|
||||
import org.schabi.newpipe.extractor.playlist.PlaylistInfo;
|
||||
import org.schabi.newpipe.extractor.stream.StreamInfo;
|
||||
import org.schabi.newpipe.extractor.stream.VideoStream;
|
||||
import org.schabi.newpipe.player.MainPlayer;
|
||||
import org.schabi.newpipe.player.helper.PlayerHelper;
|
||||
import org.schabi.newpipe.player.helper.PlayerHolder;
|
||||
import org.schabi.newpipe.player.playqueue.ChannelPlayQueue;
|
||||
import org.schabi.newpipe.player.playqueue.PlayQueue;
|
||||
import org.schabi.newpipe.player.playqueue.PlaylistPlayQueue;
|
||||
import org.schabi.newpipe.player.playqueue.SinglePlayQueue;
|
||||
import org.schabi.newpipe.report.UserAction;
|
||||
import org.schabi.newpipe.util.Constants;
|
||||
import org.schabi.newpipe.util.DeviceUtils;
|
||||
import org.schabi.newpipe.util.ExtractorHelper;
|
||||
import org.schabi.newpipe.util.ListHelper;
|
||||
import org.schabi.newpipe.util.NavigationHelper;
|
||||
import org.schabi.newpipe.util.PermissionHelper;
|
||||
import org.schabi.newpipe.util.ShareUtils;
|
||||
import org.schabi.newpipe.util.ThemeHelper;
|
||||
import org.schabi.newpipe.util.urlfinder.UrlFinder;
|
||||
import org.schabi.newpipe.views.FocusOverlayView;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.ArrayList;
|
||||
@@ -58,42 +66,44 @@ import java.util.List;
|
||||
|
||||
import icepick.Icepick;
|
||||
import icepick.State;
|
||||
import io.reactivex.Observable;
|
||||
import io.reactivex.Single;
|
||||
import io.reactivex.android.schedulers.AndroidSchedulers;
|
||||
import io.reactivex.disposables.CompositeDisposable;
|
||||
import io.reactivex.disposables.Disposable;
|
||||
import io.reactivex.functions.Consumer;
|
||||
import io.reactivex.schedulers.Schedulers;
|
||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
|
||||
import io.reactivex.rxjava3.core.Observable;
|
||||
import io.reactivex.rxjava3.core.Single;
|
||||
import io.reactivex.rxjava3.disposables.CompositeDisposable;
|
||||
import io.reactivex.rxjava3.disposables.Disposable;
|
||||
import io.reactivex.rxjava3.functions.Consumer;
|
||||
import io.reactivex.rxjava3.schedulers.Schedulers;
|
||||
|
||||
import static org.schabi.newpipe.extractor.StreamingService.ServiceInfo.MediaCapability.AUDIO;
|
||||
import static org.schabi.newpipe.extractor.StreamingService.ServiceInfo.MediaCapability.VIDEO;
|
||||
import static org.schabi.newpipe.util.ThemeHelper.resolveResourceIdFromAttr;
|
||||
|
||||
/**
|
||||
* Get the url from the intent and open it in the chosen preferred player
|
||||
* Get the url from the intent and open it in the chosen preferred player.
|
||||
*/
|
||||
public class RouterActivity extends AppCompatActivity {
|
||||
|
||||
public static final String INTERNAL_ROUTE_KEY = "internalRoute";
|
||||
/**
|
||||
* Removes invisible separators (\p{Z}) and punctuation characters including
|
||||
* brackets (\p{P}). See http://www.regular-expressions.info/unicode.html for
|
||||
* more details.
|
||||
*/
|
||||
private static final String REGEX_REMOVE_FROM_URL = "[\\p{Z}\\p{P}]";
|
||||
protected final CompositeDisposable disposables = new CompositeDisposable();
|
||||
@State
|
||||
protected int currentServiceId = -1;
|
||||
private StreamingService currentService;
|
||||
@State
|
||||
protected LinkType currentLinkType;
|
||||
@State
|
||||
protected int selectedRadioPosition = -1;
|
||||
protected int selectedPreviously = -1;
|
||||
|
||||
protected String currentUrl;
|
||||
protected boolean internalRoute = false;
|
||||
protected final CompositeDisposable disposables = new CompositeDisposable();
|
||||
|
||||
private StreamingService currentService;
|
||||
private boolean selectionIsDownload = false;
|
||||
|
||||
public static final String internalRouteKey = "internalRoute";
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
protected void onCreate(final Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
Icepick.restoreInstanceState(this, savedInstanceState);
|
||||
|
||||
@@ -106,14 +116,12 @@ public class RouterActivity extends AppCompatActivity {
|
||||
}
|
||||
}
|
||||
|
||||
internalRoute = getIntent().getBooleanExtra(internalRouteKey, false);
|
||||
|
||||
setTheme(ThemeHelper.isLightThemeSelected(this)
|
||||
? R.style.RouterActivityThemeLight : R.style.RouterActivityThemeDark);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onSaveInstanceState(Bundle outState) {
|
||||
protected void onSaveInstanceState(final Bundle outState) {
|
||||
super.onSaveInstanceState(outState);
|
||||
Icepick.saveInstanceState(this, outState);
|
||||
}
|
||||
@@ -132,7 +140,7 @@ public class RouterActivity extends AppCompatActivity {
|
||||
disposables.clear();
|
||||
}
|
||||
|
||||
private void handleUrl(String url) {
|
||||
private void handleUrl(final String url) {
|
||||
disposables.add(Observable
|
||||
.fromCallable(() -> {
|
||||
if (currentServiceId == -1) {
|
||||
@@ -152,31 +160,44 @@ public class RouterActivity extends AppCompatActivity {
|
||||
if (result) {
|
||||
onSuccess();
|
||||
} else {
|
||||
onError();
|
||||
showUnsupportedUrlDialog(url);
|
||||
}
|
||||
}, this::handleError));
|
||||
}, throwable -> handleError(throwable, url)));
|
||||
}
|
||||
|
||||
private void handleError(Throwable error) {
|
||||
error.printStackTrace();
|
||||
private void handleError(final Throwable throwable, final String url) {
|
||||
throwable.printStackTrace();
|
||||
|
||||
if (error instanceof ExtractionException) {
|
||||
Toast.makeText(this, R.string.url_not_supported_toast, Toast.LENGTH_LONG).show();
|
||||
if (throwable instanceof ExtractionException) {
|
||||
showUnsupportedUrlDialog(url);
|
||||
} else {
|
||||
ExtractorHelper.handleGeneralException(this, -1, null, error, UserAction.SOMETHING_ELSE, null);
|
||||
ExtractorHelper.handleGeneralException(this, -1, url, throwable,
|
||||
UserAction.SOMETHING_ELSE, null);
|
||||
finish();
|
||||
}
|
||||
|
||||
finish();
|
||||
}
|
||||
|
||||
private void onError() {
|
||||
Toast.makeText(this, R.string.url_not_supported_toast, Toast.LENGTH_LONG).show();
|
||||
finish();
|
||||
private void showUnsupportedUrlDialog(final String url) {
|
||||
final Context context = getThemeWrapperContext();
|
||||
new AlertDialog.Builder(context)
|
||||
.setTitle(R.string.unsupported_url)
|
||||
.setMessage(R.string.unsupported_url_dialog_message)
|
||||
.setIcon(ThemeHelper.resolveResourceIdFromAttr(context, R.attr.ic_share))
|
||||
.setPositiveButton(R.string.open_in_browser,
|
||||
(dialog, which) -> ShareUtils.openUrlInBrowser(this, url))
|
||||
.setNegativeButton(R.string.share,
|
||||
(dialog, which) -> ShareUtils.shareUrl(this, "", url)) // no subject
|
||||
.setNeutralButton(R.string.cancel, null)
|
||||
.setOnDismissListener(dialog -> finish())
|
||||
.show();
|
||||
}
|
||||
|
||||
protected void onSuccess() {
|
||||
final SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(this);
|
||||
final String selectedChoiceKey = preferences.getString(getString(R.string.preferred_open_action_key), getString(R.string.preferred_open_action_default));
|
||||
final SharedPreferences preferences = PreferenceManager
|
||||
.getDefaultSharedPreferences(this);
|
||||
final String selectedChoiceKey = preferences
|
||||
.getString(getString(R.string.preferred_open_action_key),
|
||||
getString(R.string.preferred_open_action_default));
|
||||
|
||||
final String showInfoKey = getString(R.string.show_info_key);
|
||||
final String videoPlayerKey = getString(R.string.video_player_key);
|
||||
@@ -186,7 +207,8 @@ public class RouterActivity extends AppCompatActivity {
|
||||
final String alwaysAskKey = getString(R.string.always_ask_open_action_key);
|
||||
|
||||
if (selectedChoiceKey.equals(alwaysAskKey)) {
|
||||
final List<AdapterChoiceItem> choices = getChoicesForService(currentService, currentLinkType);
|
||||
final List<AdapterChoiceItem> choices
|
||||
= getChoicesForService(currentService, currentLinkType);
|
||||
|
||||
switch (choices.size()) {
|
||||
case 1:
|
||||
@@ -204,20 +226,26 @@ public class RouterActivity extends AppCompatActivity {
|
||||
} else if (selectedChoiceKey.equals(downloadKey)) {
|
||||
handleChoice(downloadKey);
|
||||
} else {
|
||||
final boolean isExtVideoEnabled = preferences.getBoolean(getString(R.string.use_external_video_player_key), false);
|
||||
final boolean isExtAudioEnabled = preferences.getBoolean(getString(R.string.use_external_audio_player_key), false);
|
||||
final boolean isVideoPlayerSelected = selectedChoiceKey.equals(videoPlayerKey) || selectedChoiceKey.equals(popupPlayerKey);
|
||||
final boolean isExtVideoEnabled = preferences.getBoolean(
|
||||
getString(R.string.use_external_video_player_key), false);
|
||||
final boolean isExtAudioEnabled = preferences.getBoolean(
|
||||
getString(R.string.use_external_audio_player_key), false);
|
||||
final boolean isVideoPlayerSelected = selectedChoiceKey.equals(videoPlayerKey)
|
||||
|| selectedChoiceKey.equals(popupPlayerKey);
|
||||
final boolean isAudioPlayerSelected = selectedChoiceKey.equals(backgroundPlayerKey);
|
||||
|
||||
if (currentLinkType != LinkType.STREAM) {
|
||||
if (isExtAudioEnabled && isAudioPlayerSelected || isExtVideoEnabled && isVideoPlayerSelected) {
|
||||
Toast.makeText(this, R.string.external_player_unsupported_link_type, Toast.LENGTH_LONG).show();
|
||||
if (isExtAudioEnabled && isAudioPlayerSelected
|
||||
|| isExtVideoEnabled && isVideoPlayerSelected) {
|
||||
Toast.makeText(this, R.string.external_player_unsupported_link_type,
|
||||
Toast.LENGTH_LONG).show();
|
||||
handleChoice(showInfoKey);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
final List<StreamingService.ServiceInfo.MediaCapability> capabilities = currentService.getServiceInfo().getMediaCapabilities();
|
||||
final List<StreamingService.ServiceInfo.MediaCapability> capabilities
|
||||
= currentService.getServiceInfo().getMediaCapabilities();
|
||||
|
||||
boolean serviceSupportsChoice = false;
|
||||
if (isVideoPlayerSelected) {
|
||||
@@ -239,7 +267,8 @@ public class RouterActivity extends AppCompatActivity {
|
||||
final Context themeWrapperContext = getThemeWrapperContext();
|
||||
|
||||
final LayoutInflater inflater = LayoutInflater.from(themeWrapperContext);
|
||||
final LinearLayout rootLayout = (LinearLayout) inflater.inflate(R.layout.preferred_player_dialog_view, null, false);
|
||||
final LinearLayout rootLayout = (LinearLayout) inflater.inflate(
|
||||
R.layout.single_choice_dialog_view, null, false);
|
||||
final RadioGroup radioGroup = rootLayout.findViewById(android.R.id.list);
|
||||
|
||||
final DialogInterface.OnClickListener dialogButtonsClickListener = (dialog, which) -> {
|
||||
@@ -249,8 +278,11 @@ public class RouterActivity extends AppCompatActivity {
|
||||
|
||||
handleChoice(choice.key);
|
||||
|
||||
// open future streams always like this one, because "always" button was used by user
|
||||
if (which == DialogInterface.BUTTON_POSITIVE) {
|
||||
preferences.edit().putString(getString(R.string.preferred_open_action_key), choice.key).apply();
|
||||
preferences.edit()
|
||||
.putString(getString(R.string.preferred_open_action_key), choice.key)
|
||||
.apply();
|
||||
}
|
||||
};
|
||||
|
||||
@@ -261,7 +293,9 @@ public class RouterActivity extends AppCompatActivity {
|
||||
.setNegativeButton(R.string.just_once, dialogButtonsClickListener)
|
||||
.setPositiveButton(R.string.always, dialogButtonsClickListener)
|
||||
.setOnDismissListener((dialog) -> {
|
||||
if (!selectionIsDownload) finish();
|
||||
if (!selectionIsDownload) {
|
||||
finish();
|
||||
}
|
||||
})
|
||||
.create();
|
||||
|
||||
@@ -270,10 +304,13 @@ public class RouterActivity extends AppCompatActivity {
|
||||
setDialogButtonsState(alertDialog, radioGroup.getCheckedRadioButtonId() != -1);
|
||||
});
|
||||
|
||||
radioGroup.setOnCheckedChangeListener((group, checkedId) -> setDialogButtonsState(alertDialog, true));
|
||||
radioGroup.setOnCheckedChangeListener((group, checkedId) ->
|
||||
setDialogButtonsState(alertDialog, true));
|
||||
final View.OnClickListener radioButtonsClickListener = v -> {
|
||||
final int indexOfChild = radioGroup.indexOfChild(v);
|
||||
if (indexOfChild == -1) return;
|
||||
if (indexOfChild == -1) {
|
||||
return;
|
||||
}
|
||||
|
||||
selectedPreviously = selectedRadioPosition;
|
||||
selectedRadioPosition = indexOfChild;
|
||||
@@ -284,22 +321,27 @@ public class RouterActivity extends AppCompatActivity {
|
||||
};
|
||||
|
||||
int id = 12345;
|
||||
for (AdapterChoiceItem item : choices) {
|
||||
final RadioButton radioButton = (RadioButton) inflater.inflate(R.layout.list_radio_icon_item, null);
|
||||
for (final AdapterChoiceItem item : choices) {
|
||||
final RadioButton radioButton
|
||||
= (RadioButton) inflater.inflate(R.layout.list_radio_icon_item, null);
|
||||
radioButton.setText(item.description);
|
||||
radioButton.setCompoundDrawablesWithIntrinsicBounds(item.icon, 0, 0, 0);
|
||||
TextViewCompat.setCompoundDrawablesRelativeWithIntrinsicBounds(radioButton,
|
||||
AppCompatResources.getDrawable(getApplicationContext(), item.icon),
|
||||
null, null, null);
|
||||
radioButton.setChecked(false);
|
||||
radioButton.setId(id++);
|
||||
radioButton.setLayoutParams(new RadioGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT));
|
||||
radioButton.setLayoutParams(new RadioGroup.LayoutParams(
|
||||
ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT));
|
||||
radioButton.setOnClickListener(radioButtonsClickListener);
|
||||
radioGroup.addView(radioButton);
|
||||
}
|
||||
|
||||
if (selectedRadioPosition == -1) {
|
||||
final String lastSelectedPlayer = preferences.getString(getString(R.string.preferred_open_action_last_selected_key), null);
|
||||
final String lastSelectedPlayer = preferences.getString(
|
||||
getString(R.string.preferred_open_action_last_selected_key), null);
|
||||
if (!TextUtils.isEmpty(lastSelectedPlayer)) {
|
||||
for (int i = 0; i < choices.size(); i++) {
|
||||
AdapterChoiceItem c = choices.get(i);
|
||||
final AdapterChoiceItem c = choices.get(i);
|
||||
if (lastSelectedPlayer.equals(c.key)) {
|
||||
selectedRadioPosition = i;
|
||||
break;
|
||||
@@ -315,78 +357,133 @@ public class RouterActivity extends AppCompatActivity {
|
||||
selectedPreviously = selectedRadioPosition;
|
||||
|
||||
alertDialog.show();
|
||||
|
||||
if (DeviceUtils.isTv(this)) {
|
||||
FocusOverlayView.setupFocusObserver(alertDialog);
|
||||
}
|
||||
}
|
||||
|
||||
private List<AdapterChoiceItem> getChoicesForService(StreamingService service, LinkType linkType) {
|
||||
private List<AdapterChoiceItem> getChoicesForService(final StreamingService service,
|
||||
final LinkType linkType) {
|
||||
final Context context = getThemeWrapperContext();
|
||||
|
||||
final List<AdapterChoiceItem> returnList = new ArrayList<>();
|
||||
final List<StreamingService.ServiceInfo.MediaCapability> capabilities = service.getServiceInfo().getMediaCapabilities();
|
||||
final List<StreamingService.ServiceInfo.MediaCapability> capabilities
|
||||
= service.getServiceInfo().getMediaCapabilities();
|
||||
|
||||
final SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(this);
|
||||
boolean isExtVideoEnabled = preferences.getBoolean(getString(R.string.use_external_video_player_key), false);
|
||||
boolean isExtAudioEnabled = preferences.getBoolean(getString(R.string.use_external_audio_player_key), false);
|
||||
final SharedPreferences preferences = PreferenceManager
|
||||
.getDefaultSharedPreferences(this);
|
||||
final boolean isExtVideoEnabled = preferences.getBoolean(
|
||||
getString(R.string.use_external_video_player_key), false);
|
||||
final boolean isExtAudioEnabled = preferences.getBoolean(
|
||||
getString(R.string.use_external_audio_player_key), false);
|
||||
|
||||
returnList.add(new AdapterChoiceItem(getString(R.string.show_info_key), getString(R.string.show_info),
|
||||
resolveResourceIdFromAttr(context, R.attr.info)));
|
||||
final AdapterChoiceItem videoPlayer = new AdapterChoiceItem(
|
||||
getString(R.string.video_player_key), getString(R.string.video_player),
|
||||
resolveResourceIdFromAttr(context, R.attr.ic_play_arrow));
|
||||
final AdapterChoiceItem showInfo = new AdapterChoiceItem(
|
||||
getString(R.string.show_info_key), getString(R.string.show_info),
|
||||
resolveResourceIdFromAttr(context, R.attr.ic_info_outline));
|
||||
final AdapterChoiceItem popupPlayer = new AdapterChoiceItem(
|
||||
getString(R.string.popup_player_key), getString(R.string.popup_player),
|
||||
resolveResourceIdFromAttr(context, R.attr.ic_popup));
|
||||
final AdapterChoiceItem backgroundPlayer = new AdapterChoiceItem(
|
||||
getString(R.string.background_player_key), getString(R.string.background_player),
|
||||
resolveResourceIdFromAttr(context, R.attr.ic_headset));
|
||||
|
||||
if (capabilities.contains(VIDEO) && !(isExtVideoEnabled && linkType != LinkType.STREAM)) {
|
||||
returnList.add(new AdapterChoiceItem(getString(R.string.video_player_key), getString(R.string.video_player),
|
||||
resolveResourceIdFromAttr(context, R.attr.play)));
|
||||
returnList.add(new AdapterChoiceItem(getString(R.string.popup_player_key), getString(R.string.popup_player),
|
||||
resolveResourceIdFromAttr(context, R.attr.popup)));
|
||||
if (linkType == LinkType.STREAM) {
|
||||
if (isExtVideoEnabled) {
|
||||
// show both "show info" and "video player", they are two different activities
|
||||
returnList.add(showInfo);
|
||||
returnList.add(videoPlayer);
|
||||
} else {
|
||||
final MainPlayer.PlayerType playerType = PlayerHolder.getType();
|
||||
if (capabilities.contains(VIDEO)
|
||||
&& PlayerHelper.isAutoplayAllowedByUser(context)
|
||||
&& playerType == null || playerType == MainPlayer.PlayerType.VIDEO) {
|
||||
// show only "video player" since the details activity will be opened and the
|
||||
// video will be auto played there. Since "show info" would do the exact same
|
||||
// thing, use that as a key to let VideoDetailFragment load the stream instead
|
||||
// of using FetcherService (see comment in handleChoice())
|
||||
returnList.add(new AdapterChoiceItem(
|
||||
showInfo.key, videoPlayer.description, videoPlayer.icon));
|
||||
} else {
|
||||
// show only "show info" if video player is not applicable, auto play is
|
||||
// disabled or a video is playing in a player different than the main one
|
||||
returnList.add(showInfo);
|
||||
}
|
||||
}
|
||||
|
||||
if (capabilities.contains(VIDEO)) {
|
||||
returnList.add(popupPlayer);
|
||||
}
|
||||
if (capabilities.contains(AUDIO)) {
|
||||
returnList.add(backgroundPlayer);
|
||||
}
|
||||
|
||||
} else {
|
||||
returnList.add(showInfo);
|
||||
if (capabilities.contains(VIDEO) && !isExtVideoEnabled) {
|
||||
returnList.add(videoPlayer);
|
||||
returnList.add(popupPlayer);
|
||||
}
|
||||
if (capabilities.contains(AUDIO) && !isExtAudioEnabled) {
|
||||
returnList.add(backgroundPlayer);
|
||||
}
|
||||
}
|
||||
|
||||
if (capabilities.contains(AUDIO) && !(isExtAudioEnabled && linkType != LinkType.STREAM)) {
|
||||
returnList.add(new AdapterChoiceItem(getString(R.string.background_player_key), getString(R.string.background_player),
|
||||
resolveResourceIdFromAttr(context, R.attr.audio)));
|
||||
}
|
||||
|
||||
returnList.add(new AdapterChoiceItem(getString(R.string.download_key), getString(R.string.download),
|
||||
resolveResourceIdFromAttr(context, R.attr.download)));
|
||||
returnList.add(new AdapterChoiceItem(getString(R.string.download_key),
|
||||
getString(R.string.download),
|
||||
resolveResourceIdFromAttr(context, R.attr.ic_file_download)));
|
||||
|
||||
return returnList;
|
||||
}
|
||||
|
||||
private Context getThemeWrapperContext() {
|
||||
return new ContextThemeWrapper(this,
|
||||
ThemeHelper.isLightThemeSelected(this) ? R.style.LightTheme : R.style.DarkTheme);
|
||||
return new ContextThemeWrapper(this, ThemeHelper.isLightThemeSelected(this)
|
||||
? R.style.LightTheme : R.style.DarkTheme);
|
||||
}
|
||||
|
||||
private void setDialogButtonsState(AlertDialog dialog, boolean state) {
|
||||
private void setDialogButtonsState(final AlertDialog dialog, final boolean state) {
|
||||
final Button negativeButton = dialog.getButton(DialogInterface.BUTTON_NEGATIVE);
|
||||
final Button positiveButton = dialog.getButton(DialogInterface.BUTTON_POSITIVE);
|
||||
if (negativeButton == null || positiveButton == null) return;
|
||||
if (negativeButton == null || positiveButton == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
negativeButton.setEnabled(state);
|
||||
positiveButton.setEnabled(state);
|
||||
}
|
||||
|
||||
private void handleText() {
|
||||
String searchString = getIntent().getStringExtra(Intent.EXTRA_TEXT);
|
||||
int serviceId = getIntent().getIntExtra(Constants.KEY_SERVICE_ID, 0);
|
||||
Intent intent = new Intent(getThemeWrapperContext(), MainActivity.class);
|
||||
final String searchString = getIntent().getStringExtra(Intent.EXTRA_TEXT);
|
||||
final int serviceId = getIntent().getIntExtra(Constants.KEY_SERVICE_ID, 0);
|
||||
final Intent intent = new Intent(getThemeWrapperContext(), MainActivity.class);
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||
startActivity(intent);
|
||||
NavigationHelper.openSearch(getThemeWrapperContext(), serviceId, searchString);
|
||||
}
|
||||
|
||||
private void handleChoice(final String selectedChoiceKey) {
|
||||
final List<String> validChoicesList = Arrays.asList(getResources().getStringArray(R.array.preferred_open_action_values_list));
|
||||
final List<String> validChoicesList = Arrays.asList(getResources()
|
||||
.getStringArray(R.array.preferred_open_action_values_list));
|
||||
if (validChoicesList.contains(selectedChoiceKey)) {
|
||||
PreferenceManager.getDefaultSharedPreferences(this).edit()
|
||||
.putString(getString(R.string.preferred_open_action_last_selected_key), selectedChoiceKey)
|
||||
.putString(getString(
|
||||
R.string.preferred_open_action_last_selected_key), selectedChoiceKey)
|
||||
.apply();
|
||||
}
|
||||
|
||||
if (selectedChoiceKey.equals(getString(R.string.popup_player_key)) && !PermissionHelper.isPopupEnabled(this)) {
|
||||
if (selectedChoiceKey.equals(getString(R.string.popup_player_key))
|
||||
&& !PermissionHelper.isPopupEnabled(this)) {
|
||||
PermissionHelper.showPopupEnablementToast(this);
|
||||
finish();
|
||||
return;
|
||||
}
|
||||
|
||||
if (selectedChoiceKey.equals(getString(R.string.download_key))) {
|
||||
if (PermissionHelper.checkStoragePermissions(this, PermissionHelper.DOWNLOAD_DIALOG_REQUEST_CODE)) {
|
||||
if (PermissionHelper.checkStoragePermissions(this,
|
||||
PermissionHelper.DOWNLOAD_DIALOG_REQUEST_CODE)) {
|
||||
selectionIsDownload = true;
|
||||
openDownloadDialog();
|
||||
}
|
||||
@@ -401,20 +498,16 @@ public class RouterActivity extends AppCompatActivity {
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(intent -> {
|
||||
if (!internalRoute) {
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK);
|
||||
}
|
||||
startActivity(intent);
|
||||
|
||||
finish();
|
||||
}, this::handleError)
|
||||
}, throwable -> handleError(throwable, currentUrl))
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
final Intent intent = new Intent(this, FetcherService.class);
|
||||
final Choice choice = new Choice(currentService.getServiceId(), currentLinkType, currentUrl, selectedChoiceKey);
|
||||
final Choice choice = new Choice(currentService.getServiceId(), currentLinkType,
|
||||
currentUrl, selectedChoiceKey);
|
||||
intent.putExtra(FetcherService.KEY_CHOICE, choice);
|
||||
startService(intent);
|
||||
|
||||
@@ -423,35 +516,33 @@ public class RouterActivity extends AppCompatActivity {
|
||||
|
||||
@SuppressLint("CheckResult")
|
||||
private void openDownloadDialog() {
|
||||
ExtractorHelper.getStreamInfo(currentServiceId, currentUrl, true)
|
||||
disposables.add(ExtractorHelper.getStreamInfo(currentServiceId, currentUrl, true)
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe((@NonNull StreamInfo result) -> {
|
||||
List<VideoStream> sortedVideoStreams = ListHelper.getSortedStreamVideosList(this,
|
||||
result.getVideoStreams(),
|
||||
result.getVideoOnlyStreams(),
|
||||
false);
|
||||
int selectedVideoStreamIndex = ListHelper.getDefaultResolutionIndex(this,
|
||||
sortedVideoStreams);
|
||||
.subscribe(result -> {
|
||||
final List<VideoStream> sortedVideoStreams = ListHelper
|
||||
.getSortedStreamVideosList(this, result.getVideoStreams(),
|
||||
result.getVideoOnlyStreams(), false);
|
||||
final int selectedVideoStreamIndex = ListHelper
|
||||
.getDefaultResolutionIndex(this, sortedVideoStreams);
|
||||
|
||||
FragmentManager fm = getSupportFragmentManager();
|
||||
DownloadDialog downloadDialog = DownloadDialog.newInstance(result);
|
||||
final FragmentManager fm = getSupportFragmentManager();
|
||||
final DownloadDialog downloadDialog = DownloadDialog.newInstance(result);
|
||||
downloadDialog.setVideoStreams(sortedVideoStreams);
|
||||
downloadDialog.setAudioStreams(result.getAudioStreams());
|
||||
downloadDialog.setSelectedVideoStream(selectedVideoStreamIndex);
|
||||
downloadDialog.show(fm, "downloadDialog");
|
||||
fm.executePendingTransactions();
|
||||
downloadDialog.getDialog().setOnDismissListener(dialog -> {
|
||||
finish();
|
||||
});
|
||||
}, (@NonNull Throwable throwable) -> {
|
||||
onError();
|
||||
});
|
||||
downloadDialog.requireDialog().setOnDismissListener(dialog -> finish());
|
||||
}, throwable ->
|
||||
showUnsupportedUrlDialog(currentUrl)));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
|
||||
for (int i : grantResults) {
|
||||
public void onRequestPermissionsResult(final int requestCode,
|
||||
@NonNull final String[] permissions,
|
||||
@NonNull final int[] grantResults) {
|
||||
for (final int i : grantResults) {
|
||||
if (i == PackageManager.PERMISSION_DENIED) {
|
||||
finish();
|
||||
return;
|
||||
@@ -463,11 +554,12 @@ public class RouterActivity extends AppCompatActivity {
|
||||
}
|
||||
|
||||
private static class AdapterChoiceItem {
|
||||
final String description, key;
|
||||
final String description;
|
||||
final String key;
|
||||
@DrawableRes
|
||||
final int icon;
|
||||
|
||||
AdapterChoiceItem(String key, String description, int icon) {
|
||||
AdapterChoiceItem(final String key, final String description, final int icon) {
|
||||
this.description = description;
|
||||
this.key = key;
|
||||
this.icon = icon;
|
||||
@@ -476,10 +568,12 @@ public class RouterActivity extends AppCompatActivity {
|
||||
|
||||
private static class Choice implements Serializable {
|
||||
final int serviceId;
|
||||
final String url, playerChoice;
|
||||
final String url;
|
||||
final String playerChoice;
|
||||
final LinkType linkType;
|
||||
|
||||
Choice(int serviceId, LinkType linkType, String url, String playerChoice) {
|
||||
Choice(final int serviceId, final LinkType linkType,
|
||||
final String url, final String playerChoice) {
|
||||
this.serviceId = serviceId;
|
||||
this.linkType = linkType;
|
||||
this.url = url;
|
||||
@@ -492,14 +586,10 @@ public class RouterActivity extends AppCompatActivity {
|
||||
}
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Service Fetcher
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
public static class FetcherService extends IntentService {
|
||||
|
||||
private static final int ID = 456;
|
||||
public static final String KEY_CHOICE = "key_choice";
|
||||
private static final int ID = 456;
|
||||
private Disposable fetcher;
|
||||
|
||||
public FetcherService() {
|
||||
@@ -513,16 +603,20 @@ public class RouterActivity extends AppCompatActivity {
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onHandleIntent(@Nullable Intent intent) {
|
||||
if (intent == null) return;
|
||||
protected void onHandleIntent(@Nullable final Intent intent) {
|
||||
if (intent == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
final Serializable serializable = intent.getSerializableExtra(KEY_CHOICE);
|
||||
if (!(serializable instanceof Choice)) return;
|
||||
Choice playerChoice = (Choice) serializable;
|
||||
if (!(serializable instanceof Choice)) {
|
||||
return;
|
||||
}
|
||||
final Choice playerChoice = (Choice) serializable;
|
||||
handleChoice(playerChoice);
|
||||
}
|
||||
|
||||
public void handleChoice(Choice choice) {
|
||||
public void handleChoice(final Choice choice) {
|
||||
Single<? extends Info> single = null;
|
||||
UserAction userAction = UserAction.SOMETHING_ELSE;
|
||||
|
||||
@@ -549,56 +643,52 @@ public class RouterActivity extends AppCompatActivity {
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(info -> {
|
||||
resultHandler.accept(info);
|
||||
if (fetcher != null) fetcher.dispose();
|
||||
if (fetcher != null) {
|
||||
fetcher.dispose();
|
||||
}
|
||||
}, throwable -> ExtractorHelper.handleGeneralException(this,
|
||||
choice.serviceId, choice.url, throwable, finalUserAction, ", opened with " + choice.playerChoice));
|
||||
choice.serviceId, choice.url, throwable, finalUserAction,
|
||||
", opened with " + choice.playerChoice));
|
||||
}
|
||||
}
|
||||
|
||||
public Consumer<Info> getResultHandler(Choice choice) {
|
||||
public Consumer<Info> getResultHandler(final Choice choice) {
|
||||
return info -> {
|
||||
final String videoPlayerKey = getString(R.string.video_player_key);
|
||||
final String backgroundPlayerKey = getString(R.string.background_player_key);
|
||||
final String popupPlayerKey = getString(R.string.popup_player_key);
|
||||
|
||||
final SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(this);
|
||||
boolean isExtVideoEnabled = preferences.getBoolean(getString(R.string.use_external_video_player_key), false);
|
||||
boolean isExtAudioEnabled = preferences.getBoolean(getString(R.string.use_external_audio_player_key), false);
|
||||
;
|
||||
|
||||
PlayQueue playQueue;
|
||||
String playerChoice = choice.playerChoice;
|
||||
final SharedPreferences preferences = PreferenceManager
|
||||
.getDefaultSharedPreferences(this);
|
||||
final boolean isExtVideoEnabled = preferences.getBoolean(
|
||||
getString(R.string.use_external_video_player_key), false);
|
||||
final boolean isExtAudioEnabled = preferences.getBoolean(
|
||||
getString(R.string.use_external_audio_player_key), false);
|
||||
|
||||
final PlayQueue playQueue;
|
||||
if (info instanceof StreamInfo) {
|
||||
if (playerChoice.equals(backgroundPlayerKey) && isExtAudioEnabled) {
|
||||
if (choice.playerChoice.equals(backgroundPlayerKey) && isExtAudioEnabled) {
|
||||
NavigationHelper.playOnExternalAudioPlayer(this, (StreamInfo) info);
|
||||
|
||||
} else if (playerChoice.equals(videoPlayerKey) && isExtVideoEnabled) {
|
||||
return;
|
||||
} else if (choice.playerChoice.equals(videoPlayerKey) && isExtVideoEnabled) {
|
||||
NavigationHelper.playOnExternalVideoPlayer(this, (StreamInfo) info);
|
||||
|
||||
} else {
|
||||
playQueue = new SinglePlayQueue((StreamInfo) info);
|
||||
|
||||
if (playerChoice.equals(videoPlayerKey)) {
|
||||
NavigationHelper.playOnMainPlayer(this, playQueue, true);
|
||||
} else if (playerChoice.equals(backgroundPlayerKey)) {
|
||||
NavigationHelper.enqueueOnBackgroundPlayer(this, playQueue, true);
|
||||
} else if (playerChoice.equals(popupPlayerKey)) {
|
||||
NavigationHelper.enqueueOnPopupPlayer(this, playQueue, true);
|
||||
}
|
||||
return;
|
||||
}
|
||||
playQueue = new SinglePlayQueue((StreamInfo) info);
|
||||
} else if (info instanceof ChannelInfo) {
|
||||
playQueue = new ChannelPlayQueue((ChannelInfo) info);
|
||||
} else if (info instanceof PlaylistInfo) {
|
||||
playQueue = new PlaylistPlayQueue((PlaylistInfo) info);
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
|
||||
if (info instanceof ChannelInfo || info instanceof PlaylistInfo) {
|
||||
playQueue = info instanceof ChannelInfo ? new ChannelPlayQueue((ChannelInfo) info) : new PlaylistPlayQueue((PlaylistInfo) info);
|
||||
|
||||
if (playerChoice.equals(videoPlayerKey)) {
|
||||
NavigationHelper.playOnMainPlayer(this, playQueue, true);
|
||||
} else if (playerChoice.equals(backgroundPlayerKey)) {
|
||||
NavigationHelper.playOnBackgroundPlayer(this, playQueue, true);
|
||||
} else if (playerChoice.equals(popupPlayerKey)) {
|
||||
NavigationHelper.playOnPopupPlayer(this, playQueue, true);
|
||||
}
|
||||
if (choice.playerChoice.equals(videoPlayerKey)) {
|
||||
NavigationHelper.playOnMainPlayer(this, playQueue, false);
|
||||
} else if (choice.playerChoice.equals(backgroundPlayerKey)) {
|
||||
NavigationHelper.playOnBackgroundPlayer(this, playQueue, true);
|
||||
} else if (choice.playerChoice.equals(popupPlayerKey)) {
|
||||
NavigationHelper.playOnPopupPlayer(this, playQueue, true);
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -607,7 +697,9 @@ public class RouterActivity extends AppCompatActivity {
|
||||
public void onDestroy() {
|
||||
super.onDestroy();
|
||||
stopForeground(true);
|
||||
if (fetcher != null) fetcher.dispose();
|
||||
if (fetcher != null) {
|
||||
fetcher.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
private NotificationCompat.Builder createNotification() {
|
||||
@@ -615,8 +707,10 @@ public class RouterActivity extends AppCompatActivity {
|
||||
.setOngoing(true)
|
||||
.setSmallIcon(R.drawable.ic_newpipe_triangle_white)
|
||||
.setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
|
||||
.setContentTitle(getString(R.string.preferred_player_fetcher_notification_title))
|
||||
.setContentText(getString(R.string.preferred_player_fetcher_notification_message));
|
||||
.setContentTitle(
|
||||
getString(R.string.preferred_player_fetcher_notification_title))
|
||||
.setContentText(
|
||||
getString(R.string.preferred_player_fetcher_notification_message));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -625,7 +719,7 @@ public class RouterActivity extends AppCompatActivity {
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
@Nullable
|
||||
private String getUrl(Intent intent) {
|
||||
private String getUrl(final Intent intent) {
|
||||
String foundUrl = null;
|
||||
if (intent.getData() != null) {
|
||||
// Called from another app
|
||||
|
||||
@@ -1,71 +1,87 @@
|
||||
package org.schabi.newpipe.about;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
import com.google.android.material.tabs.TabLayout;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.fragment.app.FragmentManager;
|
||||
import androidx.fragment.app.FragmentPagerAdapter;
|
||||
import androidx.fragment.app.FragmentStatePagerAdapter;
|
||||
import androidx.viewpager.widget.PagerAdapter;
|
||||
import androidx.viewpager.widget.ViewPager;
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
import androidx.appcompat.widget.Toolbar;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
import androidx.appcompat.widget.Toolbar;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.fragment.app.FragmentActivity;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
import androidx.viewpager2.adapter.FragmentStateAdapter;
|
||||
import androidx.viewpager2.widget.ViewPager2;
|
||||
|
||||
import com.google.android.material.tabs.TabLayout;
|
||||
import com.google.android.material.tabs.TabLayoutMediator;
|
||||
|
||||
import org.schabi.newpipe.BuildConfig;
|
||||
import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.util.ThemeHelper;
|
||||
|
||||
import static org.schabi.newpipe.util.Localization.assureCorrectAppLanguage;
|
||||
import static org.schabi.newpipe.util.ShareUtils.openUrlInBrowser;
|
||||
|
||||
public class AboutActivity extends AppCompatActivity {
|
||||
|
||||
/**
|
||||
* List of all software components
|
||||
* List of all software components.
|
||||
*/
|
||||
private static final SoftwareComponent[] SOFTWARE_COMPONENTS = new SoftwareComponent[]{
|
||||
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 - 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 - 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),
|
||||
new SoftwareComponent("Groupie", "2016", "Lisa Wray", "https://github.com/lisawray/groupie", StandardLicenses.MIT)
|
||||
private static final SoftwareComponent[] SOFTWARE_COMPONENTS = {
|
||||
new SoftwareComponent("Giga Get", "2014 - 2015", "Peter Cai",
|
||||
"https://github.com/PaperAirplane-Dev-Team/GigaGet", StandardLicenses.GPL3),
|
||||
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 - 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 - 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),
|
||||
new SoftwareComponent("Groupie", "2016", "Lisa Wray",
|
||||
"https://github.com/lisawray/groupie", StandardLicenses.MIT)
|
||||
};
|
||||
|
||||
private static final int POS_ABOUT = 0;
|
||||
private static final int POS_LICENSE = 1;
|
||||
private static final int TOTAL_COUNT = 2;
|
||||
/**
|
||||
* The {@link PagerAdapter} that will provide
|
||||
* The {@link RecyclerView.Adapter} that will provide
|
||||
* fragments for each of the sections. We use a
|
||||
* {@link FragmentPagerAdapter} derivative, which will keep every
|
||||
* loaded fragment in memory. If this becomes too memory intensive, it
|
||||
* may be best to switch to a
|
||||
* {@link FragmentStatePagerAdapter}.
|
||||
* {@link FragmentStateAdapter} derivative, which will keep every
|
||||
* loaded fragment in memory.
|
||||
*/
|
||||
private SectionsPagerAdapter mSectionsPagerAdapter;
|
||||
|
||||
/**
|
||||
* The {@link ViewPager} that will host the section contents.
|
||||
* The {@link ViewPager2} that will host the section contents.
|
||||
*/
|
||||
private ViewPager mViewPager;
|
||||
private ViewPager2 mViewPager;
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
protected void onCreate(final Bundle savedInstanceState) {
|
||||
assureCorrectAppLanguage(this);
|
||||
super.onCreate(savedInstanceState);
|
||||
ThemeHelper.setTheme(this);
|
||||
@@ -73,26 +89,34 @@ public class AboutActivity extends AppCompatActivity {
|
||||
|
||||
setContentView(R.layout.activity_about);
|
||||
|
||||
Toolbar toolbar = findViewById(R.id.toolbar);
|
||||
final Toolbar toolbar = findViewById(R.id.toolbar);
|
||||
setSupportActionBar(toolbar);
|
||||
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
|
||||
// Create the adapter that will return a fragment for each of the three
|
||||
// primary sections of the activity.
|
||||
mSectionsPagerAdapter = new SectionsPagerAdapter(getSupportFragmentManager());
|
||||
mSectionsPagerAdapter = new SectionsPagerAdapter(this);
|
||||
|
||||
// Set up the ViewPager with the sections adapter.
|
||||
mViewPager = findViewById(R.id.container);
|
||||
mViewPager.setAdapter(mSectionsPagerAdapter);
|
||||
|
||||
TabLayout tabLayout = findViewById(R.id.tabs);
|
||||
tabLayout.setupWithViewPager(mViewPager);
|
||||
final TabLayout tabLayout = findViewById(R.id.tabs);
|
||||
new TabLayoutMediator(tabLayout, mViewPager, (tab, position) -> {
|
||||
switch (position) {
|
||||
default:
|
||||
case POS_ABOUT:
|
||||
tab.setText(R.string.tab_about);
|
||||
break;
|
||||
case POS_LICENSE:
|
||||
tab.setText(R.string.tab_licenses);
|
||||
break;
|
||||
}
|
||||
}).attach();
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(MenuItem item) {
|
||||
|
||||
int id = item.getItemId();
|
||||
public boolean onOptionsItemSelected(final MenuItem item) {
|
||||
final int id = item.getItemId();
|
||||
|
||||
switch (id) {
|
||||
case android.R.id.home:
|
||||
@@ -107,86 +131,72 @@ public class AboutActivity extends AppCompatActivity {
|
||||
* A placeholder fragment containing a simple view.
|
||||
*/
|
||||
public static class AboutFragment extends Fragment {
|
||||
|
||||
public AboutFragment() {
|
||||
}
|
||||
public AboutFragment() { }
|
||||
|
||||
/**
|
||||
* Returns a new instance of this fragment for the given section
|
||||
* number.
|
||||
* Created a new instance of this fragment for the given section number.
|
||||
*
|
||||
* @return New instance of {@link AboutFragment}
|
||||
*/
|
||||
public static AboutFragment newInstance() {
|
||||
return new AboutFragment();
|
||||
}
|
||||
|
||||
@Override
|
||||
public View onCreateView(LayoutInflater inflater, ViewGroup container,
|
||||
Bundle savedInstanceState) {
|
||||
View rootView = inflater.inflate(R.layout.fragment_about, container, false);
|
||||
Context context = this.getContext();
|
||||
public View onCreateView(final LayoutInflater inflater, final ViewGroup container,
|
||||
final Bundle savedInstanceState) {
|
||||
final View rootView = inflater.inflate(R.layout.fragment_about, container, false);
|
||||
final Context context = this.getContext();
|
||||
|
||||
TextView version = rootView.findViewById(R.id.app_version);
|
||||
final TextView version = rootView.findViewById(R.id.app_version);
|
||||
version.setText(BuildConfig.VERSION_NAME);
|
||||
|
||||
View githubLink = rootView.findViewById(R.id.github_link);
|
||||
githubLink.setOnClickListener(nv -> openWebsite(context.getString(R.string.github_url), context));
|
||||
final View githubLink = rootView.findViewById(R.id.github_link);
|
||||
githubLink.setOnClickListener(nv ->
|
||||
openUrlInBrowser(context, context.getString(R.string.github_url)));
|
||||
|
||||
View donationLink = rootView.findViewById(R.id.donation_link);
|
||||
donationLink.setOnClickListener(v -> openWebsite(context.getString(R.string.donation_url), context));
|
||||
final View donationLink = rootView.findViewById(R.id.donation_link);
|
||||
donationLink.setOnClickListener(v ->
|
||||
openUrlInBrowser(context, context.getString(R.string.donation_url)));
|
||||
|
||||
View websiteLink = rootView.findViewById(R.id.website_link);
|
||||
websiteLink.setOnClickListener(nv -> openWebsite(context.getString(R.string.website_url), context));
|
||||
final View websiteLink = rootView.findViewById(R.id.website_link);
|
||||
websiteLink.setOnClickListener(nv ->
|
||||
openUrlInBrowser(context, context.getString(R.string.website_url)));
|
||||
|
||||
View privacyPolicyLink = rootView.findViewById(R.id.privacy_policy_link);
|
||||
privacyPolicyLink.setOnClickListener(v -> openWebsite(context.getString(R.string.privacy_policy_url), context));
|
||||
final View privacyPolicyLink = rootView.findViewById(R.id.privacy_policy_link);
|
||||
privacyPolicyLink.setOnClickListener(v ->
|
||||
openUrlInBrowser(context, context.getString(R.string.privacy_policy_url)));
|
||||
|
||||
return rootView;
|
||||
}
|
||||
|
||||
private void openWebsite(String url, Context context) {
|
||||
Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(url));
|
||||
context.startActivity(intent);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* A {@link FragmentPagerAdapter} that returns a fragment corresponding to
|
||||
* A {@link FragmentStateAdapter} that returns a fragment corresponding to
|
||||
* one of the sections/tabs/pages.
|
||||
*/
|
||||
public class SectionsPagerAdapter extends FragmentPagerAdapter {
|
||||
|
||||
public SectionsPagerAdapter(FragmentManager fm) {
|
||||
super(fm);
|
||||
public static class SectionsPagerAdapter extends FragmentStateAdapter {
|
||||
public SectionsPagerAdapter(final FragmentActivity fa) {
|
||||
super(fa);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public Fragment getItem(int position) {
|
||||
public Fragment createFragment(final int position) {
|
||||
switch (position) {
|
||||
case 0:
|
||||
default:
|
||||
case POS_ABOUT:
|
||||
return AboutFragment.newInstance();
|
||||
case 1:
|
||||
case POS_LICENSE:
|
||||
return LicenseFragment.newInstance(SOFTWARE_COMPONENTS);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getCount() {
|
||||
public int getItemCount() {
|
||||
// Show 2 total pages.
|
||||
return 2;
|
||||
}
|
||||
|
||||
@Override
|
||||
public CharSequence getPageTitle(int position) {
|
||||
switch (position) {
|
||||
case 0:
|
||||
return getString(R.string.tab_about);
|
||||
case 1:
|
||||
return getString(R.string.tab_licenses);
|
||||
}
|
||||
return null;
|
||||
return TOTAL_COUNT;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,73 +0,0 @@
|
||||
package org.schabi.newpipe.about;
|
||||
|
||||
import android.net.Uri;
|
||||
import android.os.Parcel;
|
||||
import android.os.Parcelable;
|
||||
|
||||
/**
|
||||
* A software license
|
||||
*/
|
||||
public class License implements Parcelable {
|
||||
|
||||
public static final Creator<License> CREATOR = new Creator<License>() {
|
||||
@Override
|
||||
public License createFromParcel(Parcel source) {
|
||||
return new License(source);
|
||||
}
|
||||
|
||||
@Override
|
||||
public License[] newArray(int size) {
|
||||
return new License[size];
|
||||
}
|
||||
};
|
||||
private final String abbreviation;
|
||||
private final String name;
|
||||
private String filename;
|
||||
|
||||
public License(String name, String abbreviation, String filename) {
|
||||
if(name == null) throw new NullPointerException("name is null");
|
||||
if(abbreviation == null) throw new NullPointerException("abbreviation is null");
|
||||
if(filename == null) throw new NullPointerException("filename is null");
|
||||
this.name = name;
|
||||
this.filename = filename;
|
||||
this.abbreviation = abbreviation;
|
||||
}
|
||||
|
||||
protected License(Parcel in) {
|
||||
this.filename = in.readString();
|
||||
this.abbreviation = in.readString();
|
||||
this.name = in.readString();
|
||||
}
|
||||
|
||||
public Uri getContentUri() {
|
||||
return new Uri.Builder()
|
||||
.scheme("file")
|
||||
.path("/android_asset")
|
||||
.appendPath(filename)
|
||||
.build();
|
||||
}
|
||||
|
||||
public String getAbbreviation() {
|
||||
return abbreviation;
|
||||
}
|
||||
|
||||
public String getFilename() {
|
||||
return filename;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int describeContents() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeToParcel(Parcel dest, int flags) {
|
||||
dest.writeString(this.filename);
|
||||
dest.writeString(this.abbreviation);
|
||||
dest.writeString(this.name);
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
}
|
||||
19
app/src/main/java/org/schabi/newpipe/about/License.kt
Normal file
19
app/src/main/java/org/schabi/newpipe/about/License.kt
Normal file
@@ -0,0 +1,19 @@
|
||||
package org.schabi.newpipe.about
|
||||
|
||||
import android.net.Uri
|
||||
import android.os.Parcelable
|
||||
import kotlinx.android.parcel.Parcelize
|
||||
import java.io.Serializable
|
||||
|
||||
/**
|
||||
* Class for storing information about a software license.
|
||||
*/
|
||||
@Parcelize
|
||||
class License(val name: String, val abbreviation: String, val filename: String) : Parcelable, Serializable {
|
||||
val contentUri: Uri
|
||||
get() = Uri.Builder()
|
||||
.scheme("file")
|
||||
.path("/android_asset")
|
||||
.appendPath(filename)
|
||||
.build()
|
||||
}
|
||||
@@ -1,133 +1,147 @@
|
||||
package org.schabi.newpipe.about;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
import android.view.ContextMenu;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.MenuInflater;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import android.view.*;
|
||||
import android.widget.TextView;
|
||||
import org.schabi.newpipe.R;
|
||||
|
||||
import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.util.ShareUtils;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.Arrays;
|
||||
import java.util.Comparator;
|
||||
|
||||
import io.reactivex.rxjava3.disposables.CompositeDisposable;
|
||||
|
||||
/**
|
||||
* Fragment containing the software licenses
|
||||
* Fragment containing the software licenses.
|
||||
*/
|
||||
public class LicenseFragment extends Fragment {
|
||||
|
||||
private static final String ARG_COMPONENTS = "components";
|
||||
private SoftwareComponent[] softwareComponents;
|
||||
private SoftwareComponent mComponentForContextMenu;
|
||||
private static final String LICENSE_KEY = "ACTIVE_LICENSE";
|
||||
|
||||
public static LicenseFragment newInstance(SoftwareComponent[] softwareComponents) {
|
||||
if(softwareComponents == null) {
|
||||
private SoftwareComponent[] softwareComponents;
|
||||
private SoftwareComponent componentForContextMenu;
|
||||
private License activeLicense;
|
||||
private final CompositeDisposable compositeDisposable = new CompositeDisposable();
|
||||
|
||||
public static LicenseFragment newInstance(final SoftwareComponent[] softwareComponents) {
|
||||
if (softwareComponents == null) {
|
||||
throw new NullPointerException("softwareComponents is null");
|
||||
}
|
||||
LicenseFragment fragment = new LicenseFragment();
|
||||
Bundle bundle = new Bundle();
|
||||
final LicenseFragment fragment = new LicenseFragment();
|
||||
final Bundle bundle = new Bundle();
|
||||
bundle.putParcelableArray(ARG_COMPONENTS, softwareComponents);
|
||||
fragment.setArguments(bundle);
|
||||
return fragment;
|
||||
}
|
||||
|
||||
/**
|
||||
* Shows a popup containing the license
|
||||
* @param context the context to use
|
||||
* @param license the license to show
|
||||
*/
|
||||
public static void showLicense(Context context, License license) {
|
||||
new LicenseFragmentHelper((Activity) context).execute(license);
|
||||
@Override
|
||||
public void onCreate(@Nullable final Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
softwareComponents = (SoftwareComponent[]) getArguments()
|
||||
.getParcelableArray(ARG_COMPONENTS);
|
||||
|
||||
if (savedInstanceState != null) {
|
||||
final Serializable license = savedInstanceState.getSerializable(LICENSE_KEY);
|
||||
if (license != null) {
|
||||
activeLicense = (License) license;
|
||||
}
|
||||
}
|
||||
// Sort components by name
|
||||
Arrays.sort(softwareComponents, Comparator.comparing(SoftwareComponent::getName));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate(@Nullable Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
softwareComponents = (SoftwareComponent[]) getArguments().getParcelableArray(ARG_COMPONENTS);
|
||||
|
||||
// Sort components by name
|
||||
Arrays.sort(softwareComponents, new Comparator<SoftwareComponent>() {
|
||||
@Override
|
||||
public int compare(SoftwareComponent o1, SoftwareComponent o2) {
|
||||
return o1.getName().compareTo(o2.getName());
|
||||
}
|
||||
});
|
||||
public void onDestroy() {
|
||||
compositeDisposable.dispose();
|
||||
super.onDestroy();
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
|
||||
View rootView = inflater.inflate(R.layout.fragment_licenses, container, false);
|
||||
ViewGroup softwareComponentsView = rootView.findViewById(R.id.software_components);
|
||||
public View onCreateView(final LayoutInflater inflater, @Nullable final ViewGroup container,
|
||||
@Nullable final Bundle savedInstanceState) {
|
||||
final View rootView = inflater.inflate(R.layout.fragment_licenses, container, false);
|
||||
final ViewGroup softwareComponentsView = rootView.findViewById(R.id.software_components);
|
||||
|
||||
View licenseLink = rootView.findViewById(R.id.app_read_license);
|
||||
licenseLink.setOnClickListener(new OnReadFullLicenseClickListener());
|
||||
final View licenseLink = rootView.findViewById(R.id.app_read_license);
|
||||
licenseLink.setOnClickListener(v -> {
|
||||
activeLicense = StandardLicenses.GPL3;
|
||||
compositeDisposable.add(LicenseFragmentHelper.showLicense(getActivity(),
|
||||
StandardLicenses.GPL3));
|
||||
});
|
||||
|
||||
for (final SoftwareComponent component : softwareComponents) {
|
||||
View componentView = inflater.inflate(R.layout.item_software_component, container, false);
|
||||
TextView softwareName = componentView.findViewById(R.id.name);
|
||||
TextView copyright = componentView.findViewById(R.id.copyright);
|
||||
final View componentView = inflater
|
||||
.inflate(R.layout.item_software_component, container, false);
|
||||
final TextView softwareName = componentView.findViewById(R.id.name);
|
||||
final TextView copyright = componentView.findViewById(R.id.copyright);
|
||||
softwareName.setText(component.getName());
|
||||
copyright.setText(getContext().getString(R.string.copyright,
|
||||
copyright.setText(getString(R.string.copyright,
|
||||
component.getYears(),
|
||||
component.getCopyrightOwner(),
|
||||
component.getLicense().getAbbreviation()));
|
||||
|
||||
componentView.setTag(component);
|
||||
componentView.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
Context context = v.getContext();
|
||||
if (context != null) {
|
||||
showLicense(context, component.getLicense());
|
||||
}
|
||||
}
|
||||
componentView.setOnClickListener(v -> {
|
||||
activeLicense = component.getLicense();
|
||||
compositeDisposable.add(LicenseFragmentHelper.showLicense(getActivity(),
|
||||
component.getLicense()));
|
||||
});
|
||||
softwareComponentsView.addView(componentView);
|
||||
registerForContextMenu(componentView);
|
||||
}
|
||||
if (activeLicense != null) {
|
||||
compositeDisposable.add(LicenseFragmentHelper.showLicense(getActivity(),
|
||||
activeLicense));
|
||||
}
|
||||
return rootView;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreateContextMenu(ContextMenu menu, View v, ContextMenu.ContextMenuInfo menuInfo) {
|
||||
MenuInflater inflater = getActivity().getMenuInflater();
|
||||
SoftwareComponent component = (SoftwareComponent) v.getTag();
|
||||
public void onCreateContextMenu(final ContextMenu menu, final View v,
|
||||
final ContextMenu.ContextMenuInfo menuInfo) {
|
||||
final MenuInflater inflater = getActivity().getMenuInflater();
|
||||
final SoftwareComponent component = (SoftwareComponent) v.getTag();
|
||||
menu.setHeaderTitle(component.getName());
|
||||
inflater.inflate(R.menu.software_component, menu);
|
||||
super.onCreateContextMenu(menu, v, menuInfo);
|
||||
mComponentForContextMenu = (SoftwareComponent) v.getTag();
|
||||
componentForContextMenu = (SoftwareComponent) v.getTag();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onContextItemSelected(MenuItem item) {
|
||||
public boolean onContextItemSelected(@NonNull final MenuItem item) {
|
||||
// item.getMenuInfo() is null so we use the tag of the view
|
||||
final SoftwareComponent component = mComponentForContextMenu;
|
||||
final SoftwareComponent component = componentForContextMenu;
|
||||
if (component == null) {
|
||||
return false;
|
||||
}
|
||||
switch (item.getItemId()) {
|
||||
case R.id.action_website:
|
||||
openWebsite(component.getLink());
|
||||
ShareUtils.openUrlInBrowser(getActivity(), component.getLink());
|
||||
return true;
|
||||
case R.id.action_show_license:
|
||||
showLicense(getContext(), component.getLicense());
|
||||
compositeDisposable.add(LicenseFragmentHelper.showLicense(getActivity(),
|
||||
component.getLicense()));
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private void openWebsite(String componentLink) {
|
||||
Intent browserIntent = new Intent(Intent.ACTION_VIEW, Uri.parse(componentLink));
|
||||
startActivity(browserIntent);
|
||||
}
|
||||
|
||||
private static class OnReadFullLicenseClickListener implements View.OnClickListener {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
LicenseFragment.showLicense(v.getContext(), StandardLicenses.GPL3);
|
||||
@Override
|
||||
public void onSaveInstanceState(@NonNull final Bundle savedInstanceState) {
|
||||
super.onSaveInstanceState(savedInstanceState);
|
||||
if (activeLicense != null) {
|
||||
savedInstanceState.putSerializable(LICENSE_KEY, activeLicense);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,136 +1,109 @@
|
||||
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 android.util.Base64;
|
||||
import android.webkit.WebView;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
import android.webkit.WebView;
|
||||
|
||||
import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.util.ThemeHelper;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStreamReader;
|
||||
import java.lang.ref.WeakReference;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
|
||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
|
||||
import io.reactivex.rxjava3.core.Observable;
|
||||
import io.reactivex.rxjava3.disposables.Disposable;
|
||||
import io.reactivex.rxjava3.schedulers.Schedulers;
|
||||
|
||||
import static org.schabi.newpipe.util.Localization.assureCorrectAppLanguage;
|
||||
|
||||
public class LicenseFragmentHelper extends AsyncTask<Object, Void, Integer> {
|
||||
|
||||
final WeakReference<Activity> weakReference;
|
||||
private License license;
|
||||
|
||||
public LicenseFragmentHelper(@Nullable Activity activity) {
|
||||
weakReference = new WeakReference<>(activity);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private Activity getActivity() {
|
||||
Activity activity = weakReference.get();
|
||||
|
||||
if (activity != null && activity.isFinishing()) {
|
||||
return null;
|
||||
} else {
|
||||
return activity;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Integer doInBackground(Object... objects) {
|
||||
license = (License) objects[0];
|
||||
return 1;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPostExecute(Integer result) {
|
||||
Activity activity = getActivity();
|
||||
if (activity == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
String webViewData = getFormattedLicense(activity, license);
|
||||
AlertDialog.Builder alert = new AlertDialog.Builder(activity);
|
||||
alert.setTitle(license.getName());
|
||||
|
||||
WebView wv = new WebView(activity);
|
||||
wv.loadData(webViewData, "text/html; charset=UTF-8", null);
|
||||
|
||||
alert.setView(wv);
|
||||
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);
|
||||
}
|
||||
public final class LicenseFragmentHelper {
|
||||
private LicenseFragmentHelper() { }
|
||||
|
||||
/**
|
||||
* @param context the context to use
|
||||
* @param license the license
|
||||
* @return String which contains a HTML formatted license page styled according to the context's theme
|
||||
* @return String which contains a HTML formatted license page
|
||||
* styled according to the context's theme
|
||||
*/
|
||||
public static String getFormattedLicense(Context context, License license) {
|
||||
if(context == null) {
|
||||
throw new NullPointerException("context is null");
|
||||
}
|
||||
if(license == null) {
|
||||
throw new NullPointerException("license is null");
|
||||
}
|
||||
|
||||
StringBuilder licenseContent = new StringBuilder();
|
||||
String webViewData;
|
||||
try {
|
||||
BufferedReader in = new BufferedReader(new InputStreamReader(context.getAssets().open(license.getFilename()), "UTF-8"));
|
||||
private static String getFormattedLicense(@NonNull final Context context,
|
||||
@NonNull final License license) {
|
||||
final StringBuilder licenseContent = new StringBuilder();
|
||||
final String webViewData;
|
||||
try (BufferedReader in = new BufferedReader(new InputStreamReader(
|
||||
context.getAssets().open(license.getFilename()), StandardCharsets.UTF_8))) {
|
||||
String str;
|
||||
while ((str = in.readLine()) != null) {
|
||||
licenseContent.append(str);
|
||||
}
|
||||
in.close();
|
||||
|
||||
// split the HTML file and insert the stylesheet into the HEAD of the file
|
||||
String[] insert = licenseContent.toString().split("</head>");
|
||||
webViewData = insert[0] + "<style type=\"text/css\">"
|
||||
+ getLicenseStylesheet(context) + "</style></head>"
|
||||
+ insert[1];
|
||||
} catch (Exception e) {
|
||||
throw new NullPointerException("could not get license file:" + getLicenseStylesheet(context));
|
||||
webViewData = licenseContent.toString().replace("</head>",
|
||||
"<style>" + getLicenseStylesheet(context) + "</style></head>");
|
||||
} catch (final IOException e) {
|
||||
throw new IllegalArgumentException(
|
||||
"Could not get license file: " + license.getFilename(), e);
|
||||
}
|
||||
return webViewData;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param context
|
||||
* @param context the Android context
|
||||
* @return String which is a CSS stylesheet according to the context's theme
|
||||
*/
|
||||
public static String getLicenseStylesheet(Context context) {
|
||||
boolean isLightTheme = ThemeHelper.isLightThemeSelected(context);
|
||||
return "body{padding:12px 15px;margin:0;background:#"
|
||||
+ getHexRGBColor(context, isLightTheme
|
||||
? R.color.light_license_background_color
|
||||
: R.color.dark_license_background_color)
|
||||
+ ";color:#"
|
||||
+ getHexRGBColor(context, isLightTheme
|
||||
? R.color.light_license_text_color
|
||||
: R.color.dark_license_text_color) + ";}"
|
||||
+ "a[href]{color:#"
|
||||
+ getHexRGBColor(context, isLightTheme
|
||||
? R.color.light_youtube_primary_color
|
||||
: R.color.dark_youtube_primary_color) + ";}"
|
||||
+ "pre{white-space: pre-wrap;}";
|
||||
private static String getLicenseStylesheet(@NonNull final Context context) {
|
||||
final boolean isLightTheme = ThemeHelper.isLightThemeSelected(context);
|
||||
return "body{padding:12px 15px;margin:0;"
|
||||
+ "background:#" + getHexRGBColor(context, isLightTheme
|
||||
? R.color.light_license_background_color
|
||||
: R.color.dark_license_background_color) + ";"
|
||||
+ "color:#" + getHexRGBColor(context, isLightTheme
|
||||
? R.color.light_license_text_color
|
||||
: R.color.dark_license_text_color) + "}"
|
||||
+ "a[href]{color:#" + getHexRGBColor(context, isLightTheme
|
||||
? R.color.light_youtube_primary_color
|
||||
: R.color.dark_youtube_primary_color) + "}"
|
||||
+ "pre{white-space:pre-wrap}";
|
||||
}
|
||||
|
||||
/**
|
||||
* Cast R.color to a hexadecimal color value
|
||||
* Cast R.color to a hexadecimal color value.
|
||||
*
|
||||
* @param context the context to use
|
||||
* @param color the color number from R.color
|
||||
* @param color the color number from R.color
|
||||
* @return a six characters long String with hexadecimal RGB values
|
||||
*/
|
||||
public static String getHexRGBColor(Context context, int color) {
|
||||
private static String getHexRGBColor(@NonNull final Context context, final int color) {
|
||||
return context.getResources().getString(color).substring(3);
|
||||
}
|
||||
|
||||
static Disposable showLicense(@Nullable final Context context, @NonNull final License license) {
|
||||
if (context == null) {
|
||||
return Disposable.empty();
|
||||
}
|
||||
|
||||
return Observable.fromCallable(() -> getFormattedLicense(context, license))
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(formattedLicense -> {
|
||||
final String webViewData = Base64.encodeToString(formattedLicense
|
||||
.getBytes(StandardCharsets.UTF_8), Base64.NO_PADDING);
|
||||
final WebView webView = new WebView(context);
|
||||
webView.loadData(webViewData, "text/html; charset=UTF-8", "base64");
|
||||
|
||||
final AlertDialog.Builder alert = new AlertDialog.Builder(context);
|
||||
alert.setTitle(license.getName());
|
||||
alert.setView(webView);
|
||||
assureCorrectAppLanguage(context);
|
||||
alert.setNegativeButton(context.getString(R.string.finish),
|
||||
(dialog, which) -> dialog.dismiss());
|
||||
alert.show();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,83 +0,0 @@
|
||||
package org.schabi.newpipe.about;
|
||||
|
||||
import android.os.Parcel;
|
||||
import android.os.Parcelable;
|
||||
|
||||
public class SoftwareComponent implements Parcelable {
|
||||
|
||||
public static final Creator<SoftwareComponent> CREATOR = new Creator<SoftwareComponent>() {
|
||||
@Override
|
||||
public SoftwareComponent createFromParcel(Parcel source) {
|
||||
return new SoftwareComponent(source);
|
||||
}
|
||||
|
||||
@Override
|
||||
public SoftwareComponent[] newArray(int size) {
|
||||
return new SoftwareComponent[size];
|
||||
}
|
||||
};
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
public String getYears() {
|
||||
return years;
|
||||
}
|
||||
|
||||
public String getCopyrightOwner() {
|
||||
return copyrightOwner;
|
||||
}
|
||||
|
||||
public String getLink() {
|
||||
return link;
|
||||
}
|
||||
|
||||
public String getVersion() {
|
||||
return version;
|
||||
}
|
||||
|
||||
private final License license;
|
||||
private final String name;
|
||||
private final String years;
|
||||
private final String copyrightOwner;
|
||||
private final String link;
|
||||
private final String version;
|
||||
|
||||
public SoftwareComponent(String name, String years, String copyrightOwner, String link, License license) {
|
||||
this.name = name;
|
||||
this.years = years;
|
||||
this.copyrightOwner = copyrightOwner;
|
||||
this.link = link;
|
||||
this.license = license;
|
||||
this.version = null;
|
||||
}
|
||||
|
||||
protected SoftwareComponent(Parcel in) {
|
||||
this.name = in.readString();
|
||||
this.license = in.readParcelable(License.class.getClassLoader());
|
||||
this.copyrightOwner = in.readString();
|
||||
this.link = in.readString();
|
||||
this.years = in.readString();
|
||||
this.version = in.readString();
|
||||
}
|
||||
|
||||
public License getLicense() {
|
||||
return license;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int describeContents() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeToParcel(Parcel dest, int flags) {
|
||||
dest.writeString(name);
|
||||
dest.writeParcelable(license, flags);
|
||||
dest.writeString(copyrightOwner);
|
||||
dest.writeString(link);
|
||||
dest.writeString(years);
|
||||
dest.writeString(version);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package org.schabi.newpipe.about
|
||||
|
||||
import android.os.Parcelable
|
||||
import kotlinx.android.parcel.Parcelize
|
||||
|
||||
@Parcelize
|
||||
class SoftwareComponent
|
||||
@JvmOverloads
|
||||
constructor(
|
||||
val name: String,
|
||||
val years: String,
|
||||
val copyrightOwner: String,
|
||||
val link: String,
|
||||
val license: License,
|
||||
val version: String? = null
|
||||
) : Parcelable
|
||||
@@ -1,12 +1,17 @@
|
||||
package org.schabi.newpipe.about;
|
||||
|
||||
/**
|
||||
* Standard software licenses
|
||||
* Class containing information about standard software licenses.
|
||||
*/
|
||||
public final class StandardLicenses {
|
||||
public static final License GPL2 = new License("GNU General Public License, Version 2.0", "GPLv2", "gpl_2.html");
|
||||
public static final License GPL3 = new License("GNU General Public License, Version 3.0", "GPLv3", "gpl_3.html");
|
||||
public static final License APACHE2 = new License("Apache License, Version 2.0", "ALv2", "apache2.html");
|
||||
public static final License MPL2 = new License("Mozilla Public License, Version 2.0", "MPL 2.0", "mpl2.html");
|
||||
public static final License MIT = new License("MIT License", "MIT", "mit.html");
|
||||
public static final License GPL3
|
||||
= new License("GNU General Public License, Version 3.0", "GPLv3", "gpl_3.html");
|
||||
public static final License APACHE2
|
||||
= new License("Apache License, Version 2.0", "ALv2", "apache2.html");
|
||||
public static final License MPL2
|
||||
= new License("Mozilla Public License, Version 2.0", "MPL 2.0", "mpl2.html");
|
||||
public static final License MIT
|
||||
= new License("MIT License", "MIT", "mit.html");
|
||||
|
||||
private StandardLicenses() { }
|
||||
}
|
||||
|
||||
@@ -46,14 +46,20 @@ public abstract class AppDatabase extends RoomDatabase {
|
||||
public abstract SearchHistoryDAO searchHistoryDAO();
|
||||
|
||||
public abstract StreamDAO streamDAO();
|
||||
|
||||
public abstract StreamHistoryDAO streamHistoryDAO();
|
||||
|
||||
public abstract StreamStateDAO streamStateDAO();
|
||||
|
||||
public abstract PlaylistDAO playlistDAO();
|
||||
|
||||
public abstract PlaylistStreamDAO playlistStreamDAO();
|
||||
|
||||
public abstract PlaylistRemoteDAO playlistRemoteDAO();
|
||||
|
||||
public abstract FeedDAO feedDAO();
|
||||
|
||||
public abstract FeedGroupDAO feedGroupDAO();
|
||||
|
||||
public abstract SubscriptionDAO subscriptionDAO();
|
||||
}
|
||||
|
||||
@@ -9,19 +9,19 @@ import androidx.room.Update;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
|
||||
import io.reactivex.Flowable;
|
||||
import io.reactivex.rxjava3.core.Flowable;
|
||||
|
||||
@Dao
|
||||
public interface BasicDAO<Entity> {
|
||||
/* Inserts */
|
||||
@Insert(onConflict = OnConflictStrategy.FAIL)
|
||||
long insert(final Entity entity);
|
||||
@Insert(onConflict = OnConflictStrategy.ABORT)
|
||||
long insert(Entity entity);
|
||||
|
||||
@Insert(onConflict = OnConflictStrategy.FAIL)
|
||||
List<Long> insertAll(final Entity... entities);
|
||||
@Insert(onConflict = OnConflictStrategy.ABORT)
|
||||
List<Long> insertAll(Entity... entities);
|
||||
|
||||
@Insert(onConflict = OnConflictStrategy.FAIL)
|
||||
List<Long> insertAll(final Collection<Entity> entities);
|
||||
@Insert(onConflict = OnConflictStrategy.ABORT)
|
||||
List<Long> insertAll(Collection<Entity> entities);
|
||||
|
||||
/* Searches */
|
||||
Flowable<List<Entity>> getAll();
|
||||
@@ -30,17 +30,17 @@ public interface BasicDAO<Entity> {
|
||||
|
||||
/* Deletes */
|
||||
@Delete
|
||||
void delete(final Entity entity);
|
||||
void delete(Entity entity);
|
||||
|
||||
@Delete
|
||||
int delete(final Collection<Entity> entities);
|
||||
int delete(Collection<Entity> entities);
|
||||
|
||||
int deleteAll();
|
||||
|
||||
/* Updates */
|
||||
@Update
|
||||
int update(final Entity entity);
|
||||
int update(Entity entity);
|
||||
|
||||
@Update
|
||||
void update(final Collection<Entity> entities);
|
||||
void update(Collection<Entity> entities);
|
||||
}
|
||||
|
||||
@@ -5,49 +5,58 @@ import androidx.room.TypeConverter;
|
||||
import org.schabi.newpipe.extractor.stream.StreamType;
|
||||
import org.schabi.newpipe.local.subscription.FeedGroupIcon;
|
||||
|
||||
import java.util.Date;
|
||||
import java.time.Instant;
|
||||
import java.time.OffsetDateTime;
|
||||
import java.time.ZoneOffset;
|
||||
|
||||
public class Converters {
|
||||
public final class Converters {
|
||||
private Converters() { }
|
||||
|
||||
/**
|
||||
* Convert a long value to a date
|
||||
* Convert a long value to a {@link OffsetDateTime}.
|
||||
*
|
||||
* @param value the long value
|
||||
* @return the date
|
||||
* @return the {@code OffsetDateTime}
|
||||
*/
|
||||
@TypeConverter
|
||||
public static Date fromTimestamp(Long value) {
|
||||
return value == null ? null : new Date(value);
|
||||
public static OffsetDateTime offsetDateTimeFromTimestamp(final Long value) {
|
||||
return value == null ? null : OffsetDateTime.ofInstant(Instant.ofEpochMilli(value),
|
||||
ZoneOffset.UTC);
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert a date to a long value
|
||||
* @param date the date
|
||||
* Convert a {@link OffsetDateTime} to a long value.
|
||||
*
|
||||
* @param offsetDateTime the {@code OffsetDateTime}
|
||||
* @return the long value
|
||||
*/
|
||||
@TypeConverter
|
||||
public static Long dateToTimestamp(Date date) {
|
||||
return date == null ? null : date.getTime();
|
||||
public static Long offsetDateTimeToTimestamp(final OffsetDateTime offsetDateTime) {
|
||||
return offsetDateTime == null ? null : offsetDateTime.withOffsetSameInstant(ZoneOffset.UTC)
|
||||
.toInstant().toEpochMilli();
|
||||
}
|
||||
|
||||
@TypeConverter
|
||||
public static StreamType streamTypeOf(String value) {
|
||||
public static StreamType streamTypeOf(final String value) {
|
||||
return StreamType.valueOf(value);
|
||||
}
|
||||
|
||||
@TypeConverter
|
||||
public static String stringOf(StreamType streamType) {
|
||||
public static String stringOf(final StreamType streamType) {
|
||||
return streamType.name();
|
||||
}
|
||||
|
||||
@TypeConverter
|
||||
public static Integer integerOf(FeedGroupIcon feedGroupIcon) {
|
||||
public static Integer integerOf(final FeedGroupIcon feedGroupIcon) {
|
||||
return feedGroupIcon.getId();
|
||||
}
|
||||
|
||||
@TypeConverter
|
||||
public static FeedGroupIcon feedGroupIconOf(Integer id) {
|
||||
for (FeedGroupIcon icon : FeedGroupIcon.values()) {
|
||||
if (icon.getId() == id) return icon;
|
||||
public static FeedGroupIcon feedGroupIconOf(final Integer id) {
|
||||
for (final FeedGroupIcon icon : FeedGroupIcon.values()) {
|
||||
if (icon.getId() == id) {
|
||||
return icon;
|
||||
}
|
||||
}
|
||||
|
||||
throw new IllegalArgumentException("There's no feed group icon with the id \"" + id + "\"");
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
package org.schabi.newpipe.database;
|
||||
|
||||
public interface LocalItem {
|
||||
LocalItemType getLocalItemType();
|
||||
|
||||
enum LocalItemType {
|
||||
PLAYLIST_LOCAL_ITEM,
|
||||
PLAYLIST_REMOTE_ITEM,
|
||||
@@ -8,6 +10,4 @@ public interface LocalItem {
|
||||
PLAYLIST_STREAM_ITEM,
|
||||
STATISTIC_STREAM_ITEM,
|
||||
}
|
||||
|
||||
LocalItemType getLocalItemType();
|
||||
}
|
||||
|
||||
@@ -1,72 +1,103 @@
|
||||
package org.schabi.newpipe.database;
|
||||
|
||||
import androidx.sqlite.db.SupportSQLiteDatabase;
|
||||
import androidx.room.migration.Migration;
|
||||
import androidx.annotation.NonNull;
|
||||
import android.util.Log;
|
||||
|
||||
import org.schabi.newpipe.BuildConfig;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.room.migration.Migration;
|
||||
import androidx.sqlite.db.SupportSQLiteDatabase;
|
||||
|
||||
public class Migrations {
|
||||
import org.schabi.newpipe.MainActivity;
|
||||
|
||||
public final class Migrations {
|
||||
public static final int DB_VER_1 = 1;
|
||||
public static final int DB_VER_2 = 2;
|
||||
public static final int DB_VER_3 = 3;
|
||||
|
||||
public static final boolean DEBUG = !BuildConfig.BUILD_TYPE.equals("release");
|
||||
private static final String TAG = Migrations.class.getName();
|
||||
public static final boolean DEBUG = MainActivity.DEBUG;
|
||||
|
||||
public static final Migration MIGRATION_1_2 = new Migration(DB_VER_1, DB_VER_2) {
|
||||
@Override
|
||||
public void migrate(@NonNull SupportSQLiteDatabase database) {
|
||||
if(DEBUG) {
|
||||
public void migrate(@NonNull final SupportSQLiteDatabase database) {
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "Start migrating database");
|
||||
}
|
||||
/*
|
||||
* Unfortunately these queries must be hardcoded due to the possibility of
|
||||
* schema and names changing at a later date, thus invalidating the older migration
|
||||
* scripts if they are not hardcoded.
|
||||
* */
|
||||
* Unfortunately these queries must be hardcoded due to the possibility of
|
||||
* schema and names changing at a later date, thus invalidating the older migration
|
||||
* scripts if they are not hardcoded.
|
||||
* */
|
||||
|
||||
// Not much we can do about this, since room doesn't create tables before migration.
|
||||
// It's either this or blasting the entire database anew.
|
||||
database.execSQL("CREATE INDEX `index_search_history_search` ON `search_history` (`search`)");
|
||||
database.execSQL("CREATE TABLE IF NOT EXISTS `streams` (`uid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `service_id` INTEGER NOT NULL, `url` TEXT, `title` TEXT, `stream_type` TEXT, `duration` INTEGER, `uploader` TEXT, `thumbnail_url` TEXT)");
|
||||
database.execSQL("CREATE UNIQUE INDEX `index_streams_service_id_url` ON `streams` (`service_id`, `url`)");
|
||||
database.execSQL("CREATE TABLE IF NOT EXISTS `stream_history` (`stream_id` INTEGER NOT NULL, `access_date` INTEGER NOT NULL, `repeat_count` INTEGER NOT NULL, PRIMARY KEY(`stream_id`, `access_date`), FOREIGN KEY(`stream_id`) REFERENCES `streams`(`uid`) ON UPDATE CASCADE ON DELETE CASCADE )");
|
||||
database.execSQL("CREATE INDEX `index_stream_history_stream_id` ON `stream_history` (`stream_id`)");
|
||||
database.execSQL("CREATE TABLE IF NOT EXISTS `stream_state` (`stream_id` INTEGER NOT NULL, `progress_time` INTEGER NOT NULL, PRIMARY KEY(`stream_id`), FOREIGN KEY(`stream_id`) REFERENCES `streams`(`uid`) ON UPDATE CASCADE ON DELETE CASCADE )");
|
||||
database.execSQL("CREATE TABLE IF NOT EXISTS `playlists` (`uid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT, `thumbnail_url` TEXT)");
|
||||
database.execSQL("CREATE INDEX `index_search_history_search` "
|
||||
+ "ON `search_history` (`search`)");
|
||||
database.execSQL("CREATE TABLE IF NOT EXISTS `streams` "
|
||||
+ "(`uid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, "
|
||||
+ "`service_id` INTEGER NOT NULL, `url` TEXT, `title` TEXT, "
|
||||
+ "`stream_type` TEXT, `duration` INTEGER, `uploader` TEXT, "
|
||||
+ "`thumbnail_url` TEXT)");
|
||||
database.execSQL("CREATE UNIQUE INDEX `index_streams_service_id_url` "
|
||||
+ "ON `streams` (`service_id`, `url`)");
|
||||
database.execSQL("CREATE TABLE IF NOT EXISTS `stream_history` "
|
||||
+ "(`stream_id` INTEGER NOT NULL, `access_date` INTEGER NOT NULL, "
|
||||
+ "`repeat_count` INTEGER NOT NULL, PRIMARY KEY(`stream_id`, `access_date`), "
|
||||
+ "FOREIGN KEY(`stream_id`) REFERENCES `streams`(`uid`) "
|
||||
+ "ON UPDATE CASCADE ON DELETE CASCADE )");
|
||||
database.execSQL("CREATE INDEX `index_stream_history_stream_id` "
|
||||
+ "ON `stream_history` (`stream_id`)");
|
||||
database.execSQL("CREATE TABLE IF NOT EXISTS `stream_state` "
|
||||
+ "(`stream_id` INTEGER NOT NULL, `progress_time` INTEGER NOT NULL, "
|
||||
+ "PRIMARY KEY(`stream_id`), FOREIGN KEY(`stream_id`) "
|
||||
+ "REFERENCES `streams`(`uid`) ON UPDATE CASCADE ON DELETE CASCADE )");
|
||||
database.execSQL("CREATE TABLE IF NOT EXISTS `playlists` "
|
||||
+ "(`uid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, "
|
||||
+ "`name` TEXT, `thumbnail_url` TEXT)");
|
||||
database.execSQL("CREATE INDEX `index_playlists_name` ON `playlists` (`name`)");
|
||||
database.execSQL("CREATE TABLE IF NOT EXISTS `playlist_stream_join` (`playlist_id` INTEGER NOT NULL, `stream_id` INTEGER NOT NULL, `join_index` INTEGER NOT NULL, PRIMARY KEY(`playlist_id`, `join_index`), FOREIGN KEY(`playlist_id`) REFERENCES `playlists`(`uid`) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED, FOREIGN KEY(`stream_id`) REFERENCES `streams`(`uid`) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED)");
|
||||
database.execSQL("CREATE UNIQUE INDEX `index_playlist_stream_join_playlist_id_join_index` ON `playlist_stream_join` (`playlist_id`, `join_index`)");
|
||||
database.execSQL("CREATE INDEX `index_playlist_stream_join_stream_id` ON `playlist_stream_join` (`stream_id`)");
|
||||
database.execSQL("CREATE TABLE IF NOT EXISTS `remote_playlists` (`uid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `service_id` INTEGER NOT NULL, `name` TEXT, `url` TEXT, `thumbnail_url` TEXT, `uploader` TEXT, `stream_count` INTEGER)");
|
||||
database.execSQL("CREATE INDEX `index_remote_playlists_name` ON `remote_playlists` (`name`)");
|
||||
database.execSQL("CREATE UNIQUE INDEX `index_remote_playlists_service_id_url` ON `remote_playlists` (`service_id`, `url`)");
|
||||
database.execSQL("CREATE TABLE IF NOT EXISTS `playlist_stream_join` "
|
||||
+ "(`playlist_id` INTEGER NOT NULL, `stream_id` INTEGER NOT NULL, "
|
||||
+ "`join_index` INTEGER NOT NULL, PRIMARY KEY(`playlist_id`, `join_index`), "
|
||||
+ "FOREIGN KEY(`playlist_id`) REFERENCES `playlists`(`uid`) "
|
||||
+ "ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED, "
|
||||
+ "FOREIGN KEY(`stream_id`) REFERENCES `streams`(`uid`) "
|
||||
+ "ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED)");
|
||||
database.execSQL("CREATE UNIQUE INDEX "
|
||||
+ "`index_playlist_stream_join_playlist_id_join_index` "
|
||||
+ "ON `playlist_stream_join` (`playlist_id`, `join_index`)");
|
||||
database.execSQL("CREATE INDEX `index_playlist_stream_join_stream_id` "
|
||||
+ "ON `playlist_stream_join` (`stream_id`)");
|
||||
database.execSQL("CREATE TABLE IF NOT EXISTS `remote_playlists` "
|
||||
+ "(`uid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, "
|
||||
+ "`service_id` INTEGER NOT NULL, `name` TEXT, `url` TEXT, "
|
||||
+ "`thumbnail_url` TEXT, `uploader` TEXT, `stream_count` INTEGER)");
|
||||
database.execSQL("CREATE INDEX `index_remote_playlists_name` "
|
||||
+ "ON `remote_playlists` (`name`)");
|
||||
database.execSQL("CREATE UNIQUE INDEX `index_remote_playlists_service_id_url` "
|
||||
+ "ON `remote_playlists` (`service_id`, `url`)");
|
||||
|
||||
// Populate streams table with existing entries in watch history
|
||||
// Latest data first, thus ignoring older entries with the same indices
|
||||
database.execSQL("INSERT OR IGNORE INTO streams (service_id, url, title, " +
|
||||
"stream_type, duration, uploader, thumbnail_url) " +
|
||||
database.execSQL("INSERT OR IGNORE INTO streams (service_id, url, title, "
|
||||
+ "stream_type, duration, uploader, thumbnail_url) "
|
||||
|
||||
"SELECT service_id, url, title, 'VIDEO_STREAM', duration, " +
|
||||
"uploader, thumbnail_url " +
|
||||
+ "SELECT service_id, url, title, 'VIDEO_STREAM', duration, "
|
||||
+ "uploader, thumbnail_url "
|
||||
|
||||
"FROM watch_history " +
|
||||
"ORDER BY creation_date DESC");
|
||||
+ "FROM watch_history "
|
||||
+ "ORDER BY creation_date DESC");
|
||||
|
||||
// Once the streams have PKs, join them with the normalized history table
|
||||
// and populate it with the remaining data from watch history
|
||||
database.execSQL("INSERT INTO stream_history (stream_id, access_date, repeat_count)" +
|
||||
"SELECT uid, creation_date, 1 " +
|
||||
"FROM watch_history INNER JOIN streams " +
|
||||
"ON watch_history.service_id == streams.service_id " +
|
||||
"AND watch_history.url == streams.url " +
|
||||
"ORDER BY creation_date DESC");
|
||||
database.execSQL("INSERT INTO stream_history (stream_id, access_date, repeat_count)"
|
||||
+ "SELECT uid, creation_date, 1 "
|
||||
+ "FROM watch_history INNER JOIN streams "
|
||||
+ "ON watch_history.service_id == streams.service_id "
|
||||
+ "AND watch_history.url == streams.url "
|
||||
+ "ORDER BY creation_date DESC");
|
||||
|
||||
database.execSQL("DROP TABLE IF EXISTS watch_history");
|
||||
|
||||
if(DEBUG) {
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "Stop migrating database");
|
||||
}
|
||||
}
|
||||
@@ -74,29 +105,60 @@ public class Migrations {
|
||||
|
||||
public static final Migration MIGRATION_2_3 = new Migration(DB_VER_2, DB_VER_3) {
|
||||
@Override
|
||||
public void migrate(@NonNull SupportSQLiteDatabase database) {
|
||||
public void migrate(@NonNull final SupportSQLiteDatabase database) {
|
||||
// Add NOT NULLs and new fields
|
||||
database.execSQL("CREATE TABLE IF NOT EXISTS streams_new " +
|
||||
"(uid INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, service_id INTEGER NOT NULL, url TEXT NOT NULL, title TEXT NOT NULL, stream_type TEXT NOT NULL," +
|
||||
" duration INTEGER NOT NULL, uploader TEXT NOT NULL, thumbnail_url TEXT, view_count INTEGER, textual_upload_date TEXT, upload_date INTEGER," +
|
||||
" is_upload_date_approximation INTEGER)");
|
||||
database.execSQL("CREATE TABLE IF NOT EXISTS streams_new "
|
||||
+ "(uid INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, "
|
||||
+ "service_id INTEGER NOT NULL, url TEXT NOT NULL, title TEXT NOT NULL, "
|
||||
+ "stream_type TEXT NOT NULL, duration INTEGER NOT NULL, "
|
||||
+ "uploader TEXT NOT NULL, thumbnail_url TEXT, view_count INTEGER, "
|
||||
+ "textual_upload_date TEXT, upload_date INTEGER, "
|
||||
+ "is_upload_date_approximation INTEGER)");
|
||||
|
||||
database.execSQL("INSERT INTO streams_new (uid, service_id, url, title, stream_type, duration, uploader, thumbnail_url, view_count, textual_upload_date, upload_date, is_upload_date_approximation)"+
|
||||
" SELECT uid, service_id, url, title, stream_type, duration, uploader, thumbnail_url, NULL, NULL, NULL, NULL FROM streams");
|
||||
database.execSQL("INSERT INTO streams_new (uid, service_id, url, title, stream_type, "
|
||||
+ "duration, uploader, thumbnail_url, view_count, textual_upload_date, "
|
||||
+ "upload_date, is_upload_date_approximation) "
|
||||
|
||||
+ "SELECT uid, service_id, url, ifnull(title, ''), "
|
||||
+ "ifnull(stream_type, 'VIDEO_STREAM'), ifnull(duration, 0), "
|
||||
+ "ifnull(uploader, ''), ifnull(thumbnail_url, ''), NULL, NULL, NULL, NULL "
|
||||
|
||||
+ "FROM streams WHERE url IS NOT NULL");
|
||||
|
||||
database.execSQL("DROP TABLE streams");
|
||||
database.execSQL("ALTER TABLE streams_new RENAME TO streams");
|
||||
database.execSQL("CREATE UNIQUE INDEX index_streams_service_id_url ON streams (service_id, url)");
|
||||
database.execSQL("CREATE UNIQUE INDEX index_streams_service_id_url "
|
||||
+ "ON streams (service_id, url)");
|
||||
|
||||
// Tables for feed feature
|
||||
database.execSQL("CREATE TABLE IF NOT EXISTS feed (stream_id INTEGER NOT NULL, subscription_id INTEGER NOT NULL, PRIMARY KEY(stream_id, subscription_id), FOREIGN KEY(stream_id) REFERENCES streams(uid) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED, FOREIGN KEY(subscription_id) REFERENCES subscriptions(uid) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED)");
|
||||
database.execSQL("CREATE TABLE IF NOT EXISTS feed "
|
||||
+ "(stream_id INTEGER NOT NULL, subscription_id INTEGER NOT NULL, "
|
||||
+ "PRIMARY KEY(stream_id, subscription_id), "
|
||||
+ "FOREIGN KEY(stream_id) REFERENCES streams(uid) "
|
||||
+ "ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED, "
|
||||
+ "FOREIGN KEY(subscription_id) REFERENCES subscriptions(uid) "
|
||||
+ "ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED)");
|
||||
database.execSQL("CREATE INDEX index_feed_subscription_id ON feed (subscription_id)");
|
||||
database.execSQL("CREATE TABLE IF NOT EXISTS feed_group (uid INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, name TEXT NOT NULL, icon_id INTEGER NOT NULL, sort_order INTEGER NOT NULL)");
|
||||
database.execSQL("CREATE TABLE IF NOT EXISTS feed_group "
|
||||
+ "(uid INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, name TEXT NOT NULL, "
|
||||
+ "icon_id INTEGER NOT NULL, sort_order INTEGER NOT NULL)");
|
||||
database.execSQL("CREATE INDEX index_feed_group_sort_order ON feed_group (sort_order)");
|
||||
database.execSQL("CREATE TABLE IF NOT EXISTS feed_group_subscription_join (group_id INTEGER NOT NULL, subscription_id INTEGER NOT NULL, PRIMARY KEY(group_id, subscription_id), FOREIGN KEY(group_id) REFERENCES feed_group(uid) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED, FOREIGN KEY(subscription_id) REFERENCES subscriptions(uid) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED)");
|
||||
database.execSQL("CREATE INDEX index_feed_group_subscription_join_subscription_id ON feed_group_subscription_join (subscription_id)");
|
||||
database.execSQL("CREATE TABLE IF NOT EXISTS feed_last_updated (subscription_id INTEGER NOT NULL, last_updated INTEGER, PRIMARY KEY(subscription_id), FOREIGN KEY(subscription_id) REFERENCES subscriptions(uid) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED)");
|
||||
database.execSQL("CREATE TABLE IF NOT EXISTS feed_group_subscription_join "
|
||||
+ "(group_id INTEGER NOT NULL, subscription_id INTEGER NOT NULL, "
|
||||
+ "PRIMARY KEY(group_id, subscription_id), "
|
||||
+ "FOREIGN KEY(group_id) REFERENCES feed_group(uid) "
|
||||
+ "ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED, "
|
||||
+ "FOREIGN KEY(subscription_id) REFERENCES subscriptions(uid) "
|
||||
+ "ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED)");
|
||||
database.execSQL("CREATE INDEX index_feed_group_subscription_join_subscription_id "
|
||||
+ "ON feed_group_subscription_join (subscription_id)");
|
||||
database.execSQL("CREATE TABLE IF NOT EXISTS feed_last_updated "
|
||||
+ "(subscription_id INTEGER NOT NULL, last_updated INTEGER, "
|
||||
+ "PRIMARY KEY(subscription_id), "
|
||||
+ "FOREIGN KEY(subscription_id) REFERENCES subscriptions(uid) "
|
||||
+ "ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED)");
|
||||
}
|
||||
};
|
||||
|
||||
private Migrations() { }
|
||||
}
|
||||
|
||||
@@ -1,19 +1,25 @@
|
||||
package org.schabi.newpipe.database.feed.dao
|
||||
|
||||
import androidx.room.*
|
||||
import io.reactivex.Flowable
|
||||
import androidx.room.Dao
|
||||
import androidx.room.Insert
|
||||
import androidx.room.OnConflictStrategy
|
||||
import androidx.room.Query
|
||||
import androidx.room.Transaction
|
||||
import androidx.room.Update
|
||||
import io.reactivex.rxjava3.core.Flowable
|
||||
import org.schabi.newpipe.database.feed.model.FeedEntity
|
||||
import org.schabi.newpipe.database.feed.model.FeedLastUpdatedEntity
|
||||
import org.schabi.newpipe.database.stream.model.StreamEntity
|
||||
import org.schabi.newpipe.database.subscription.SubscriptionEntity
|
||||
import java.util.*
|
||||
import java.time.OffsetDateTime
|
||||
|
||||
@Dao
|
||||
abstract class FeedDAO {
|
||||
@Query("DELETE FROM feed")
|
||||
abstract fun deleteAll(): Int
|
||||
|
||||
@Query("""
|
||||
@Query(
|
||||
"""
|
||||
SELECT s.* FROM streams s
|
||||
|
||||
INNER JOIN feed f
|
||||
@@ -22,10 +28,12 @@ abstract class FeedDAO {
|
||||
ORDER BY s.upload_date IS NULL DESC, s.upload_date DESC, s.uploader ASC
|
||||
|
||||
LIMIT 500
|
||||
""")
|
||||
"""
|
||||
)
|
||||
abstract fun getAllStreams(): Flowable<List<StreamEntity>>
|
||||
|
||||
@Query("""
|
||||
@Query(
|
||||
"""
|
||||
SELECT s.* FROM streams s
|
||||
|
||||
INNER JOIN feed f
|
||||
@@ -41,10 +49,12 @@ abstract class FeedDAO {
|
||||
|
||||
ORDER BY s.upload_date IS NULL DESC, s.upload_date DESC, s.uploader ASC
|
||||
LIMIT 500
|
||||
""")
|
||||
"""
|
||||
)
|
||||
abstract fun getAllStreamsFromGroup(groupId: Long): Flowable<List<StreamEntity>>
|
||||
|
||||
@Query("""
|
||||
@Query(
|
||||
"""
|
||||
DELETE FROM feed WHERE
|
||||
|
||||
feed.stream_id IN (
|
||||
@@ -53,12 +63,14 @@ abstract class FeedDAO {
|
||||
INNER JOIN feed f
|
||||
ON s.uid = f.stream_id
|
||||
|
||||
WHERE s.upload_date < :date
|
||||
WHERE s.upload_date < :offsetDateTime
|
||||
)
|
||||
""")
|
||||
abstract fun unlinkStreamsOlderThan(date: Date)
|
||||
"""
|
||||
)
|
||||
abstract fun unlinkStreamsOlderThan(offsetDateTime: OffsetDateTime)
|
||||
|
||||
@Query("""
|
||||
@Query(
|
||||
"""
|
||||
DELETE FROM feed
|
||||
|
||||
WHERE feed.subscription_id = :subscriptionId
|
||||
@@ -71,7 +83,8 @@ abstract class FeedDAO {
|
||||
|
||||
WHERE s.stream_type = "LIVE_STREAM" OR s.stream_type = "AUDIO_LIVE_STREAM"
|
||||
)
|
||||
""")
|
||||
"""
|
||||
)
|
||||
abstract fun unlinkOldLivestreams(subscriptionId: Long)
|
||||
|
||||
@Insert(onConflict = OnConflictStrategy.IGNORE)
|
||||
@@ -95,21 +108,24 @@ abstract class FeedDAO {
|
||||
}
|
||||
}
|
||||
|
||||
@Query("""
|
||||
@Query(
|
||||
"""
|
||||
SELECT MIN(lu.last_updated) FROM feed_last_updated lu
|
||||
|
||||
INNER JOIN feed_group_subscription_join fgs
|
||||
ON fgs.subscription_id = lu.subscription_id AND fgs.group_id = :groupId
|
||||
""")
|
||||
abstract fun oldestSubscriptionUpdate(groupId: Long): Flowable<List<Date>>
|
||||
"""
|
||||
)
|
||||
abstract fun oldestSubscriptionUpdate(groupId: Long): Flowable<List<OffsetDateTime>>
|
||||
|
||||
@Query("SELECT MIN(last_updated) FROM feed_last_updated")
|
||||
abstract fun oldestSubscriptionUpdateFromAll(): Flowable<List<Date>>
|
||||
abstract fun oldestSubscriptionUpdateFromAll(): Flowable<List<OffsetDateTime>>
|
||||
|
||||
@Query("SELECT COUNT(*) FROM feed_last_updated WHERE last_updated IS NULL")
|
||||
abstract fun notLoadedCount(): Flowable<Long>
|
||||
|
||||
@Query("""
|
||||
@Query(
|
||||
"""
|
||||
SELECT COUNT(*) FROM subscriptions s
|
||||
|
||||
INNER JOIN feed_group_subscription_join fgs
|
||||
@@ -119,20 +135,24 @@ abstract class FeedDAO {
|
||||
ON s.uid = lu.subscription_id
|
||||
|
||||
WHERE lu.last_updated IS NULL
|
||||
""")
|
||||
"""
|
||||
)
|
||||
abstract fun notLoadedCountForGroup(groupId: Long): Flowable<Long>
|
||||
|
||||
@Query("""
|
||||
@Query(
|
||||
"""
|
||||
SELECT s.* FROM subscriptions s
|
||||
|
||||
LEFT JOIN feed_last_updated lu
|
||||
ON s.uid = lu.subscription_id
|
||||
|
||||
WHERE lu.last_updated IS NULL OR lu.last_updated < :outdatedThreshold
|
||||
""")
|
||||
abstract fun getAllOutdated(outdatedThreshold: Date): Flowable<List<SubscriptionEntity>>
|
||||
"""
|
||||
)
|
||||
abstract fun getAllOutdated(outdatedThreshold: OffsetDateTime): Flowable<List<SubscriptionEntity>>
|
||||
|
||||
@Query("""
|
||||
@Query(
|
||||
"""
|
||||
SELECT s.* FROM subscriptions s
|
||||
|
||||
INNER JOIN feed_group_subscription_join fgs
|
||||
@@ -142,6 +162,7 @@ abstract class FeedDAO {
|
||||
ON s.uid = lu.subscription_id
|
||||
|
||||
WHERE lu.last_updated IS NULL OR lu.last_updated < :outdatedThreshold
|
||||
""")
|
||||
abstract fun getAllOutdatedForGroup(groupId: Long, outdatedThreshold: Date): Flowable<List<SubscriptionEntity>>
|
||||
"""
|
||||
)
|
||||
abstract fun getAllOutdatedForGroup(groupId: Long, outdatedThreshold: OffsetDateTime): Flowable<List<SubscriptionEntity>>
|
||||
}
|
||||
|
||||
@@ -1,8 +1,13 @@
|
||||
package org.schabi.newpipe.database.feed.dao
|
||||
|
||||
import androidx.room.*
|
||||
import io.reactivex.Flowable
|
||||
import io.reactivex.Maybe
|
||||
import androidx.room.Dao
|
||||
import androidx.room.Insert
|
||||
import androidx.room.OnConflictStrategy
|
||||
import androidx.room.Query
|
||||
import androidx.room.Transaction
|
||||
import androidx.room.Update
|
||||
import io.reactivex.rxjava3.core.Flowable
|
||||
import io.reactivex.rxjava3.core.Maybe
|
||||
import org.schabi.newpipe.database.feed.model.FeedGroupEntity
|
||||
import org.schabi.newpipe.database.feed.model.FeedGroupSubscriptionEntity
|
||||
|
||||
|
||||
@@ -10,28 +10,31 @@ import org.schabi.newpipe.database.feed.model.FeedEntity.Companion.SUBSCRIPTION_
|
||||
import org.schabi.newpipe.database.stream.model.StreamEntity
|
||||
import org.schabi.newpipe.database.subscription.SubscriptionEntity
|
||||
|
||||
@Entity(tableName = FEED_TABLE,
|
||||
primaryKeys = [STREAM_ID, SUBSCRIPTION_ID],
|
||||
indices = [Index(SUBSCRIPTION_ID)],
|
||||
foreignKeys = [
|
||||
ForeignKey(
|
||||
entity = StreamEntity::class,
|
||||
parentColumns = [StreamEntity.STREAM_ID],
|
||||
childColumns = [STREAM_ID],
|
||||
onDelete = ForeignKey.CASCADE, onUpdate = ForeignKey.CASCADE, deferred = true),
|
||||
ForeignKey(
|
||||
entity = SubscriptionEntity::class,
|
||||
parentColumns = [SubscriptionEntity.SUBSCRIPTION_UID],
|
||||
childColumns = [SUBSCRIPTION_ID],
|
||||
onDelete = ForeignKey.CASCADE, onUpdate = ForeignKey.CASCADE, deferred = true)
|
||||
]
|
||||
@Entity(
|
||||
tableName = FEED_TABLE,
|
||||
primaryKeys = [STREAM_ID, SUBSCRIPTION_ID],
|
||||
indices = [Index(SUBSCRIPTION_ID)],
|
||||
foreignKeys = [
|
||||
ForeignKey(
|
||||
entity = StreamEntity::class,
|
||||
parentColumns = [StreamEntity.STREAM_ID],
|
||||
childColumns = [STREAM_ID],
|
||||
onDelete = ForeignKey.CASCADE, onUpdate = ForeignKey.CASCADE, deferred = true
|
||||
),
|
||||
ForeignKey(
|
||||
entity = SubscriptionEntity::class,
|
||||
parentColumns = [SubscriptionEntity.SUBSCRIPTION_UID],
|
||||
childColumns = [SUBSCRIPTION_ID],
|
||||
onDelete = ForeignKey.CASCADE, onUpdate = ForeignKey.CASCADE, deferred = true
|
||||
)
|
||||
]
|
||||
)
|
||||
data class FeedEntity(
|
||||
@ColumnInfo(name = STREAM_ID)
|
||||
var streamId: Long,
|
||||
@ColumnInfo(name = STREAM_ID)
|
||||
var streamId: Long,
|
||||
|
||||
@ColumnInfo(name = SUBSCRIPTION_ID)
|
||||
var subscriptionId: Long
|
||||
@ColumnInfo(name = SUBSCRIPTION_ID)
|
||||
var subscriptionId: Long
|
||||
) {
|
||||
|
||||
companion object {
|
||||
@@ -40,4 +43,4 @@ data class FeedEntity(
|
||||
const val STREAM_ID = "stream_id"
|
||||
const val SUBSCRIPTION_ID = "subscription_id"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,22 +9,22 @@ import org.schabi.newpipe.database.feed.model.FeedGroupEntity.Companion.SORT_ORD
|
||||
import org.schabi.newpipe.local.subscription.FeedGroupIcon
|
||||
|
||||
@Entity(
|
||||
tableName = FEED_GROUP_TABLE,
|
||||
indices = [Index(SORT_ORDER)]
|
||||
tableName = FEED_GROUP_TABLE,
|
||||
indices = [Index(SORT_ORDER)]
|
||||
)
|
||||
data class FeedGroupEntity(
|
||||
@PrimaryKey(autoGenerate = true)
|
||||
@ColumnInfo(name = ID)
|
||||
val uid: Long,
|
||||
@PrimaryKey(autoGenerate = true)
|
||||
@ColumnInfo(name = ID)
|
||||
val uid: Long,
|
||||
|
||||
@ColumnInfo(name = NAME)
|
||||
var name: String,
|
||||
@ColumnInfo(name = NAME)
|
||||
var name: String,
|
||||
|
||||
@ColumnInfo(name = ICON)
|
||||
var icon: FeedGroupIcon,
|
||||
@ColumnInfo(name = ICON)
|
||||
var icon: FeedGroupIcon,
|
||||
|
||||
@ColumnInfo(name = SORT_ORDER)
|
||||
var sortOrder: Long = -1
|
||||
@ColumnInfo(name = SORT_ORDER)
|
||||
var sortOrder: Long = -1
|
||||
) {
|
||||
companion object {
|
||||
const val FEED_GROUP_TABLE = "feed_group"
|
||||
@@ -36,4 +36,4 @@ data class FeedGroupEntity(
|
||||
|
||||
const val GROUP_ALL_ID = -1L
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,29 +11,31 @@ import org.schabi.newpipe.database.feed.model.FeedGroupSubscriptionEntity.Compan
|
||||
import org.schabi.newpipe.database.subscription.SubscriptionEntity
|
||||
|
||||
@Entity(
|
||||
tableName = FEED_GROUP_SUBSCRIPTION_TABLE,
|
||||
primaryKeys = [GROUP_ID, SUBSCRIPTION_ID],
|
||||
indices = [Index(SUBSCRIPTION_ID)],
|
||||
foreignKeys = [
|
||||
ForeignKey(
|
||||
entity = FeedGroupEntity::class,
|
||||
parentColumns = [FeedGroupEntity.ID],
|
||||
childColumns = [GROUP_ID],
|
||||
onDelete = CASCADE, onUpdate = CASCADE, deferred = true),
|
||||
tableName = FEED_GROUP_SUBSCRIPTION_TABLE,
|
||||
primaryKeys = [GROUP_ID, SUBSCRIPTION_ID],
|
||||
indices = [Index(SUBSCRIPTION_ID)],
|
||||
foreignKeys = [
|
||||
ForeignKey(
|
||||
entity = FeedGroupEntity::class,
|
||||
parentColumns = [FeedGroupEntity.ID],
|
||||
childColumns = [GROUP_ID],
|
||||
onDelete = CASCADE, onUpdate = CASCADE, deferred = true
|
||||
),
|
||||
|
||||
ForeignKey(
|
||||
entity = SubscriptionEntity::class,
|
||||
parentColumns = [SubscriptionEntity.SUBSCRIPTION_UID],
|
||||
childColumns = [SUBSCRIPTION_ID],
|
||||
onDelete = CASCADE, onUpdate = CASCADE, deferred = true)
|
||||
]
|
||||
ForeignKey(
|
||||
entity = SubscriptionEntity::class,
|
||||
parentColumns = [SubscriptionEntity.SUBSCRIPTION_UID],
|
||||
childColumns = [SUBSCRIPTION_ID],
|
||||
onDelete = CASCADE, onUpdate = CASCADE, deferred = true
|
||||
)
|
||||
]
|
||||
)
|
||||
data class FeedGroupSubscriptionEntity(
|
||||
@ColumnInfo(name = GROUP_ID)
|
||||
var feedGroupId: Long,
|
||||
@ColumnInfo(name = GROUP_ID)
|
||||
var feedGroupId: Long,
|
||||
|
||||
@ColumnInfo(name = SUBSCRIPTION_ID)
|
||||
var subscriptionId: Long
|
||||
@ColumnInfo(name = SUBSCRIPTION_ID)
|
||||
var subscriptionId: Long
|
||||
) {
|
||||
|
||||
companion object {
|
||||
@@ -42,4 +44,4 @@ data class FeedGroupSubscriptionEntity(
|
||||
const val GROUP_ID = "group_id"
|
||||
const val SUBSCRIPTION_ID = "subscription_id"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,31 +7,31 @@ import androidx.room.PrimaryKey
|
||||
import org.schabi.newpipe.database.feed.model.FeedLastUpdatedEntity.Companion.FEED_LAST_UPDATED_TABLE
|
||||
import org.schabi.newpipe.database.feed.model.FeedLastUpdatedEntity.Companion.SUBSCRIPTION_ID
|
||||
import org.schabi.newpipe.database.subscription.SubscriptionEntity
|
||||
import java.util.*
|
||||
import java.time.OffsetDateTime
|
||||
|
||||
@Entity(
|
||||
tableName = FEED_LAST_UPDATED_TABLE,
|
||||
foreignKeys = [
|
||||
ForeignKey(
|
||||
entity = SubscriptionEntity::class,
|
||||
parentColumns = [SubscriptionEntity.SUBSCRIPTION_UID],
|
||||
childColumns = [SUBSCRIPTION_ID],
|
||||
onDelete = ForeignKey.CASCADE, onUpdate = ForeignKey.CASCADE, deferred = true)
|
||||
]
|
||||
tableName = FEED_LAST_UPDATED_TABLE,
|
||||
foreignKeys = [
|
||||
ForeignKey(
|
||||
entity = SubscriptionEntity::class,
|
||||
parentColumns = [SubscriptionEntity.SUBSCRIPTION_UID],
|
||||
childColumns = [SUBSCRIPTION_ID],
|
||||
onDelete = ForeignKey.CASCADE, onUpdate = ForeignKey.CASCADE, deferred = true
|
||||
)
|
||||
]
|
||||
)
|
||||
data class FeedLastUpdatedEntity(
|
||||
@PrimaryKey
|
||||
@ColumnInfo(name = SUBSCRIPTION_ID)
|
||||
var subscriptionId: Long,
|
||||
@PrimaryKey
|
||||
@ColumnInfo(name = SUBSCRIPTION_ID)
|
||||
var subscriptionId: Long,
|
||||
|
||||
@ColumnInfo(name = LAST_UPDATED)
|
||||
var lastUpdated: Date? = null
|
||||
@ColumnInfo(name = LAST_UPDATED)
|
||||
var lastUpdated: OffsetDateTime? = null
|
||||
) {
|
||||
|
||||
companion object {
|
||||
const val FEED_LAST_UPDATED_TABLE = "feed_last_updated"
|
||||
|
||||
const val SUBSCRIPTION_ID = "subscription_id"
|
||||
const val LAST_UPDATED = "last_updated"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
package org.schabi.newpipe.database.history.dao;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.room.Dao;
|
||||
import androidx.room.Query;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import org.schabi.newpipe.database.history.model.SearchHistoryEntry;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import io.reactivex.Flowable;
|
||||
import io.reactivex.rxjava3.core.Flowable;
|
||||
|
||||
import static org.schabi.newpipe.database.history.model.SearchHistoryEntry.CREATION_DATE;
|
||||
import static org.schabi.newpipe.database.history.model.SearchHistoryEntry.ID;
|
||||
@@ -18,11 +18,10 @@ import static org.schabi.newpipe.database.history.model.SearchHistoryEntry.TABLE
|
||||
|
||||
@Dao
|
||||
public interface SearchHistoryDAO extends HistoryDAO<SearchHistoryEntry> {
|
||||
|
||||
String ORDER_BY_CREATION_DATE = " ORDER BY " + CREATION_DATE + " DESC";
|
||||
|
||||
@Query("SELECT * FROM " + TABLE_NAME +
|
||||
" WHERE " + ID + " = (SELECT MAX(" + ID + ") FROM " + TABLE_NAME + ")")
|
||||
@Query("SELECT * FROM " + TABLE_NAME
|
||||
+ " WHERE " + ID + " = (SELECT MAX(" + ID + ") FROM " + TABLE_NAME + ")")
|
||||
@Nullable
|
||||
SearchHistoryEntry getLatestEntry();
|
||||
|
||||
@@ -37,13 +36,16 @@ public interface SearchHistoryDAO extends HistoryDAO<SearchHistoryEntry> {
|
||||
@Override
|
||||
Flowable<List<SearchHistoryEntry>> getAll();
|
||||
|
||||
@Query("SELECT * FROM " + TABLE_NAME + " GROUP BY " + SEARCH + ORDER_BY_CREATION_DATE + " LIMIT :limit")
|
||||
@Query("SELECT * FROM " + TABLE_NAME + " GROUP BY " + SEARCH + ORDER_BY_CREATION_DATE
|
||||
+ " LIMIT :limit")
|
||||
Flowable<List<SearchHistoryEntry>> getUniqueEntries(int limit);
|
||||
|
||||
@Query("SELECT * FROM " + TABLE_NAME + " WHERE " + SERVICE_ID + " = :serviceId" + ORDER_BY_CREATION_DATE)
|
||||
@Query("SELECT * FROM " + TABLE_NAME
|
||||
+ " WHERE " + SERVICE_ID + " = :serviceId" + ORDER_BY_CREATION_DATE)
|
||||
@Override
|
||||
Flowable<List<SearchHistoryEntry>> listByService(int serviceId);
|
||||
|
||||
@Query("SELECT * FROM " + TABLE_NAME + " WHERE " + SEARCH + " LIKE :query || '%' GROUP BY " + SEARCH + " LIMIT :limit")
|
||||
@Query("SELECT * FROM " + TABLE_NAME + " WHERE " + SEARCH + " LIKE :query || '%'"
|
||||
+ " GROUP BY " + SEARCH + " LIMIT :limit")
|
||||
Flowable<List<SearchHistoryEntry>> getSimilarEntries(String query, int limit);
|
||||
}
|
||||
|
||||
@@ -1,32 +1,34 @@
|
||||
package org.schabi.newpipe.database.history.dao;
|
||||
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.room.Dao;
|
||||
import androidx.room.Query;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import org.schabi.newpipe.database.history.model.StreamHistoryEntity;
|
||||
import org.schabi.newpipe.database.history.model.StreamHistoryEntry;
|
||||
import org.schabi.newpipe.database.stream.StreamStatisticsEntry;
|
||||
import org.schabi.newpipe.database.history.model.StreamHistoryEntity;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import io.reactivex.Flowable;
|
||||
import io.reactivex.rxjava3.core.Flowable;
|
||||
|
||||
import static org.schabi.newpipe.database.history.model.StreamHistoryEntity.JOIN_STREAM_ID;
|
||||
import static org.schabi.newpipe.database.history.model.StreamHistoryEntity.STREAM_ACCESS_DATE;
|
||||
import static org.schabi.newpipe.database.history.model.StreamHistoryEntity.STREAM_HISTORY_TABLE;
|
||||
import static org.schabi.newpipe.database.history.model.StreamHistoryEntity.STREAM_REPEAT_COUNT;
|
||||
import static org.schabi.newpipe.database.stream.StreamStatisticsEntry.STREAM_LATEST_DATE;
|
||||
import static org.schabi.newpipe.database.stream.StreamStatisticsEntry.STREAM_WATCH_COUNT;
|
||||
import static org.schabi.newpipe.database.stream.model.StreamEntity.STREAM_ID;
|
||||
import static org.schabi.newpipe.database.stream.model.StreamEntity.STREAM_TABLE;
|
||||
import static org.schabi.newpipe.database.history.model.StreamHistoryEntity.JOIN_STREAM_ID;
|
||||
import static org.schabi.newpipe.database.history.model.StreamHistoryEntity.STREAM_ACCESS_DATE;
|
||||
import static org.schabi.newpipe.database.history.model.StreamHistoryEntity.STREAM_HISTORY_TABLE;
|
||||
import static org.schabi.newpipe.database.stream.model.StreamStateEntity.JOIN_STREAM_ID_ALIAS;
|
||||
import static org.schabi.newpipe.database.stream.model.StreamStateEntity.STREAM_PROGRESS_TIME;
|
||||
import static org.schabi.newpipe.database.stream.model.StreamStateEntity.STREAM_STATE_TABLE;
|
||||
|
||||
@Dao
|
||||
public abstract class StreamHistoryDAO implements HistoryDAO<StreamHistoryEntity> {
|
||||
@Query("SELECT * FROM " + STREAM_HISTORY_TABLE +
|
||||
" WHERE " + STREAM_ACCESS_DATE + " = " +
|
||||
"(SELECT MAX(" + STREAM_ACCESS_DATE + ") FROM " + STREAM_HISTORY_TABLE + ")")
|
||||
@Query("SELECT * FROM " + STREAM_HISTORY_TABLE
|
||||
+ " WHERE " + STREAM_ACCESS_DATE + " = "
|
||||
+ "(SELECT MAX(" + STREAM_ACCESS_DATE + ") FROM " + STREAM_HISTORY_TABLE + ")")
|
||||
@Override
|
||||
@Nullable
|
||||
public abstract StreamHistoryEntity getLatestEntry();
|
||||
@@ -40,33 +42,46 @@ public abstract class StreamHistoryDAO implements HistoryDAO<StreamHistoryEntity
|
||||
public abstract int deleteAll();
|
||||
|
||||
@Override
|
||||
public Flowable<List<StreamHistoryEntity>> listByService(int serviceId) {
|
||||
public Flowable<List<StreamHistoryEntity>> listByService(final int serviceId) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Query("SELECT * FROM " + STREAM_TABLE +
|
||||
" INNER JOIN " + STREAM_HISTORY_TABLE +
|
||||
" ON " + STREAM_ID + " = " + JOIN_STREAM_ID +
|
||||
" ORDER BY " + STREAM_ACCESS_DATE + " DESC")
|
||||
@Query("SELECT * FROM " + STREAM_TABLE
|
||||
+ " INNER JOIN " + STREAM_HISTORY_TABLE
|
||||
+ " ON " + STREAM_ID + " = " + JOIN_STREAM_ID
|
||||
+ " ORDER BY " + STREAM_ACCESS_DATE + " DESC")
|
||||
public abstract Flowable<List<StreamHistoryEntry>> getHistory();
|
||||
|
||||
@Query("SELECT * FROM " + STREAM_HISTORY_TABLE + " WHERE " + JOIN_STREAM_ID +
|
||||
" = :streamId ORDER BY " + STREAM_ACCESS_DATE + " DESC LIMIT 1")
|
||||
|
||||
@Query("SELECT * FROM " + STREAM_TABLE
|
||||
+ " INNER JOIN " + STREAM_HISTORY_TABLE
|
||||
+ " ON " + STREAM_ID + " = " + JOIN_STREAM_ID
|
||||
+ " ORDER BY " + STREAM_ID + " ASC")
|
||||
public abstract Flowable<List<StreamHistoryEntry>> getHistorySortedById();
|
||||
|
||||
@Query("SELECT * FROM " + STREAM_HISTORY_TABLE + " WHERE " + JOIN_STREAM_ID
|
||||
+ " = :streamId ORDER BY " + STREAM_ACCESS_DATE + " DESC LIMIT 1")
|
||||
@Nullable
|
||||
public abstract StreamHistoryEntity getLatestEntry(final long streamId);
|
||||
public abstract StreamHistoryEntity getLatestEntry(long streamId);
|
||||
|
||||
@Query("DELETE FROM " + STREAM_HISTORY_TABLE + " WHERE " + JOIN_STREAM_ID + " = :streamId")
|
||||
public abstract int deleteStreamHistory(final long streamId);
|
||||
public abstract int deleteStreamHistory(long streamId);
|
||||
|
||||
@Query("SELECT * FROM " + STREAM_TABLE +
|
||||
@Query("SELECT * FROM " + STREAM_TABLE
|
||||
|
||||
// Select the latest entry and watch count for each stream id on history table
|
||||
" INNER JOIN " +
|
||||
"(SELECT " + JOIN_STREAM_ID + ", " +
|
||||
" MAX(" + STREAM_ACCESS_DATE + ") AS " + STREAM_LATEST_DATE + ", " +
|
||||
" SUM(" + STREAM_REPEAT_COUNT + ") AS " + STREAM_WATCH_COUNT +
|
||||
" FROM " + STREAM_HISTORY_TABLE + " GROUP BY " + JOIN_STREAM_ID + ")" +
|
||||
+ " INNER JOIN "
|
||||
+ "(SELECT " + JOIN_STREAM_ID + ", "
|
||||
+ " MAX(" + STREAM_ACCESS_DATE + ") AS " + STREAM_LATEST_DATE + ", "
|
||||
+ " SUM(" + STREAM_REPEAT_COUNT + ") AS " + STREAM_WATCH_COUNT
|
||||
+ " FROM " + STREAM_HISTORY_TABLE + " GROUP BY " + JOIN_STREAM_ID + ")"
|
||||
|
||||
" ON " + STREAM_ID + " = " + JOIN_STREAM_ID)
|
||||
+ " ON " + STREAM_ID + " = " + JOIN_STREAM_ID
|
||||
|
||||
+ " LEFT JOIN "
|
||||
+ "(SELECT " + JOIN_STREAM_ID + " AS " + JOIN_STREAM_ID_ALIAS + ", "
|
||||
+ STREAM_PROGRESS_TIME
|
||||
+ " FROM " + STREAM_STATE_TABLE + " )"
|
||||
+ " ON " + STREAM_ID + " = " + JOIN_STREAM_ID_ALIAS)
|
||||
public abstract Flowable<List<StreamStatisticsEntry>> getStatistics();
|
||||
}
|
||||
|
||||
@@ -6,14 +6,13 @@ import androidx.room.Ignore;
|
||||
import androidx.room.Index;
|
||||
import androidx.room.PrimaryKey;
|
||||
|
||||
import java.util.Date;
|
||||
import java.time.OffsetDateTime;
|
||||
|
||||
import static org.schabi.newpipe.database.history.model.SearchHistoryEntry.SEARCH;
|
||||
|
||||
@Entity(tableName = SearchHistoryEntry.TABLE_NAME,
|
||||
indices = {@Index(value = SEARCH)})
|
||||
public class SearchHistoryEntry {
|
||||
|
||||
public static final String ID = "id";
|
||||
public static final String TABLE_NAME = "search_history";
|
||||
public static final String SERVICE_ID = "service_id";
|
||||
@@ -25,7 +24,7 @@ public class SearchHistoryEntry {
|
||||
private long id;
|
||||
|
||||
@ColumnInfo(name = CREATION_DATE)
|
||||
private Date creationDate;
|
||||
private OffsetDateTime creationDate;
|
||||
|
||||
@ColumnInfo(name = SERVICE_ID)
|
||||
private int serviceId;
|
||||
@@ -33,7 +32,8 @@ public class SearchHistoryEntry {
|
||||
@ColumnInfo(name = SEARCH)
|
||||
private String search;
|
||||
|
||||
public SearchHistoryEntry(Date creationDate, int serviceId, String search) {
|
||||
public SearchHistoryEntry(final OffsetDateTime creationDate, final int serviceId,
|
||||
final String search) {
|
||||
this.serviceId = serviceId;
|
||||
this.creationDate = creationDate;
|
||||
this.search = search;
|
||||
@@ -43,15 +43,15 @@ public class SearchHistoryEntry {
|
||||
return id;
|
||||
}
|
||||
|
||||
public void setId(long id) {
|
||||
public void setId(final long id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
public Date getCreationDate() {
|
||||
public OffsetDateTime getCreationDate() {
|
||||
return creationDate;
|
||||
}
|
||||
|
||||
public void setCreationDate(Date creationDate) {
|
||||
public void setCreationDate(final OffsetDateTime creationDate) {
|
||||
this.creationDate = creationDate;
|
||||
}
|
||||
|
||||
@@ -59,7 +59,7 @@ public class SearchHistoryEntry {
|
||||
return serviceId;
|
||||
}
|
||||
|
||||
public void setServiceId(int serviceId) {
|
||||
public void setServiceId(final int serviceId) {
|
||||
this.serviceId = serviceId;
|
||||
}
|
||||
|
||||
@@ -67,13 +67,13 @@ public class SearchHistoryEntry {
|
||||
return search;
|
||||
}
|
||||
|
||||
public void setSearch(String search) {
|
||||
public void setSearch(final String search) {
|
||||
this.search = search;
|
||||
}
|
||||
|
||||
@Ignore
|
||||
public boolean hasEqualValues(SearchHistoryEntry otherEntry) {
|
||||
return getServiceId() == otherEntry.getServiceId() &&
|
||||
getSearch().equals(otherEntry.getSearch());
|
||||
public boolean hasEqualValues(final SearchHistoryEntry otherEntry) {
|
||||
return getServiceId() == otherEntry.getServiceId()
|
||||
&& getSearch().equals(otherEntry.getSearch());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,20 +1,20 @@
|
||||
package org.schabi.newpipe.database.history.model;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.room.ColumnInfo;
|
||||
import androidx.room.Entity;
|
||||
import androidx.room.ForeignKey;
|
||||
import androidx.room.Ignore;
|
||||
import androidx.room.Index;
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import org.schabi.newpipe.database.stream.model.StreamEntity;
|
||||
|
||||
import java.util.Date;
|
||||
import java.time.OffsetDateTime;
|
||||
|
||||
import static androidx.room.ForeignKey.CASCADE;
|
||||
import static org.schabi.newpipe.database.history.model.StreamHistoryEntity.STREAM_HISTORY_TABLE;
|
||||
import static org.schabi.newpipe.database.history.model.StreamHistoryEntity.JOIN_STREAM_ID;
|
||||
import static org.schabi.newpipe.database.history.model.StreamHistoryEntity.STREAM_ACCESS_DATE;
|
||||
import static org.schabi.newpipe.database.history.model.StreamHistoryEntity.STREAM_HISTORY_TABLE;
|
||||
|
||||
@Entity(tableName = STREAM_HISTORY_TABLE,
|
||||
primaryKeys = {JOIN_STREAM_ID, STREAM_ACCESS_DATE},
|
||||
@@ -27,29 +27,30 @@ import static org.schabi.newpipe.database.history.model.StreamHistoryEntity.STRE
|
||||
onDelete = CASCADE, onUpdate = CASCADE)
|
||||
})
|
||||
public class StreamHistoryEntity {
|
||||
final public static String STREAM_HISTORY_TABLE = "stream_history";
|
||||
final public static String JOIN_STREAM_ID = "stream_id";
|
||||
final public static String STREAM_ACCESS_DATE = "access_date";
|
||||
final public static String STREAM_REPEAT_COUNT = "repeat_count";
|
||||
public static final String STREAM_HISTORY_TABLE = "stream_history";
|
||||
public static final String JOIN_STREAM_ID = "stream_id";
|
||||
public static final String STREAM_ACCESS_DATE = "access_date";
|
||||
public static final String STREAM_REPEAT_COUNT = "repeat_count";
|
||||
|
||||
@ColumnInfo(name = JOIN_STREAM_ID)
|
||||
private long streamUid;
|
||||
|
||||
@NonNull
|
||||
@ColumnInfo(name = STREAM_ACCESS_DATE)
|
||||
private Date accessDate;
|
||||
private OffsetDateTime accessDate;
|
||||
|
||||
@ColumnInfo(name = STREAM_REPEAT_COUNT)
|
||||
private long repeatCount;
|
||||
|
||||
public StreamHistoryEntity(long streamUid, @NonNull Date accessDate, long repeatCount) {
|
||||
public StreamHistoryEntity(final long streamUid, @NonNull final OffsetDateTime accessDate,
|
||||
final long repeatCount) {
|
||||
this.streamUid = streamUid;
|
||||
this.accessDate = accessDate;
|
||||
this.repeatCount = repeatCount;
|
||||
}
|
||||
|
||||
@Ignore
|
||||
public StreamHistoryEntity(long streamUid, @NonNull Date accessDate) {
|
||||
public StreamHistoryEntity(final long streamUid, @NonNull final OffsetDateTime accessDate) {
|
||||
this(streamUid, accessDate, 1);
|
||||
}
|
||||
|
||||
@@ -57,15 +58,16 @@ public class StreamHistoryEntity {
|
||||
return streamUid;
|
||||
}
|
||||
|
||||
public void setStreamUid(long streamUid) {
|
||||
public void setStreamUid(final long streamUid) {
|
||||
this.streamUid = streamUid;
|
||||
}
|
||||
|
||||
public Date getAccessDate() {
|
||||
@NonNull
|
||||
public OffsetDateTime getAccessDate() {
|
||||
return accessDate;
|
||||
}
|
||||
|
||||
public void setAccessDate(@NonNull Date accessDate) {
|
||||
public void setAccessDate(@NonNull final OffsetDateTime accessDate) {
|
||||
this.accessDate = accessDate;
|
||||
}
|
||||
|
||||
@@ -73,7 +75,7 @@ public class StreamHistoryEntity {
|
||||
return repeatCount;
|
||||
}
|
||||
|
||||
public void setRepeatCount(long repeatCount) {
|
||||
public void setRepeatCount(final long repeatCount) {
|
||||
this.repeatCount = repeatCount;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,20 +3,20 @@ package org.schabi.newpipe.database.history.model
|
||||
import androidx.room.ColumnInfo
|
||||
import androidx.room.Embedded
|
||||
import org.schabi.newpipe.database.stream.model.StreamEntity
|
||||
import java.util.*
|
||||
import java.time.OffsetDateTime
|
||||
|
||||
data class StreamHistoryEntry(
|
||||
@Embedded
|
||||
val streamEntity: StreamEntity,
|
||||
@Embedded
|
||||
val streamEntity: StreamEntity,
|
||||
|
||||
@ColumnInfo(name = StreamHistoryEntity.JOIN_STREAM_ID)
|
||||
val streamId: Long,
|
||||
@ColumnInfo(name = StreamHistoryEntity.JOIN_STREAM_ID)
|
||||
val streamId: Long,
|
||||
|
||||
@ColumnInfo(name = StreamHistoryEntity.STREAM_ACCESS_DATE)
|
||||
val accessDate: Date,
|
||||
@ColumnInfo(name = StreamHistoryEntity.STREAM_ACCESS_DATE)
|
||||
val accessDate: OffsetDateTime,
|
||||
|
||||
@ColumnInfo(name = StreamHistoryEntity.STREAM_REPEAT_COUNT)
|
||||
val repeatCount: Long
|
||||
@ColumnInfo(name = StreamHistoryEntity.STREAM_REPEAT_COUNT)
|
||||
val repeatCount: Long
|
||||
) {
|
||||
|
||||
fun toStreamHistoryEntity(): StreamHistoryEntity {
|
||||
@@ -25,6 +25,6 @@ data class StreamHistoryEntry(
|
||||
|
||||
fun hasEqualValues(other: StreamHistoryEntry): Boolean {
|
||||
return this.streamEntity.uid == other.streamEntity.uid && streamId == other.streamId &&
|
||||
accessDate.compareTo(other.accessDate) == 0
|
||||
accessDate.isEqual(other.accessDate)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,27 @@
|
||||
package org.schabi.newpipe.database.playlist;
|
||||
|
||||
import org.schabi.newpipe.database.LocalItem;
|
||||
import org.schabi.newpipe.database.playlist.model.PlaylistRemoteEntity;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
|
||||
public interface PlaylistLocalItem extends LocalItem {
|
||||
String getOrderingName();
|
||||
|
||||
static List<PlaylistLocalItem> merge(
|
||||
final List<PlaylistMetadataEntry> localPlaylists,
|
||||
final List<PlaylistRemoteEntity> remotePlaylists) {
|
||||
final List<PlaylistLocalItem> items = new ArrayList<>(
|
||||
localPlaylists.size() + remotePlaylists.size());
|
||||
items.addAll(localPlaylists);
|
||||
items.addAll(remotePlaylists);
|
||||
|
||||
Collections.sort(items, Comparator.comparing(PlaylistLocalItem::getOrderingName,
|
||||
Comparator.nullsLast(String.CASE_INSENSITIVE_ORDER)));
|
||||
|
||||
return items;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,18 +7,19 @@ import static org.schabi.newpipe.database.playlist.model.PlaylistEntity.PLAYLIST
|
||||
import static org.schabi.newpipe.database.playlist.model.PlaylistEntity.PLAYLIST_THUMBNAIL_URL;
|
||||
|
||||
public class PlaylistMetadataEntry implements PlaylistLocalItem {
|
||||
final public static String PLAYLIST_STREAM_COUNT = "streamCount";
|
||||
public static final String PLAYLIST_STREAM_COUNT = "streamCount";
|
||||
|
||||
@ColumnInfo(name = PLAYLIST_ID)
|
||||
final public long uid;
|
||||
public final long uid;
|
||||
@ColumnInfo(name = PLAYLIST_NAME)
|
||||
final public String name;
|
||||
public final String name;
|
||||
@ColumnInfo(name = PLAYLIST_THUMBNAIL_URL)
|
||||
final public String thumbnailUrl;
|
||||
public final String thumbnailUrl;
|
||||
@ColumnInfo(name = PLAYLIST_STREAM_COUNT)
|
||||
final public long streamCount;
|
||||
public final long streamCount;
|
||||
|
||||
public PlaylistMetadataEntry(long uid, String name, String thumbnailUrl, long streamCount) {
|
||||
public PlaylistMetadataEntry(final long uid, final String name, final String thumbnailUrl,
|
||||
final long streamCount) {
|
||||
this.uid = uid;
|
||||
this.name = name;
|
||||
this.thumbnailUrl = thumbnailUrl;
|
||||
|
||||
@@ -5,17 +5,22 @@ import androidx.room.Embedded
|
||||
import org.schabi.newpipe.database.LocalItem
|
||||
import org.schabi.newpipe.database.playlist.model.PlaylistStreamEntity
|
||||
import org.schabi.newpipe.database.stream.model.StreamEntity
|
||||
import org.schabi.newpipe.database.stream.model.StreamStateEntity
|
||||
import org.schabi.newpipe.extractor.stream.StreamInfoItem
|
||||
import kotlin.jvm.Throws
|
||||
|
||||
class PlaylistStreamEntry(
|
||||
@Embedded
|
||||
val streamEntity: StreamEntity,
|
||||
@Embedded
|
||||
val streamEntity: StreamEntity,
|
||||
|
||||
@ColumnInfo(name = PlaylistStreamEntity.JOIN_STREAM_ID)
|
||||
val streamId: Long,
|
||||
@ColumnInfo(name = StreamStateEntity.STREAM_PROGRESS_TIME, defaultValue = "0")
|
||||
val progressTime: Long,
|
||||
|
||||
@ColumnInfo(name = PlaylistStreamEntity.JOIN_INDEX)
|
||||
val joinIndex: Int
|
||||
@ColumnInfo(name = PlaylistStreamEntity.JOIN_STREAM_ID)
|
||||
val streamId: Long,
|
||||
|
||||
@ColumnInfo(name = PlaylistStreamEntity.JOIN_INDEX)
|
||||
val joinIndex: Int
|
||||
) : LocalItem {
|
||||
|
||||
@Throws(IllegalArgumentException::class)
|
||||
|
||||
@@ -8,7 +8,7 @@ import org.schabi.newpipe.database.playlist.model.PlaylistEntity;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import io.reactivex.Flowable;
|
||||
import io.reactivex.rxjava3.core.Flowable;
|
||||
|
||||
import static org.schabi.newpipe.database.playlist.model.PlaylistEntity.PLAYLIST_ID;
|
||||
import static org.schabi.newpipe.database.playlist.model.PlaylistEntity.PLAYLIST_TABLE;
|
||||
@@ -24,13 +24,16 @@ public abstract class PlaylistDAO implements BasicDAO<PlaylistEntity> {
|
||||
public abstract int deleteAll();
|
||||
|
||||
@Override
|
||||
public Flowable<List<PlaylistEntity>> listByService(int serviceId) {
|
||||
public Flowable<List<PlaylistEntity>> listByService(final int serviceId) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Query("SELECT * FROM " + PLAYLIST_TABLE + " WHERE " + PLAYLIST_ID + " = :playlistId")
|
||||
public abstract Flowable<List<PlaylistEntity>> getPlaylist(final long playlistId);
|
||||
public abstract Flowable<List<PlaylistEntity>> getPlaylist(long playlistId);
|
||||
|
||||
@Query("DELETE FROM " + PLAYLIST_TABLE + " WHERE " + PLAYLIST_ID + " = :playlistId")
|
||||
public abstract int deletePlaylist(final long playlistId);
|
||||
public abstract int deletePlaylist(long playlistId);
|
||||
|
||||
@Query("SELECT COUNT(*) FROM " + PLAYLIST_TABLE)
|
||||
public abstract Flowable<Long> getCount();
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ import org.schabi.newpipe.database.playlist.model.PlaylistRemoteEntity;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import io.reactivex.Flowable;
|
||||
import io.reactivex.rxjava3.core.Flowable;
|
||||
|
||||
import static org.schabi.newpipe.database.playlist.model.PlaylistRemoteEntity.REMOTE_PLAYLIST_ID;
|
||||
import static org.schabi.newpipe.database.playlist.model.PlaylistRemoteEntity.REMOTE_PLAYLIST_SERVICE_ID;
|
||||
@@ -27,22 +27,21 @@ public abstract class PlaylistRemoteDAO implements BasicDAO<PlaylistRemoteEntity
|
||||
public abstract int deleteAll();
|
||||
|
||||
@Override
|
||||
@Query("SELECT * FROM " + REMOTE_PLAYLIST_TABLE +
|
||||
" WHERE " + REMOTE_PLAYLIST_SERVICE_ID + " = :serviceId")
|
||||
@Query("SELECT * FROM " + REMOTE_PLAYLIST_TABLE
|
||||
+ " WHERE " + REMOTE_PLAYLIST_SERVICE_ID + " = :serviceId")
|
||||
public abstract Flowable<List<PlaylistRemoteEntity>> listByService(int serviceId);
|
||||
|
||||
@Query("SELECT * FROM " + REMOTE_PLAYLIST_TABLE + " WHERE " +
|
||||
REMOTE_PLAYLIST_URL + " = :url AND " +
|
||||
REMOTE_PLAYLIST_SERVICE_ID + " = :serviceId")
|
||||
@Query("SELECT * FROM " + REMOTE_PLAYLIST_TABLE + " WHERE "
|
||||
+ REMOTE_PLAYLIST_URL + " = :url AND " + REMOTE_PLAYLIST_SERVICE_ID + " = :serviceId")
|
||||
public abstract Flowable<List<PlaylistRemoteEntity>> getPlaylist(long serviceId, String url);
|
||||
|
||||
@Query("SELECT " + REMOTE_PLAYLIST_ID + " FROM " + REMOTE_PLAYLIST_TABLE +
|
||||
" WHERE " +
|
||||
REMOTE_PLAYLIST_URL + " = :url AND " + REMOTE_PLAYLIST_SERVICE_ID + " = :serviceId")
|
||||
@Query("SELECT " + REMOTE_PLAYLIST_ID + " FROM " + REMOTE_PLAYLIST_TABLE
|
||||
+ " WHERE " + REMOTE_PLAYLIST_URL + " = :url "
|
||||
+ "AND " + REMOTE_PLAYLIST_SERVICE_ID + " = :serviceId")
|
||||
abstract Long getPlaylistIdInternal(long serviceId, String url);
|
||||
|
||||
@Transaction
|
||||
public long upsert(PlaylistRemoteEntity playlist) {
|
||||
public long upsert(final PlaylistRemoteEntity playlist) {
|
||||
final Long playlistId = getPlaylistIdInternal(playlist.getServiceId(), playlist.getUrl());
|
||||
|
||||
if (playlistId == null) {
|
||||
@@ -54,7 +53,7 @@ public abstract class PlaylistRemoteDAO implements BasicDAO<PlaylistRemoteEntity
|
||||
}
|
||||
}
|
||||
|
||||
@Query("DELETE FROM " + REMOTE_PLAYLIST_TABLE +
|
||||
" WHERE " + REMOTE_PLAYLIST_ID + " = :playlistId")
|
||||
public abstract int deletePlaylist(final long playlistId);
|
||||
@Query("DELETE FROM " + REMOTE_PLAYLIST_TABLE
|
||||
+ " WHERE " + REMOTE_PLAYLIST_ID + " = :playlistId")
|
||||
public abstract int deletePlaylist(long playlistId);
|
||||
}
|
||||
|
||||
@@ -11,12 +11,22 @@ import org.schabi.newpipe.database.playlist.model.PlaylistStreamEntity;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import io.reactivex.Flowable;
|
||||
import io.reactivex.rxjava3.core.Flowable;
|
||||
|
||||
import static org.schabi.newpipe.database.playlist.PlaylistMetadataEntry.PLAYLIST_STREAM_COUNT;
|
||||
import static org.schabi.newpipe.database.playlist.model.PlaylistEntity.*;
|
||||
import static org.schabi.newpipe.database.playlist.model.PlaylistStreamEntity.*;
|
||||
import static org.schabi.newpipe.database.stream.model.StreamEntity.*;
|
||||
import static org.schabi.newpipe.database.playlist.model.PlaylistEntity.PLAYLIST_ID;
|
||||
import static org.schabi.newpipe.database.playlist.model.PlaylistEntity.PLAYLIST_NAME;
|
||||
import static org.schabi.newpipe.database.playlist.model.PlaylistEntity.PLAYLIST_TABLE;
|
||||
import static org.schabi.newpipe.database.playlist.model.PlaylistEntity.PLAYLIST_THUMBNAIL_URL;
|
||||
import static org.schabi.newpipe.database.playlist.model.PlaylistStreamEntity.JOIN_INDEX;
|
||||
import static org.schabi.newpipe.database.playlist.model.PlaylistStreamEntity.JOIN_PLAYLIST_ID;
|
||||
import static org.schabi.newpipe.database.playlist.model.PlaylistStreamEntity.JOIN_STREAM_ID;
|
||||
import static org.schabi.newpipe.database.playlist.model.PlaylistStreamEntity.PLAYLIST_STREAM_JOIN_TABLE;
|
||||
import static org.schabi.newpipe.database.stream.model.StreamEntity.STREAM_ID;
|
||||
import static org.schabi.newpipe.database.stream.model.StreamEntity.STREAM_TABLE;
|
||||
import static org.schabi.newpipe.database.stream.model.StreamStateEntity.JOIN_STREAM_ID_ALIAS;
|
||||
import static org.schabi.newpipe.database.stream.model.StreamStateEntity.STREAM_PROGRESS_TIME;
|
||||
import static org.schabi.newpipe.database.stream.model.StreamStateEntity.STREAM_STATE_TABLE;
|
||||
|
||||
@Dao
|
||||
public abstract class PlaylistStreamDAO implements BasicDAO<PlaylistStreamEntity> {
|
||||
@@ -29,40 +39,46 @@ public abstract class PlaylistStreamDAO implements BasicDAO<PlaylistStreamEntity
|
||||
public abstract int deleteAll();
|
||||
|
||||
@Override
|
||||
public Flowable<List<PlaylistStreamEntity>> listByService(int serviceId) {
|
||||
public Flowable<List<PlaylistStreamEntity>> listByService(final int serviceId) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Query("DELETE FROM " + PLAYLIST_STREAM_JOIN_TABLE +
|
||||
" WHERE " + JOIN_PLAYLIST_ID + " = :playlistId")
|
||||
public abstract void deleteBatch(final long playlistId);
|
||||
@Query("DELETE FROM " + PLAYLIST_STREAM_JOIN_TABLE
|
||||
+ " WHERE " + JOIN_PLAYLIST_ID + " = :playlistId")
|
||||
public abstract void deleteBatch(long playlistId);
|
||||
|
||||
@Query("SELECT COALESCE(MAX(" + JOIN_INDEX + "), -1)" +
|
||||
" FROM " + PLAYLIST_STREAM_JOIN_TABLE +
|
||||
" WHERE " + JOIN_PLAYLIST_ID + " = :playlistId")
|
||||
public abstract Flowable<Integer> getMaximumIndexOf(final long playlistId);
|
||||
@Query("SELECT COALESCE(MAX(" + JOIN_INDEX + "), -1)"
|
||||
+ " FROM " + PLAYLIST_STREAM_JOIN_TABLE
|
||||
+ " WHERE " + JOIN_PLAYLIST_ID + " = :playlistId")
|
||||
public abstract Flowable<Integer> getMaximumIndexOf(long playlistId);
|
||||
|
||||
@Transaction
|
||||
@Query("SELECT * FROM " + STREAM_TABLE + " INNER JOIN " +
|
||||
@Query("SELECT * FROM " + STREAM_TABLE + " INNER JOIN "
|
||||
// get ids of streams of the given playlist
|
||||
"(SELECT " + JOIN_STREAM_ID + "," + JOIN_INDEX +
|
||||
" FROM " + PLAYLIST_STREAM_JOIN_TABLE +
|
||||
" WHERE " + JOIN_PLAYLIST_ID + " = :playlistId)" +
|
||||
+ "(SELECT " + JOIN_STREAM_ID + "," + JOIN_INDEX
|
||||
+ " FROM " + PLAYLIST_STREAM_JOIN_TABLE
|
||||
+ " WHERE " + JOIN_PLAYLIST_ID + " = :playlistId)"
|
||||
|
||||
// then merge with the stream metadata
|
||||
" ON " + STREAM_ID + " = " + JOIN_STREAM_ID +
|
||||
" ORDER BY " + JOIN_INDEX + " ASC")
|
||||
+ " ON " + STREAM_ID + " = " + JOIN_STREAM_ID
|
||||
|
||||
+ " LEFT JOIN "
|
||||
+ "(SELECT " + JOIN_STREAM_ID + " AS " + JOIN_STREAM_ID_ALIAS + ", "
|
||||
+ STREAM_PROGRESS_TIME
|
||||
+ " FROM " + STREAM_STATE_TABLE + " )"
|
||||
+ " ON " + STREAM_ID + " = " + JOIN_STREAM_ID_ALIAS
|
||||
|
||||
+ " ORDER BY " + JOIN_INDEX + " ASC")
|
||||
public abstract Flowable<List<PlaylistStreamEntry>> getOrderedStreamsOf(long playlistId);
|
||||
|
||||
@Transaction
|
||||
@Query("SELECT " + PLAYLIST_ID + ", " + PLAYLIST_NAME + ", " +
|
||||
PLAYLIST_THUMBNAIL_URL + ", " +
|
||||
"COALESCE(COUNT(" + JOIN_PLAYLIST_ID + "), 0) AS " + PLAYLIST_STREAM_COUNT +
|
||||
@Query("SELECT " + PLAYLIST_ID + ", " + PLAYLIST_NAME + ", " + PLAYLIST_THUMBNAIL_URL + ", "
|
||||
+ "COALESCE(COUNT(" + JOIN_PLAYLIST_ID + "), 0) AS " + PLAYLIST_STREAM_COUNT
|
||||
|
||||
" FROM " + PLAYLIST_TABLE +
|
||||
" LEFT JOIN " + PLAYLIST_STREAM_JOIN_TABLE +
|
||||
" ON " + PLAYLIST_ID + " = " + JOIN_PLAYLIST_ID +
|
||||
" GROUP BY " + JOIN_PLAYLIST_ID +
|
||||
" ORDER BY " + PLAYLIST_NAME + " COLLATE NOCASE ASC")
|
||||
+ " FROM " + PLAYLIST_TABLE
|
||||
+ " LEFT JOIN " + PLAYLIST_STREAM_JOIN_TABLE
|
||||
+ " ON " + PLAYLIST_ID + " = " + JOIN_PLAYLIST_ID
|
||||
+ " GROUP BY " + JOIN_PLAYLIST_ID
|
||||
+ " ORDER BY " + PLAYLIST_NAME + " COLLATE NOCASE ASC")
|
||||
public abstract Flowable<List<PlaylistMetadataEntry>> getPlaylistMetadata();
|
||||
}
|
||||
|
||||
@@ -11,10 +11,10 @@ import static org.schabi.newpipe.database.playlist.model.PlaylistEntity.PLAYLIST
|
||||
@Entity(tableName = PLAYLIST_TABLE,
|
||||
indices = {@Index(value = {PLAYLIST_NAME})})
|
||||
public class PlaylistEntity {
|
||||
final public static String PLAYLIST_TABLE = "playlists";
|
||||
final public static String PLAYLIST_ID = "uid";
|
||||
final public static String PLAYLIST_NAME = "name";
|
||||
final public static String PLAYLIST_THUMBNAIL_URL = "thumbnail_url";
|
||||
public static final String PLAYLIST_TABLE = "playlists";
|
||||
public static final String PLAYLIST_ID = "uid";
|
||||
public static final String PLAYLIST_NAME = "name";
|
||||
public static final String PLAYLIST_THUMBNAIL_URL = "thumbnail_url";
|
||||
|
||||
@PrimaryKey(autoGenerate = true)
|
||||
@ColumnInfo(name = PLAYLIST_ID)
|
||||
@@ -26,7 +26,7 @@ public class PlaylistEntity {
|
||||
@ColumnInfo(name = PLAYLIST_THUMBNAIL_URL)
|
||||
private String thumbnailUrl;
|
||||
|
||||
public PlaylistEntity(String name, String thumbnailUrl) {
|
||||
public PlaylistEntity(final String name, final String thumbnailUrl) {
|
||||
this.name = name;
|
||||
this.thumbnailUrl = thumbnailUrl;
|
||||
}
|
||||
@@ -35,7 +35,7 @@ public class PlaylistEntity {
|
||||
return uid;
|
||||
}
|
||||
|
||||
public void setUid(long uid) {
|
||||
public void setUid(final long uid) {
|
||||
this.uid = uid;
|
||||
}
|
||||
|
||||
@@ -43,7 +43,7 @@ public class PlaylistEntity {
|
||||
return name;
|
||||
}
|
||||
|
||||
public void setName(String name) {
|
||||
public void setName(final String name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
@@ -51,7 +51,7 @@ public class PlaylistEntity {
|
||||
return thumbnailUrl;
|
||||
}
|
||||
|
||||
public void setThumbnailUrl(String thumbnailUrl) {
|
||||
public void setThumbnailUrl(final String thumbnailUrl) {
|
||||
this.thumbnailUrl = thumbnailUrl;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,14 +24,14 @@ import static org.schabi.newpipe.database.playlist.model.PlaylistRemoteEntity.RE
|
||||
@Index(value = {REMOTE_PLAYLIST_SERVICE_ID, REMOTE_PLAYLIST_URL}, unique = true)
|
||||
})
|
||||
public class PlaylistRemoteEntity implements PlaylistLocalItem {
|
||||
final public static String REMOTE_PLAYLIST_TABLE = "remote_playlists";
|
||||
final public static String REMOTE_PLAYLIST_ID = "uid";
|
||||
final public static String REMOTE_PLAYLIST_SERVICE_ID = "service_id";
|
||||
final public static String REMOTE_PLAYLIST_NAME = "name";
|
||||
final public static String REMOTE_PLAYLIST_URL = "url";
|
||||
final public static String REMOTE_PLAYLIST_THUMBNAIL_URL = "thumbnail_url";
|
||||
final public static String REMOTE_PLAYLIST_UPLOADER_NAME = "uploader";
|
||||
final public static String REMOTE_PLAYLIST_STREAM_COUNT = "stream_count";
|
||||
public static final String REMOTE_PLAYLIST_TABLE = "remote_playlists";
|
||||
public static final String REMOTE_PLAYLIST_ID = "uid";
|
||||
public static final String REMOTE_PLAYLIST_SERVICE_ID = "service_id";
|
||||
public static final String REMOTE_PLAYLIST_NAME = "name";
|
||||
public static final String REMOTE_PLAYLIST_URL = "url";
|
||||
public static final String REMOTE_PLAYLIST_THUMBNAIL_URL = "thumbnail_url";
|
||||
public static final String REMOTE_PLAYLIST_UPLOADER_NAME = "uploader";
|
||||
public static final String REMOTE_PLAYLIST_STREAM_COUNT = "stream_count";
|
||||
|
||||
@PrimaryKey(autoGenerate = true)
|
||||
@ColumnInfo(name = REMOTE_PLAYLIST_ID)
|
||||
@@ -55,8 +55,9 @@ public class PlaylistRemoteEntity implements PlaylistLocalItem {
|
||||
@ColumnInfo(name = REMOTE_PLAYLIST_STREAM_COUNT)
|
||||
private Long streamCount;
|
||||
|
||||
public PlaylistRemoteEntity(int serviceId, String name, String url, String thumbnailUrl,
|
||||
String uploader, Long streamCount) {
|
||||
public PlaylistRemoteEntity(final int serviceId, final String name, final String url,
|
||||
final String thumbnailUrl, final String uploader,
|
||||
final Long streamCount) {
|
||||
this.serviceId = serviceId;
|
||||
this.name = name;
|
||||
this.url = url;
|
||||
@@ -68,7 +69,8 @@ public class PlaylistRemoteEntity implements PlaylistLocalItem {
|
||||
@Ignore
|
||||
public PlaylistRemoteEntity(final PlaylistInfo info) {
|
||||
this(info.getServiceId(), info.getName(), info.getUrl(),
|
||||
info.getThumbnailUrl() == null ? info.getUploaderAvatarUrl() : info.getThumbnailUrl(),
|
||||
info.getThumbnailUrl() == null
|
||||
? info.getUploaderAvatarUrl() : info.getThumbnailUrl(),
|
||||
info.getUploaderName(), info.getStreamCount());
|
||||
}
|
||||
|
||||
@@ -90,7 +92,7 @@ public class PlaylistRemoteEntity implements PlaylistLocalItem {
|
||||
return uid;
|
||||
}
|
||||
|
||||
public void setUid(long uid) {
|
||||
public void setUid(final long uid) {
|
||||
this.uid = uid;
|
||||
}
|
||||
|
||||
@@ -98,7 +100,7 @@ public class PlaylistRemoteEntity implements PlaylistLocalItem {
|
||||
return serviceId;
|
||||
}
|
||||
|
||||
public void setServiceId(int serviceId) {
|
||||
public void setServiceId(final int serviceId) {
|
||||
this.serviceId = serviceId;
|
||||
}
|
||||
|
||||
@@ -106,7 +108,7 @@ public class PlaylistRemoteEntity implements PlaylistLocalItem {
|
||||
return name;
|
||||
}
|
||||
|
||||
public void setName(String name) {
|
||||
public void setName(final String name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
@@ -114,7 +116,7 @@ public class PlaylistRemoteEntity implements PlaylistLocalItem {
|
||||
return thumbnailUrl;
|
||||
}
|
||||
|
||||
public void setThumbnailUrl(String thumbnailUrl) {
|
||||
public void setThumbnailUrl(final String thumbnailUrl) {
|
||||
this.thumbnailUrl = thumbnailUrl;
|
||||
}
|
||||
|
||||
@@ -122,7 +124,7 @@ public class PlaylistRemoteEntity implements PlaylistLocalItem {
|
||||
return url;
|
||||
}
|
||||
|
||||
public void setUrl(String url) {
|
||||
public void setUrl(final String url) {
|
||||
this.url = url;
|
||||
}
|
||||
|
||||
@@ -130,7 +132,7 @@ public class PlaylistRemoteEntity implements PlaylistLocalItem {
|
||||
return uploader;
|
||||
}
|
||||
|
||||
public void setUploader(String uploader) {
|
||||
public void setUploader(final String uploader) {
|
||||
this.uploader = uploader;
|
||||
}
|
||||
|
||||
@@ -138,7 +140,7 @@ public class PlaylistRemoteEntity implements PlaylistLocalItem {
|
||||
return streamCount;
|
||||
}
|
||||
|
||||
public void setStreamCount(Long streamCount) {
|
||||
public void setStreamCount(final Long streamCount) {
|
||||
this.streamCount = streamCount;
|
||||
}
|
||||
|
||||
|
||||
@@ -30,11 +30,10 @@ import static org.schabi.newpipe.database.playlist.model.PlaylistStreamEntity.PL
|
||||
onDelete = CASCADE, onUpdate = CASCADE, deferred = true)
|
||||
})
|
||||
public class PlaylistStreamEntity {
|
||||
|
||||
final public static String PLAYLIST_STREAM_JOIN_TABLE = "playlist_stream_join";
|
||||
final public static String JOIN_PLAYLIST_ID = "playlist_id";
|
||||
final public static String JOIN_STREAM_ID = "stream_id";
|
||||
final public static String JOIN_INDEX = "join_index";
|
||||
public static final String PLAYLIST_STREAM_JOIN_TABLE = "playlist_stream_join";
|
||||
public static final String JOIN_PLAYLIST_ID = "playlist_id";
|
||||
public static final String JOIN_STREAM_ID = "stream_id";
|
||||
public static final String JOIN_INDEX = "join_index";
|
||||
|
||||
@ColumnInfo(name = JOIN_PLAYLIST_ID)
|
||||
private long playlistUid;
|
||||
@@ -55,23 +54,23 @@ public class PlaylistStreamEntity {
|
||||
return playlistUid;
|
||||
}
|
||||
|
||||
public void setPlaylistUid(final long playlistUid) {
|
||||
this.playlistUid = playlistUid;
|
||||
}
|
||||
|
||||
public long getStreamUid() {
|
||||
return streamUid;
|
||||
}
|
||||
|
||||
public void setStreamUid(final long streamUid) {
|
||||
this.streamUid = streamUid;
|
||||
}
|
||||
|
||||
public int getIndex() {
|
||||
return index;
|
||||
}
|
||||
|
||||
public void setPlaylistUid(long playlistUid) {
|
||||
this.playlistUid = playlistUid;
|
||||
}
|
||||
|
||||
public void setStreamUid(long streamUid) {
|
||||
this.streamUid = streamUid;
|
||||
}
|
||||
|
||||
public void setIndex(int index) {
|
||||
public void setIndex(final int index) {
|
||||
this.index = index;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,23 +5,26 @@ import androidx.room.Embedded
|
||||
import org.schabi.newpipe.database.LocalItem
|
||||
import org.schabi.newpipe.database.history.model.StreamHistoryEntity
|
||||
import org.schabi.newpipe.database.stream.model.StreamEntity
|
||||
import org.schabi.newpipe.database.stream.model.StreamStateEntity.STREAM_PROGRESS_TIME
|
||||
import org.schabi.newpipe.extractor.stream.StreamInfoItem
|
||||
import java.util.*
|
||||
import java.time.OffsetDateTime
|
||||
|
||||
class StreamStatisticsEntry(
|
||||
@Embedded
|
||||
val streamEntity: StreamEntity,
|
||||
@Embedded
|
||||
val streamEntity: StreamEntity,
|
||||
|
||||
@ColumnInfo(name = StreamHistoryEntity.JOIN_STREAM_ID)
|
||||
val streamId: Long,
|
||||
@ColumnInfo(name = STREAM_PROGRESS_TIME, defaultValue = "0")
|
||||
val progressTime: Long,
|
||||
|
||||
@ColumnInfo(name = STREAM_LATEST_DATE)
|
||||
val latestAccessDate: Date,
|
||||
@ColumnInfo(name = StreamHistoryEntity.JOIN_STREAM_ID)
|
||||
val streamId: Long,
|
||||
|
||||
@ColumnInfo(name = STREAM_WATCH_COUNT)
|
||||
val watchCount: Long
|
||||
@ColumnInfo(name = STREAM_LATEST_DATE)
|
||||
val latestAccessDate: OffsetDateTime,
|
||||
|
||||
@ColumnInfo(name = STREAM_WATCH_COUNT)
|
||||
val watchCount: Long
|
||||
) : LocalItem {
|
||||
|
||||
fun toStreamInfoItem(): StreamInfoItem {
|
||||
val item = StreamInfoItem(streamEntity.serviceId, streamEntity.url, streamEntity.title, streamEntity.streamType)
|
||||
item.duration = streamEntity.duration
|
||||
|
||||
@@ -1,15 +1,19 @@
|
||||
package org.schabi.newpipe.database.stream.dao
|
||||
|
||||
import androidx.room.*
|
||||
import io.reactivex.Flowable
|
||||
import androidx.room.ColumnInfo
|
||||
import androidx.room.Dao
|
||||
import androidx.room.Insert
|
||||
import androidx.room.OnConflictStrategy
|
||||
import androidx.room.Query
|
||||
import androidx.room.Transaction
|
||||
import io.reactivex.rxjava3.core.Flowable
|
||||
import org.schabi.newpipe.database.BasicDAO
|
||||
import org.schabi.newpipe.database.stream.model.StreamEntity
|
||||
import org.schabi.newpipe.database.stream.model.StreamEntity.Companion.STREAM_ID
|
||||
import org.schabi.newpipe.extractor.stream.StreamType
|
||||
import org.schabi.newpipe.extractor.stream.StreamType.AUDIO_LIVE_STREAM
|
||||
import org.schabi.newpipe.extractor.stream.StreamType.LIVE_STREAM
|
||||
import java.util.*
|
||||
import kotlin.collections.ArrayList
|
||||
import java.time.OffsetDateTime
|
||||
|
||||
@Dao
|
||||
abstract class StreamDAO : BasicDAO<StreamEntity> {
|
||||
@@ -31,10 +35,12 @@ abstract class StreamDAO : BasicDAO<StreamEntity> {
|
||||
@Insert(onConflict = OnConflictStrategy.IGNORE)
|
||||
internal abstract fun silentInsertAllInternal(streams: List<StreamEntity>): List<Long>
|
||||
|
||||
@Query("""
|
||||
@Query(
|
||||
"""
|
||||
SELECT uid, stream_type, textual_upload_date, upload_date, is_upload_date_approximation, duration
|
||||
FROM streams WHERE url = :url AND service_id = :serviceId
|
||||
""")
|
||||
"""
|
||||
)
|
||||
internal abstract fun getMinimalStreamForCompare(serviceId: Int, url: String): StreamCompareFeed?
|
||||
|
||||
@Transaction
|
||||
@@ -75,12 +81,17 @@ abstract class StreamDAO : BasicDAO<StreamEntity> {
|
||||
|
||||
private fun compareAndUpdateStream(newerStream: StreamEntity) {
|
||||
val existentMinimalStream = getMinimalStreamForCompare(newerStream.serviceId, newerStream.url)
|
||||
?: throw IllegalStateException("Stream cannot be null just after insertion.")
|
||||
?: throw IllegalStateException("Stream cannot be null just after insertion.")
|
||||
newerStream.uid = existentMinimalStream.uid
|
||||
|
||||
val isNewerStreamLive = newerStream.streamType == AUDIO_LIVE_STREAM || newerStream.streamType == LIVE_STREAM
|
||||
if (!isNewerStreamLive) {
|
||||
if (existentMinimalStream.uploadDate != null && existentMinimalStream.isUploadDateApproximation != true) {
|
||||
|
||||
// Use the existent upload date if the newer stream does not have a better precision
|
||||
// (i.e. is an approximation). This is done to prevent unnecessary changes.
|
||||
val hasBetterPrecision =
|
||||
newerStream.uploadDate != null && newerStream.isUploadDateApproximation != true
|
||||
if (existentMinimalStream.uploadDate != null && !hasBetterPrecision) {
|
||||
newerStream.uploadDate = existentMinimalStream.uploadDate
|
||||
newerStream.textualUploadDate = existentMinimalStream.textualUploadDate
|
||||
newerStream.isUploadDateApproximation = existentMinimalStream.isUploadDateApproximation
|
||||
@@ -89,11 +100,11 @@ abstract class StreamDAO : BasicDAO<StreamEntity> {
|
||||
if (existentMinimalStream.duration > 0 && newerStream.duration < 0) {
|
||||
newerStream.duration = existentMinimalStream.duration
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@Query("""
|
||||
@Query(
|
||||
"""
|
||||
DELETE FROM streams WHERE
|
||||
|
||||
NOT EXISTS (SELECT 1 FROM stream_history sh
|
||||
@@ -104,28 +115,30 @@ abstract class StreamDAO : BasicDAO<StreamEntity> {
|
||||
|
||||
AND NOT EXISTS (SELECT 1 FROM feed f
|
||||
WHERE f.stream_id = streams.uid)
|
||||
""")
|
||||
"""
|
||||
)
|
||||
abstract fun deleteOrphans(): Int
|
||||
|
||||
/**
|
||||
* Minimal entry class used when comparing/updating an existent stream.
|
||||
*/
|
||||
internal data class StreamCompareFeed(
|
||||
@ColumnInfo(name = STREAM_ID)
|
||||
var uid: Long = 0,
|
||||
@ColumnInfo(name = STREAM_ID)
|
||||
var uid: Long = 0,
|
||||
|
||||
@ColumnInfo(name = StreamEntity.STREAM_TYPE)
|
||||
var streamType: StreamType,
|
||||
@ColumnInfo(name = StreamEntity.STREAM_TYPE)
|
||||
var streamType: StreamType,
|
||||
|
||||
@ColumnInfo(name = StreamEntity.STREAM_TEXTUAL_UPLOAD_DATE)
|
||||
var textualUploadDate: String? = null,
|
||||
@ColumnInfo(name = StreamEntity.STREAM_TEXTUAL_UPLOAD_DATE)
|
||||
var textualUploadDate: String? = null,
|
||||
|
||||
@ColumnInfo(name = StreamEntity.STREAM_UPLOAD_DATE)
|
||||
var uploadDate: Date? = null,
|
||||
@ColumnInfo(name = StreamEntity.STREAM_UPLOAD_DATE)
|
||||
var uploadDate: OffsetDateTime? = null,
|
||||
|
||||
@ColumnInfo(name = StreamEntity.STREAM_IS_UPLOAD_DATE_APPROXIMATION)
|
||||
var isUploadDateApproximation: Boolean? = null,
|
||||
@ColumnInfo(name = StreamEntity.STREAM_IS_UPLOAD_DATE_APPROXIMATION)
|
||||
var isUploadDateApproximation: Boolean? = null,
|
||||
|
||||
@ColumnInfo(name = StreamEntity.STREAM_DURATION)
|
||||
var duration: Long)
|
||||
@ColumnInfo(name = StreamEntity.STREAM_DURATION)
|
||||
var duration: Long
|
||||
)
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ import org.schabi.newpipe.database.stream.model.StreamStateEntity;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import io.reactivex.Flowable;
|
||||
import io.reactivex.rxjava3.core.Flowable;
|
||||
|
||||
import static org.schabi.newpipe.database.stream.model.StreamStateEntity.JOIN_STREAM_ID;
|
||||
import static org.schabi.newpipe.database.stream.model.StreamStateEntity.STREAM_STATE_TABLE;
|
||||
@@ -27,21 +27,21 @@ public abstract class StreamStateDAO implements BasicDAO<StreamStateEntity> {
|
||||
public abstract int deleteAll();
|
||||
|
||||
@Override
|
||||
public Flowable<List<StreamStateEntity>> listByService(int serviceId) {
|
||||
public Flowable<List<StreamStateEntity>> listByService(final int serviceId) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Query("SELECT * FROM " + STREAM_STATE_TABLE + " WHERE " + JOIN_STREAM_ID + " = :streamId")
|
||||
public abstract Flowable<List<StreamStateEntity>> getState(final long streamId);
|
||||
public abstract Flowable<List<StreamStateEntity>> getState(long streamId);
|
||||
|
||||
@Query("DELETE FROM " + STREAM_STATE_TABLE + " WHERE " + JOIN_STREAM_ID + " = :streamId")
|
||||
public abstract int deleteState(final long streamId);
|
||||
public abstract int deleteState(long streamId);
|
||||
|
||||
@Insert(onConflict = OnConflictStrategy.IGNORE)
|
||||
abstract void silentInsertInternal(final StreamStateEntity streamState);
|
||||
abstract void silentInsertInternal(StreamStateEntity streamState);
|
||||
|
||||
@Transaction
|
||||
public long upsert(StreamStateEntity stream) {
|
||||
public long upsert(final StreamStateEntity stream) {
|
||||
silentInsertInternal(stream);
|
||||
return update(stream);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
package org.schabi.newpipe.database.stream.model
|
||||
|
||||
import androidx.room.*
|
||||
import androidx.room.ColumnInfo
|
||||
import androidx.room.Entity
|
||||
import androidx.room.Ignore
|
||||
import androidx.room.Index
|
||||
import androidx.room.PrimaryKey
|
||||
import org.schabi.newpipe.database.stream.model.StreamEntity.Companion.STREAM_SERVICE_ID
|
||||
import org.schabi.newpipe.database.stream.model.StreamEntity.Companion.STREAM_TABLE
|
||||
import org.schabi.newpipe.database.stream.model.StreamEntity.Companion.STREAM_URL
|
||||
@@ -10,75 +14,75 @@ import org.schabi.newpipe.extractor.stream.StreamInfoItem
|
||||
import org.schabi.newpipe.extractor.stream.StreamType
|
||||
import org.schabi.newpipe.player.playqueue.PlayQueueItem
|
||||
import java.io.Serializable
|
||||
import java.util.*
|
||||
import java.time.OffsetDateTime
|
||||
|
||||
@Entity(tableName = STREAM_TABLE,
|
||||
indices = [
|
||||
Index(value = [STREAM_SERVICE_ID, STREAM_URL], unique = true)
|
||||
]
|
||||
@Entity(
|
||||
tableName = STREAM_TABLE,
|
||||
indices = [
|
||||
Index(value = [STREAM_SERVICE_ID, STREAM_URL], unique = true)
|
||||
]
|
||||
)
|
||||
data class StreamEntity(
|
||||
@PrimaryKey(autoGenerate = true)
|
||||
@ColumnInfo(name = STREAM_ID)
|
||||
var uid: Long = 0,
|
||||
@PrimaryKey(autoGenerate = true)
|
||||
@ColumnInfo(name = STREAM_ID)
|
||||
var uid: Long = 0,
|
||||
|
||||
@ColumnInfo(name = STREAM_SERVICE_ID)
|
||||
var serviceId: Int,
|
||||
@ColumnInfo(name = STREAM_SERVICE_ID)
|
||||
var serviceId: Int,
|
||||
|
||||
@ColumnInfo(name = STREAM_URL)
|
||||
var url: String,
|
||||
@ColumnInfo(name = STREAM_URL)
|
||||
var url: String,
|
||||
|
||||
@ColumnInfo(name = STREAM_TITLE)
|
||||
var title: String,
|
||||
@ColumnInfo(name = STREAM_TITLE)
|
||||
var title: String,
|
||||
|
||||
@ColumnInfo(name = STREAM_TYPE)
|
||||
var streamType: StreamType,
|
||||
@ColumnInfo(name = STREAM_TYPE)
|
||||
var streamType: StreamType,
|
||||
|
||||
@ColumnInfo(name = STREAM_DURATION)
|
||||
var duration: Long,
|
||||
@ColumnInfo(name = STREAM_DURATION)
|
||||
var duration: Long,
|
||||
|
||||
@ColumnInfo(name = STREAM_UPLOADER)
|
||||
var uploader: String,
|
||||
@ColumnInfo(name = STREAM_UPLOADER)
|
||||
var uploader: String,
|
||||
|
||||
@ColumnInfo(name = STREAM_THUMBNAIL_URL)
|
||||
var thumbnailUrl: String? = null,
|
||||
@ColumnInfo(name = STREAM_THUMBNAIL_URL)
|
||||
var thumbnailUrl: String? = null,
|
||||
|
||||
@ColumnInfo(name = STREAM_VIEWS)
|
||||
var viewCount: Long? = null,
|
||||
@ColumnInfo(name = STREAM_VIEWS)
|
||||
var viewCount: Long? = null,
|
||||
|
||||
@ColumnInfo(name = STREAM_TEXTUAL_UPLOAD_DATE)
|
||||
var textualUploadDate: String? = null,
|
||||
@ColumnInfo(name = STREAM_TEXTUAL_UPLOAD_DATE)
|
||||
var textualUploadDate: String? = null,
|
||||
|
||||
@ColumnInfo(name = STREAM_UPLOAD_DATE)
|
||||
var uploadDate: Date? = null,
|
||||
@ColumnInfo(name = STREAM_UPLOAD_DATE)
|
||||
var uploadDate: OffsetDateTime? = null,
|
||||
|
||||
@ColumnInfo(name = STREAM_IS_UPLOAD_DATE_APPROXIMATION)
|
||||
var isUploadDateApproximation: Boolean? = null
|
||||
@ColumnInfo(name = STREAM_IS_UPLOAD_DATE_APPROXIMATION)
|
||||
var isUploadDateApproximation: Boolean? = null
|
||||
) : Serializable {
|
||||
|
||||
@Ignore
|
||||
constructor(item: StreamInfoItem) : this(
|
||||
serviceId = item.serviceId, url = item.url, title = item.name,
|
||||
streamType = item.streamType, duration = item.duration, uploader = item.uploaderName,
|
||||
thumbnailUrl = item.thumbnailUrl, viewCount = item.viewCount,
|
||||
textualUploadDate = item.textualUploadDate, uploadDate = item.uploadDate?.date()?.time,
|
||||
isUploadDateApproximation = item.uploadDate?.isApproximation
|
||||
serviceId = item.serviceId, url = item.url, title = item.name,
|
||||
streamType = item.streamType, duration = item.duration, uploader = item.uploaderName,
|
||||
thumbnailUrl = item.thumbnailUrl, viewCount = item.viewCount,
|
||||
textualUploadDate = item.textualUploadDate, uploadDate = item.uploadDate?.offsetDateTime(),
|
||||
isUploadDateApproximation = item.uploadDate?.isApproximation
|
||||
)
|
||||
|
||||
@Ignore
|
||||
constructor(info: StreamInfo) : this(
|
||||
serviceId = info.serviceId, url = info.url, title = info.name,
|
||||
streamType = info.streamType, duration = info.duration, uploader = info.uploaderName,
|
||||
thumbnailUrl = info.thumbnailUrl, viewCount = info.viewCount,
|
||||
textualUploadDate = info.textualUploadDate, uploadDate = info.uploadDate?.date()?.time,
|
||||
isUploadDateApproximation = info.uploadDate?.isApproximation
|
||||
serviceId = info.serviceId, url = info.url, title = info.name,
|
||||
streamType = info.streamType, duration = info.duration, uploader = info.uploaderName,
|
||||
thumbnailUrl = info.thumbnailUrl, viewCount = info.viewCount,
|
||||
textualUploadDate = info.textualUploadDate, uploadDate = info.uploadDate?.offsetDateTime(),
|
||||
isUploadDateApproximation = info.uploadDate?.isApproximation
|
||||
)
|
||||
|
||||
@Ignore
|
||||
constructor(item: PlayQueueItem) : this(
|
||||
serviceId = item.serviceId, url = item.url, title = item.title,
|
||||
streamType = item.streamType, duration = item.duration, uploader = item.uploader,
|
||||
thumbnailUrl = item.thumbnailUrl
|
||||
serviceId = item.serviceId, url = item.url, title = item.title,
|
||||
streamType = item.streamType, duration = item.duration, uploader = item.uploader,
|
||||
thumbnailUrl = item.thumbnailUrl
|
||||
)
|
||||
|
||||
fun toStreamInfoItem(): StreamInfoItem {
|
||||
@@ -90,7 +94,7 @@ data class StreamEntity(
|
||||
if (viewCount != null) item.viewCount = viewCount as Long
|
||||
item.textualUploadDate = textualUploadDate
|
||||
item.uploadDate = uploadDate?.let {
|
||||
DateWrapper(Calendar.getInstance().apply { time = it }, isUploadDateApproximation ?: false)
|
||||
DateWrapper(it, isUploadDateApproximation ?: false)
|
||||
}
|
||||
|
||||
return item
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
package org.schabi.newpipe.database.stream.model;
|
||||
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.room.ColumnInfo;
|
||||
import androidx.room.Entity;
|
||||
import androidx.room.ForeignKey;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
@@ -21,14 +20,20 @@ import static org.schabi.newpipe.database.stream.model.StreamStateEntity.STREAM_
|
||||
onDelete = CASCADE, onUpdate = CASCADE)
|
||||
})
|
||||
public class StreamStateEntity {
|
||||
final public static String STREAM_STATE_TABLE = "stream_state";
|
||||
final public static String JOIN_STREAM_ID = "stream_id";
|
||||
final public static String STREAM_PROGRESS_TIME = "progress_time";
|
||||
public static final String STREAM_STATE_TABLE = "stream_state";
|
||||
public static final String JOIN_STREAM_ID = "stream_id";
|
||||
// This additional field is required for the SQL query because 'stream_id' is used
|
||||
// for some other joins already
|
||||
public static final String JOIN_STREAM_ID_ALIAS = "stream_id_alias";
|
||||
public static final String STREAM_PROGRESS_TIME = "progress_time";
|
||||
|
||||
|
||||
/** Playback state will not be saved, if playback time less than this threshold */
|
||||
/**
|
||||
* Playback state will not be saved, if playback time is less than this threshold.
|
||||
*/
|
||||
private static final int PLAYBACK_SAVE_THRESHOLD_START_SECONDS = 5;
|
||||
/** Playback state will not be saved, if time left less than this threshold */
|
||||
/**
|
||||
* Playback state will not be saved, if time left is less than this threshold.
|
||||
*/
|
||||
private static final int PLAYBACK_SAVE_THRESHOLD_END_SECONDS = 10;
|
||||
|
||||
@ColumnInfo(name = JOIN_STREAM_ID)
|
||||
@@ -37,7 +42,7 @@ public class StreamStateEntity {
|
||||
@ColumnInfo(name = STREAM_PROGRESS_TIME)
|
||||
private long progressTime;
|
||||
|
||||
public StreamStateEntity(long streamUid, long progressTime) {
|
||||
public StreamStateEntity(final long streamUid, final long progressTime) {
|
||||
this.streamUid = streamUid;
|
||||
this.progressTime = progressTime;
|
||||
}
|
||||
@@ -46,7 +51,7 @@ public class StreamStateEntity {
|
||||
return streamUid;
|
||||
}
|
||||
|
||||
public void setStreamUid(long streamUid) {
|
||||
public void setStreamUid(final long streamUid) {
|
||||
this.streamUid = streamUid;
|
||||
}
|
||||
|
||||
@@ -54,21 +59,23 @@ public class StreamStateEntity {
|
||||
return progressTime;
|
||||
}
|
||||
|
||||
public void setProgressTime(long progressTime) {
|
||||
public void setProgressTime(final long progressTime) {
|
||||
this.progressTime = progressTime;
|
||||
}
|
||||
|
||||
public boolean isValid(int durationInSeconds) {
|
||||
public boolean isValid(final int durationInSeconds) {
|
||||
final int seconds = (int) TimeUnit.MILLISECONDS.toSeconds(progressTime);
|
||||
return seconds > PLAYBACK_SAVE_THRESHOLD_START_SECONDS
|
||||
&& seconds < durationInSeconds - PLAYBACK_SAVE_THRESHOLD_END_SECONDS;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(@Nullable Object obj) {
|
||||
public boolean equals(@Nullable final Object obj) {
|
||||
if (obj instanceof StreamStateEntity) {
|
||||
return ((StreamStateEntity) obj).streamUid == streamUid
|
||||
&& ((StreamStateEntity) obj).progressTime == progressTime;
|
||||
} else return false;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,12 @@
|
||||
package org.schabi.newpipe.database.subscription
|
||||
|
||||
import androidx.room.*
|
||||
import io.reactivex.Flowable
|
||||
import io.reactivex.Maybe
|
||||
import androidx.room.Dao
|
||||
import androidx.room.Insert
|
||||
import androidx.room.OnConflictStrategy
|
||||
import androidx.room.Query
|
||||
import androidx.room.Transaction
|
||||
import io.reactivex.rxjava3.core.Flowable
|
||||
import io.reactivex.rxjava3.core.Maybe
|
||||
import org.schabi.newpipe.database.BasicDAO
|
||||
|
||||
@Dao
|
||||
@@ -16,6 +20,51 @@ abstract class SubscriptionDAO : BasicDAO<SubscriptionEntity> {
|
||||
@Query("SELECT * FROM subscriptions ORDER BY name COLLATE NOCASE ASC")
|
||||
abstract override fun getAll(): Flowable<List<SubscriptionEntity>>
|
||||
|
||||
@Query(
|
||||
"""
|
||||
SELECT * FROM subscriptions
|
||||
|
||||
WHERE name LIKE '%' || :filter || '%'
|
||||
|
||||
ORDER BY name COLLATE NOCASE ASC
|
||||
"""
|
||||
)
|
||||
abstract fun getSubscriptionsFiltered(filter: String): Flowable<List<SubscriptionEntity>>
|
||||
|
||||
@Query(
|
||||
"""
|
||||
SELECT * FROM subscriptions s
|
||||
|
||||
LEFT JOIN feed_group_subscription_join fgs
|
||||
ON s.uid = fgs.subscription_id
|
||||
|
||||
WHERE (fgs.subscription_id IS NULL OR fgs.group_id = :currentGroupId)
|
||||
|
||||
ORDER BY name COLLATE NOCASE ASC
|
||||
"""
|
||||
)
|
||||
abstract fun getSubscriptionsOnlyUngrouped(
|
||||
currentGroupId: Long
|
||||
): Flowable<List<SubscriptionEntity>>
|
||||
|
||||
@Query(
|
||||
"""
|
||||
SELECT * FROM subscriptions s
|
||||
|
||||
LEFT JOIN feed_group_subscription_join fgs
|
||||
ON s.uid = fgs.subscription_id
|
||||
|
||||
WHERE (fgs.subscription_id IS NULL OR fgs.group_id = :currentGroupId)
|
||||
AND s.name LIKE '%' || :filter || '%'
|
||||
|
||||
ORDER BY name COLLATE NOCASE ASC
|
||||
"""
|
||||
)
|
||||
abstract fun getSubscriptionsOnlyUngroupedFiltered(
|
||||
currentGroupId: Long,
|
||||
filter: String
|
||||
): Flowable<List<SubscriptionEntity>>
|
||||
|
||||
@Query("SELECT * FROM subscriptions WHERE url LIKE :url AND service_id = :serviceId")
|
||||
abstract fun getSubscriptionFlowable(serviceId: Int, url: String): Flowable<List<SubscriptionEntity>>
|
||||
|
||||
@@ -48,7 +97,7 @@ abstract class SubscriptionDAO : BasicDAO<SubscriptionEntity> {
|
||||
entity.uid = uidFromInsert
|
||||
} else {
|
||||
val subscriptionIdFromDb = getSubscriptionIdInternal(entity.serviceId, entity.url)
|
||||
?: throw IllegalStateException("Subscription cannot be null just after insertion.")
|
||||
?: throw IllegalStateException("Subscription cannot be null just after insertion.")
|
||||
entity.uid = subscriptionIdFromDb
|
||||
|
||||
update(entity)
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
package org.schabi.newpipe.database.subscription;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.room.ColumnInfo;
|
||||
import androidx.room.Entity;
|
||||
import androidx.room.Ignore;
|
||||
import androidx.room.Index;
|
||||
import androidx.room.PrimaryKey;
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import org.schabi.newpipe.extractor.channel.ChannelInfo;
|
||||
import org.schabi.newpipe.extractor.channel.ChannelInfoItem;
|
||||
@@ -18,15 +18,14 @@ import static org.schabi.newpipe.database.subscription.SubscriptionEntity.SUBSCR
|
||||
@Entity(tableName = SUBSCRIPTION_TABLE,
|
||||
indices = {@Index(value = {SUBSCRIPTION_SERVICE_ID, SUBSCRIPTION_URL}, unique = true)})
|
||||
public class SubscriptionEntity {
|
||||
|
||||
public static final String SUBSCRIPTION_UID = "uid";
|
||||
public static final String SUBSCRIPTION_TABLE = "subscriptions";
|
||||
public static final String SUBSCRIPTION_SERVICE_ID = "service_id";
|
||||
public static final String SUBSCRIPTION_URL = "url";
|
||||
public static final String SUBSCRIPTION_NAME = "name";
|
||||
public static final String SUBSCRIPTION_AVATAR_URL = "avatar_url";
|
||||
public static final String SUBSCRIPTION_SUBSCRIBER_COUNT = "subscriber_count";
|
||||
public static final String SUBSCRIPTION_DESCRIPTION = "description";
|
||||
public static final String SUBSCRIPTION_UID = "uid";
|
||||
public static final String SUBSCRIPTION_TABLE = "subscriptions";
|
||||
public static final String SUBSCRIPTION_SERVICE_ID = "service_id";
|
||||
public static final String SUBSCRIPTION_URL = "url";
|
||||
public static final String SUBSCRIPTION_NAME = "name";
|
||||
public static final String SUBSCRIPTION_AVATAR_URL = "avatar_url";
|
||||
public static final String SUBSCRIPTION_SUBSCRIBER_COUNT = "subscriber_count";
|
||||
public static final String SUBSCRIPTION_DESCRIPTION = "description";
|
||||
|
||||
@PrimaryKey(autoGenerate = true)
|
||||
private long uid = 0;
|
||||
@@ -49,11 +48,21 @@ public class SubscriptionEntity {
|
||||
@ColumnInfo(name = SUBSCRIPTION_DESCRIPTION)
|
||||
private String description;
|
||||
|
||||
@Ignore
|
||||
public static SubscriptionEntity from(@NonNull final ChannelInfo info) {
|
||||
final SubscriptionEntity result = new SubscriptionEntity();
|
||||
result.setServiceId(info.getServiceId());
|
||||
result.setUrl(info.getUrl());
|
||||
result.setData(info.getName(), info.getAvatarUrl(), info.getDescription(),
|
||||
info.getSubscriberCount());
|
||||
return result;
|
||||
}
|
||||
|
||||
public long getUid() {
|
||||
return uid;
|
||||
}
|
||||
|
||||
public void setUid(long uid) {
|
||||
public void setUid(final long uid) {
|
||||
this.uid = uid;
|
||||
}
|
||||
|
||||
@@ -61,7 +70,7 @@ public class SubscriptionEntity {
|
||||
return serviceId;
|
||||
}
|
||||
|
||||
public void setServiceId(int serviceId) {
|
||||
public void setServiceId(final int serviceId) {
|
||||
this.serviceId = serviceId;
|
||||
}
|
||||
|
||||
@@ -69,7 +78,7 @@ public class SubscriptionEntity {
|
||||
return url;
|
||||
}
|
||||
|
||||
public void setUrl(String url) {
|
||||
public void setUrl(final String url) {
|
||||
this.url = url;
|
||||
}
|
||||
|
||||
@@ -77,7 +86,7 @@ public class SubscriptionEntity {
|
||||
return name;
|
||||
}
|
||||
|
||||
public void setName(String name) {
|
||||
public void setName(final String name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
@@ -85,7 +94,7 @@ public class SubscriptionEntity {
|
||||
return avatarUrl;
|
||||
}
|
||||
|
||||
public void setAvatarUrl(String avatarUrl) {
|
||||
public void setAvatarUrl(final String avatarUrl) {
|
||||
this.avatarUrl = avatarUrl;
|
||||
}
|
||||
|
||||
@@ -93,7 +102,7 @@ public class SubscriptionEntity {
|
||||
return subscriberCount;
|
||||
}
|
||||
|
||||
public void setSubscriberCount(Long subscriberCount) {
|
||||
public void setSubscriberCount(final Long subscriberCount) {
|
||||
this.subscriberCount = subscriberCount;
|
||||
}
|
||||
|
||||
@@ -101,36 +110,75 @@ public class SubscriptionEntity {
|
||||
return description;
|
||||
}
|
||||
|
||||
public void setDescription(String description) {
|
||||
public void setDescription(final String description) {
|
||||
this.description = description;
|
||||
}
|
||||
|
||||
@Ignore
|
||||
public void setData(final String name,
|
||||
final String avatarUrl,
|
||||
final String description,
|
||||
final Long subscriberCount) {
|
||||
this.setName(name);
|
||||
this.setAvatarUrl(avatarUrl);
|
||||
this.setDescription(description);
|
||||
this.setSubscriberCount(subscriberCount);
|
||||
public void setData(final String n, final String au, final String d, final Long sc) {
|
||||
this.setName(n);
|
||||
this.setAvatarUrl(au);
|
||||
this.setDescription(d);
|
||||
this.setSubscriberCount(sc);
|
||||
}
|
||||
|
||||
@Ignore
|
||||
public ChannelInfoItem toChannelInfoItem() {
|
||||
ChannelInfoItem item = new ChannelInfoItem(getServiceId(), getUrl(), getName());
|
||||
final ChannelInfoItem item = new ChannelInfoItem(getServiceId(), getUrl(), getName());
|
||||
item.setThumbnailUrl(getAvatarUrl());
|
||||
item.setSubscriberCount(getSubscriberCount());
|
||||
item.setDescription(getDescription());
|
||||
return item;
|
||||
}
|
||||
|
||||
@Ignore
|
||||
public static SubscriptionEntity from(@NonNull ChannelInfo info) {
|
||||
SubscriptionEntity result = new SubscriptionEntity();
|
||||
result.setServiceId(info.getServiceId());
|
||||
result.setUrl(info.getUrl());
|
||||
result.setData(info.getName(), info.getAvatarUrl(), info.getDescription(), info.getSubscriberCount());
|
||||
|
||||
// TODO: Remove these generated methods by migrating this class to a data class from Kotlin.
|
||||
@Override
|
||||
@SuppressWarnings("EqualsReplaceableByObjectsCall")
|
||||
public boolean equals(final Object o) {
|
||||
if (this == o) {
|
||||
return true;
|
||||
}
|
||||
if (o == null || getClass() != o.getClass()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
final SubscriptionEntity that = (SubscriptionEntity) o;
|
||||
|
||||
if (uid != that.uid) {
|
||||
return false;
|
||||
}
|
||||
if (serviceId != that.serviceId) {
|
||||
return false;
|
||||
}
|
||||
if (!url.equals(that.url)) {
|
||||
return false;
|
||||
}
|
||||
if (name != null ? !name.equals(that.name) : that.name != null) {
|
||||
return false;
|
||||
}
|
||||
if (avatarUrl != null ? !avatarUrl.equals(that.avatarUrl) : that.avatarUrl != null) {
|
||||
return false;
|
||||
}
|
||||
if (subscriberCount != null
|
||||
? !subscriberCount.equals(that.subscriberCount)
|
||||
: that.subscriberCount != null) {
|
||||
return false;
|
||||
}
|
||||
return description != null
|
||||
? description.equals(that.description)
|
||||
: that.description == null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
int result = (int) (uid ^ (uid >>> 32));
|
||||
result = 31 * result + serviceId;
|
||||
result = 31 * result + url.hashCode();
|
||||
result = 31 * result + (name != null ? name.hashCode() : 0);
|
||||
result = 31 * result + (avatarUrl != null ? avatarUrl.hashCode() : 0);
|
||||
result = 31 * result + (subscriberCount != null ? subscriberCount.hashCode() : 0);
|
||||
result = 31 * result + (description != null ? description.hashCode() : 0);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,19 +1,21 @@
|
||||
package org.schabi.newpipe.download;
|
||||
|
||||
import android.app.FragmentTransaction;
|
||||
import android.content.Intent;
|
||||
import android.os.Bundle;
|
||||
import androidx.appcompat.app.ActionBar;
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
import androidx.appcompat.widget.Toolbar;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuInflater;
|
||||
import android.view.MenuItem;
|
||||
import android.view.ViewTreeObserver;
|
||||
|
||||
import androidx.appcompat.app.ActionBar;
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
import androidx.appcompat.widget.Toolbar;
|
||||
import androidx.fragment.app.FragmentTransaction;
|
||||
|
||||
import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.util.NavigationHelper;
|
||||
import org.schabi.newpipe.util.DeviceUtils;
|
||||
import org.schabi.newpipe.util.ThemeHelper;
|
||||
import org.schabi.newpipe.views.FocusOverlayView;
|
||||
|
||||
import us.shandian.giga.service.DownloadManagerService;
|
||||
import us.shandian.giga.ui.fragment.MissionsFragment;
|
||||
@@ -25,9 +27,9 @@ public class DownloadActivity extends AppCompatActivity {
|
||||
private static final String MISSIONS_FRAGMENT_TAG = "fragment_tag";
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
protected void onCreate(final Bundle savedInstanceState) {
|
||||
// Service
|
||||
Intent i = new Intent();
|
||||
final Intent i = new Intent();
|
||||
i.setClass(this, DownloadManagerService.class);
|
||||
startService(i);
|
||||
|
||||
@@ -36,27 +38,32 @@ public class DownloadActivity extends AppCompatActivity {
|
||||
super.onCreate(savedInstanceState);
|
||||
setContentView(R.layout.activity_downloader);
|
||||
|
||||
Toolbar toolbar = findViewById(R.id.toolbar);
|
||||
final Toolbar toolbar = findViewById(R.id.toolbar);
|
||||
setSupportActionBar(toolbar);
|
||||
|
||||
ActionBar actionBar = getSupportActionBar();
|
||||
final ActionBar actionBar = getSupportActionBar();
|
||||
if (actionBar != null) {
|
||||
actionBar.setDisplayHomeAsUpEnabled(true);
|
||||
actionBar.setTitle(R.string.downloads_title);
|
||||
actionBar.setDisplayShowTitleEnabled(true);
|
||||
}
|
||||
|
||||
getWindow().getDecorView().getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
|
||||
getWindow().getDecorView().getViewTreeObserver()
|
||||
.addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
|
||||
@Override
|
||||
public void onGlobalLayout() {
|
||||
updateFragments();
|
||||
getWindow().getDecorView().getViewTreeObserver().removeOnGlobalLayoutListener(this);
|
||||
}
|
||||
});
|
||||
|
||||
if (DeviceUtils.isTv(this)) {
|
||||
FocusOverlayView.setupFocusObserver(this);
|
||||
}
|
||||
}
|
||||
|
||||
private void updateFragments() {
|
||||
MissionsFragment fragment = new MissionsFragment();
|
||||
final MissionsFragment fragment = new MissionsFragment();
|
||||
|
||||
getSupportFragmentManager().beginTransaction()
|
||||
.replace(R.id.frame, fragment, MISSIONS_FRAGMENT_TAG)
|
||||
@@ -65,9 +72,9 @@ public class DownloadActivity extends AppCompatActivity {
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onCreateOptionsMenu(Menu menu) {
|
||||
public boolean onCreateOptionsMenu(final Menu menu) {
|
||||
super.onCreateOptionsMenu(menu);
|
||||
MenuInflater inflater = getMenuInflater();
|
||||
final MenuInflater inflater = getMenuInflater();
|
||||
|
||||
inflater.inflate(R.menu.download_menu, menu);
|
||||
|
||||
@@ -75,7 +82,7 @@ public class DownloadActivity extends AppCompatActivity {
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(MenuItem item) {
|
||||
public boolean onOptionsItemSelected(final MenuItem item) {
|
||||
switch (item.getItemId()) {
|
||||
case android.R.id.home:
|
||||
onBackPressed();
|
||||
|
||||
@@ -10,7 +10,6 @@ import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
import android.os.Environment;
|
||||
import android.os.IBinder;
|
||||
import android.preference.PreferenceManager;
|
||||
import android.util.Log;
|
||||
import android.util.SparseArray;
|
||||
import android.view.LayoutInflater;
|
||||
@@ -34,6 +33,7 @@ import androidx.appcompat.view.menu.ActionMenuItemView;
|
||||
import androidx.appcompat.widget.Toolbar;
|
||||
import androidx.documentfile.provider.DocumentFile;
|
||||
import androidx.fragment.app.DialogFragment;
|
||||
import androidx.preference.PreferenceManager;
|
||||
|
||||
import com.nononsenseapps.filepicker.Utils;
|
||||
|
||||
@@ -49,6 +49,7 @@ 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.report.ErrorActivity;
|
||||
import org.schabi.newpipe.report.ErrorInfo;
|
||||
import org.schabi.newpipe.report.UserAction;
|
||||
import org.schabi.newpipe.settings.NewPipeSettings;
|
||||
import org.schabi.newpipe.util.FilePickerActivityHelper;
|
||||
@@ -69,7 +70,7 @@ import java.util.Locale;
|
||||
|
||||
import icepick.Icepick;
|
||||
import icepick.State;
|
||||
import io.reactivex.disposables.CompositeDisposable;
|
||||
import io.reactivex.rxjava3.disposables.CompositeDisposable;
|
||||
import us.shandian.giga.get.MissionRecoveryInfo;
|
||||
import us.shandian.giga.io.StoredDirectoryHelper;
|
||||
import us.shandian.giga.io.StoredFileHelper;
|
||||
@@ -81,25 +82,33 @@ import us.shandian.giga.service.MissionState;
|
||||
|
||||
import static org.schabi.newpipe.util.Localization.assureCorrectAppLanguage;
|
||||
|
||||
public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheckedChangeListener, AdapterView.OnItemSelectedListener {
|
||||
public class DownloadDialog extends DialogFragment
|
||||
implements RadioGroup.OnCheckedChangeListener, AdapterView.OnItemSelectedListener {
|
||||
private static final String TAG = "DialogFragment";
|
||||
private static final boolean DEBUG = MainActivity.DEBUG;
|
||||
private static final int REQUEST_DOWNLOAD_SAVE_AS = 0x1230;
|
||||
|
||||
@State
|
||||
protected StreamInfo currentInfo;
|
||||
StreamInfo currentInfo;
|
||||
@State
|
||||
protected StreamSizeWrapper<AudioStream> wrappedAudioStreams = StreamSizeWrapper.empty();
|
||||
StreamSizeWrapper<AudioStream> wrappedAudioStreams = StreamSizeWrapper.empty();
|
||||
@State
|
||||
protected StreamSizeWrapper<VideoStream> wrappedVideoStreams = StreamSizeWrapper.empty();
|
||||
StreamSizeWrapper<VideoStream> wrappedVideoStreams = StreamSizeWrapper.empty();
|
||||
@State
|
||||
protected StreamSizeWrapper<SubtitlesStream> wrappedSubtitleStreams = StreamSizeWrapper.empty();
|
||||
StreamSizeWrapper<SubtitlesStream> wrappedSubtitleStreams = StreamSizeWrapper.empty();
|
||||
@State
|
||||
protected int selectedVideoIndex = 0;
|
||||
int selectedVideoIndex = 0;
|
||||
@State
|
||||
protected int selectedAudioIndex = 0;
|
||||
int selectedAudioIndex = 0;
|
||||
@State
|
||||
protected int selectedSubtitleIndex = 0;
|
||||
int selectedSubtitleIndex = 0;
|
||||
|
||||
private StoredDirectoryHelper mainStorageAudio = null;
|
||||
private StoredDirectoryHelper mainStorageVideo = null;
|
||||
private DownloadManager downloadManager = null;
|
||||
private ActionMenuItemView okButton = null;
|
||||
private Context context;
|
||||
private boolean askForSavePath;
|
||||
|
||||
private StreamItemAdapter<AudioStream, Stream> audioStreamsAdapter;
|
||||
private StreamItemAdapter<VideoStream, AudioStream> videoStreamsAdapter;
|
||||
@@ -115,15 +124,16 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck
|
||||
|
||||
private SharedPreferences prefs;
|
||||
|
||||
public static DownloadDialog newInstance(StreamInfo info) {
|
||||
DownloadDialog dialog = new DownloadDialog();
|
||||
public static DownloadDialog newInstance(final StreamInfo info) {
|
||||
final DownloadDialog dialog = new DownloadDialog();
|
||||
dialog.setInfo(info);
|
||||
return dialog;
|
||||
}
|
||||
|
||||
public static DownloadDialog newInstance(Context context, StreamInfo info) {
|
||||
final ArrayList<VideoStream> streamsList = new ArrayList<>(ListHelper.getSortedStreamVideosList(context,
|
||||
info.getVideoStreams(), info.getVideoOnlyStreams(), false));
|
||||
public static DownloadDialog newInstance(final Context context, final StreamInfo info) {
|
||||
final ArrayList<VideoStream> streamsList = new ArrayList<>(ListHelper
|
||||
.getSortedStreamVideosList(context, info.getVideoStreams(),
|
||||
info.getVideoOnlyStreams(), false));
|
||||
final int selectedStreamIndex = ListHelper.getDefaultResolutionIndex(context, streamsList);
|
||||
|
||||
final DownloadDialog instance = newInstance(info);
|
||||
@@ -135,57 +145,61 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck
|
||||
return instance;
|
||||
}
|
||||
|
||||
private void setInfo(StreamInfo info) {
|
||||
private void setInfo(final StreamInfo info) {
|
||||
this.currentInfo = info;
|
||||
}
|
||||
|
||||
public void setAudioStreams(List<AudioStream> audioStreams) {
|
||||
public void setAudioStreams(final List<AudioStream> audioStreams) {
|
||||
setAudioStreams(new StreamSizeWrapper<>(audioStreams, getContext()));
|
||||
}
|
||||
|
||||
public void setAudioStreams(StreamSizeWrapper<AudioStream> wrappedAudioStreams) {
|
||||
this.wrappedAudioStreams = wrappedAudioStreams;
|
||||
public void setAudioStreams(final StreamSizeWrapper<AudioStream> was) {
|
||||
this.wrappedAudioStreams = was;
|
||||
}
|
||||
|
||||
public void setVideoStreams(List<VideoStream> videoStreams) {
|
||||
public void setVideoStreams(final List<VideoStream> videoStreams) {
|
||||
setVideoStreams(new StreamSizeWrapper<>(videoStreams, getContext()));
|
||||
}
|
||||
|
||||
public void setVideoStreams(StreamSizeWrapper<VideoStream> wrappedVideoStreams) {
|
||||
this.wrappedVideoStreams = wrappedVideoStreams;
|
||||
}
|
||||
|
||||
public void setSubtitleStreams(List<SubtitlesStream> subtitleStreams) {
|
||||
setSubtitleStreams(new StreamSizeWrapper<>(subtitleStreams, getContext()));
|
||||
}
|
||||
|
||||
public void setSubtitleStreams(StreamSizeWrapper<SubtitlesStream> wrappedSubtitleStreams) {
|
||||
this.wrappedSubtitleStreams = wrappedSubtitleStreams;
|
||||
}
|
||||
|
||||
public void setSelectedVideoStream(int selectedVideoIndex) {
|
||||
this.selectedVideoIndex = selectedVideoIndex;
|
||||
}
|
||||
|
||||
public void setSelectedAudioStream(int selectedAudioIndex) {
|
||||
this.selectedAudioIndex = selectedAudioIndex;
|
||||
}
|
||||
|
||||
public void setSelectedSubtitleStream(int selectedSubtitleIndex) {
|
||||
this.selectedSubtitleIndex = selectedSubtitleIndex;
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// LifeCycle
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
@Override
|
||||
public void onCreate(@Nullable Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
if (DEBUG)
|
||||
Log.d(TAG, "onCreate() called with: savedInstanceState = [" + savedInstanceState + "]");
|
||||
public void setVideoStreams(final StreamSizeWrapper<VideoStream> wvs) {
|
||||
this.wrappedVideoStreams = wvs;
|
||||
}
|
||||
|
||||
if (!PermissionHelper.checkStoragePermissions(getActivity(), PermissionHelper.DOWNLOAD_DIALOG_REQUEST_CODE)) {
|
||||
public void setSubtitleStreams(final List<SubtitlesStream> subtitleStreams) {
|
||||
setSubtitleStreams(new StreamSizeWrapper<>(subtitleStreams, getContext()));
|
||||
}
|
||||
|
||||
public void setSubtitleStreams(
|
||||
final StreamSizeWrapper<SubtitlesStream> wss) {
|
||||
this.wrappedSubtitleStreams = wss;
|
||||
}
|
||||
|
||||
public void setSelectedVideoStream(final int svi) {
|
||||
this.selectedVideoIndex = svi;
|
||||
}
|
||||
|
||||
public void setSelectedAudioStream(final int sai) {
|
||||
this.selectedAudioIndex = sai;
|
||||
}
|
||||
|
||||
public void setSelectedSubtitleStream(final int ssi) {
|
||||
this.selectedSubtitleIndex = ssi;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate(@Nullable final Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "onCreate() called with: "
|
||||
+ "savedInstanceState = [" + savedInstanceState + "]");
|
||||
}
|
||||
|
||||
if (!PermissionHelper.checkStoragePermissions(getActivity(),
|
||||
PermissionHelper.DOWNLOAD_DIALOG_REQUEST_CODE)) {
|
||||
getDialog().dismiss();
|
||||
return;
|
||||
}
|
||||
@@ -195,31 +209,38 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck
|
||||
setStyle(STYLE_NO_TITLE, ThemeHelper.getDialogTheme(context));
|
||||
Icepick.restoreInstanceState(this, savedInstanceState);
|
||||
|
||||
SparseArray<SecondaryStreamHelper<AudioStream>> secondaryStreams = new SparseArray<>(4);
|
||||
List<VideoStream> videoStreams = wrappedVideoStreams.getStreamsList();
|
||||
final SparseArray<SecondaryStreamHelper<AudioStream>> secondaryStreams
|
||||
= new SparseArray<>(4);
|
||||
final List<VideoStream> videoStreams = wrappedVideoStreams.getStreamsList();
|
||||
|
||||
for (int i = 0; i < videoStreams.size(); i++) {
|
||||
if (!videoStreams.get(i).isVideoOnly()) continue;
|
||||
AudioStream audioStream = SecondaryStreamHelper.getAudioStreamFor(wrappedAudioStreams.getStreamsList(), videoStreams.get(i));
|
||||
if (!videoStreams.get(i).isVideoOnly()) {
|
||||
continue;
|
||||
}
|
||||
final AudioStream audioStream = SecondaryStreamHelper
|
||||
.getAudioStreamFor(wrappedAudioStreams.getStreamsList(), videoStreams.get(i));
|
||||
|
||||
if (audioStream != null) {
|
||||
secondaryStreams.append(i, new SecondaryStreamHelper<>(wrappedAudioStreams, audioStream));
|
||||
secondaryStreams
|
||||
.append(i, new SecondaryStreamHelper<>(wrappedAudioStreams, audioStream));
|
||||
} else if (DEBUG) {
|
||||
Log.w(TAG, "No audio stream candidates for video format " + videoStreams.get(i).getFormat().name());
|
||||
Log.w(TAG, "No audio stream candidates for video format "
|
||||
+ videoStreams.get(i).getFormat().name());
|
||||
}
|
||||
}
|
||||
|
||||
this.videoStreamsAdapter = new StreamItemAdapter<>(context, wrappedVideoStreams, secondaryStreams);
|
||||
this.videoStreamsAdapter = new StreamItemAdapter<>(context, wrappedVideoStreams,
|
||||
secondaryStreams);
|
||||
this.audioStreamsAdapter = new StreamItemAdapter<>(context, wrappedAudioStreams);
|
||||
this.subtitleStreamsAdapter = new StreamItemAdapter<>(context, wrappedSubtitleStreams);
|
||||
|
||||
Intent intent = new Intent(context, DownloadManagerService.class);
|
||||
final Intent intent = new Intent(context, DownloadManagerService.class);
|
||||
context.startService(intent);
|
||||
|
||||
context.bindService(intent, new ServiceConnection() {
|
||||
@Override
|
||||
public void onServiceConnected(ComponentName cname, IBinder service) {
|
||||
DownloadManagerBinder mgr = (DownloadManagerBinder) service;
|
||||
public void onServiceConnected(final ComponentName cname, final IBinder service) {
|
||||
final DownloadManagerBinder mgr = (DownloadManagerBinder) service;
|
||||
|
||||
mainStorageAudio = mgr.getMainStorageAudio();
|
||||
mainStorageVideo = mgr.getMainStorageVideo();
|
||||
@@ -232,25 +253,34 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onServiceDisconnected(ComponentName name) {
|
||||
public void onServiceDisconnected(final ComponentName name) {
|
||||
// nothing to do
|
||||
}
|
||||
}, Context.BIND_AUTO_CREATE);
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Inits
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
@Override
|
||||
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
|
||||
if (DEBUG)
|
||||
Log.d(TAG, "onCreateView() called with: inflater = [" + inflater + "], container = [" + container + "], savedInstanceState = [" + savedInstanceState + "]");
|
||||
public View onCreateView(@NonNull final LayoutInflater inflater, final ViewGroup container,
|
||||
final Bundle savedInstanceState) {
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "onCreateView() called with: "
|
||||
+ "inflater = [" + inflater + "], container = [" + container + "], "
|
||||
+ "savedInstanceState = [" + savedInstanceState + "]");
|
||||
}
|
||||
return inflater.inflate(R.layout.download_dialog, container);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
|
||||
public void onViewCreated(@NonNull final View view, @Nullable final Bundle savedInstanceState) {
|
||||
super.onViewCreated(view, savedInstanceState);
|
||||
nameEditText = view.findViewById(R.id.file_name);
|
||||
nameEditText.setText(FilenameUtils.createFilename(getContext(), currentInfo.getName()));
|
||||
selectedAudioIndex = ListHelper.getDefaultAudioFormat(getContext(), currentInfo.getAudioStreams());
|
||||
selectedAudioIndex = ListHelper
|
||||
.getDefaultAudioFormat(getContext(), currentInfo.getAudioStreams());
|
||||
|
||||
selectedSubtitleIndex = getSubtitleIndexBy(subtitleStreamsAdapter.getAll());
|
||||
|
||||
@@ -266,27 +296,26 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck
|
||||
initToolbar(view.findViewById(R.id.toolbar));
|
||||
setupDownloadOptions();
|
||||
|
||||
prefs = PreferenceManager.getDefaultSharedPreferences(getContext());
|
||||
prefs = PreferenceManager.getDefaultSharedPreferences(requireContext());
|
||||
|
||||
int threads = prefs.getInt(getString(R.string.default_download_threads), 3);
|
||||
final int threads = prefs.getInt(getString(R.string.default_download_threads), 3);
|
||||
threadsCountTextView.setText(String.valueOf(threads));
|
||||
threadsSeekBar.setProgress(threads - 1);
|
||||
threadsSeekBar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {
|
||||
|
||||
@Override
|
||||
public void onProgressChanged(SeekBar seekbar, int progress, boolean fromUser) {
|
||||
progress++;
|
||||
prefs.edit().putInt(getString(R.string.default_download_threads), progress).apply();
|
||||
threadsCountTextView.setText(String.valueOf(progress));
|
||||
public void onProgressChanged(final SeekBar seekbar, final int progress,
|
||||
final boolean fromUser) {
|
||||
final int newProgress = progress + 1;
|
||||
prefs.edit().putInt(getString(R.string.default_download_threads), newProgress)
|
||||
.apply();
|
||||
threadsCountTextView.setText(String.valueOf(newProgress));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStartTrackingTouch(SeekBar p1) {
|
||||
}
|
||||
public void onStartTrackingTouch(final SeekBar p1) { }
|
||||
|
||||
@Override
|
||||
public void onStopTrackingTouch(SeekBar p1) {
|
||||
}
|
||||
public void onStopTrackingTouch(final SeekBar p1) { }
|
||||
});
|
||||
|
||||
fetchStreamsSize();
|
||||
@@ -295,17 +324,20 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck
|
||||
private void fetchStreamsSize() {
|
||||
disposables.clear();
|
||||
|
||||
disposables.add(StreamSizeWrapper.fetchSizeForWrapper(wrappedVideoStreams).subscribe(result -> {
|
||||
disposables.add(StreamSizeWrapper.fetchSizeForWrapper(wrappedVideoStreams)
|
||||
.subscribe(result -> {
|
||||
if (radioStreamsGroup.getCheckedRadioButtonId() == R.id.video_button) {
|
||||
setupVideoSpinner();
|
||||
}
|
||||
}));
|
||||
disposables.add(StreamSizeWrapper.fetchSizeForWrapper(wrappedAudioStreams).subscribe(result -> {
|
||||
disposables.add(StreamSizeWrapper.fetchSizeForWrapper(wrappedAudioStreams)
|
||||
.subscribe(result -> {
|
||||
if (radioStreamsGroup.getCheckedRadioButtonId() == R.id.audio_button) {
|
||||
setupAudioSpinner();
|
||||
}
|
||||
}));
|
||||
disposables.add(StreamSizeWrapper.fetchSizeForWrapper(wrappedSubtitleStreams).subscribe(result -> {
|
||||
disposables.add(StreamSizeWrapper.fetchSizeForWrapper(wrappedSubtitleStreams)
|
||||
.subscribe(result -> {
|
||||
if (radioStreamsGroup.getCheckedRadioButtonId() == R.id.subtitle_button) {
|
||||
setupSubtitleSpinner();
|
||||
}
|
||||
@@ -318,14 +350,22 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck
|
||||
disposables.clear();
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Radio group Video&Audio options - Listener
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
@Override
|
||||
public void onSaveInstanceState(@NonNull Bundle outState) {
|
||||
public void onSaveInstanceState(@NonNull final Bundle outState) {
|
||||
super.onSaveInstanceState(outState);
|
||||
Icepick.saveInstanceState(this, outState);
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Streams Spinner Listener
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
@Override
|
||||
public void onActivityResult(int requestCode, int resultCode, Intent data) {
|
||||
public void onActivityResult(final int requestCode, final int resultCode, final Intent data) {
|
||||
super.onActivityResult(requestCode, resultCode, data);
|
||||
|
||||
if (requestCode == REQUEST_DOWNLOAD_SAVE_AS && resultCode == Activity.RESULT_OK) {
|
||||
@@ -335,39 +375,38 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck
|
||||
}
|
||||
|
||||
if (FilePickerActivityHelper.isOwnFileUri(context, data.getData())) {
|
||||
File file = Utils.getFileForUri(data.getData());
|
||||
checkSelectedDownload(null, Uri.fromFile(file), file.getName(), StoredFileHelper.DEFAULT_MIME);
|
||||
final File file = Utils.getFileForUri(data.getData());
|
||||
checkSelectedDownload(null, Uri.fromFile(file), file.getName(),
|
||||
StoredFileHelper.DEFAULT_MIME);
|
||||
return;
|
||||
}
|
||||
|
||||
DocumentFile docFile = DocumentFile.fromSingleUri(context, data.getData());
|
||||
final DocumentFile docFile = DocumentFile.fromSingleUri(context, data.getData());
|
||||
if (docFile == null) {
|
||||
showFailedDialog(R.string.general_error);
|
||||
return;
|
||||
}
|
||||
|
||||
// check if the selected file was previously used
|
||||
checkSelectedDownload(null, data.getData(), docFile.getName(), docFile.getType());
|
||||
checkSelectedDownload(null, data.getData(), docFile.getName(),
|
||||
docFile.getType());
|
||||
}
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Inits
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
private void initToolbar(Toolbar toolbar) {
|
||||
if (DEBUG) Log.d(TAG, "initToolbar() called with: toolbar = [" + toolbar + "]");
|
||||
|
||||
boolean isLight = ThemeHelper.isLightThemeSelected(getActivity());
|
||||
private void initToolbar(final Toolbar toolbar) {
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "initToolbar() called with: toolbar = [" + toolbar + "]");
|
||||
}
|
||||
|
||||
toolbar.setTitle(R.string.download_dialog_title);
|
||||
toolbar.setNavigationIcon(isLight ? R.drawable.ic_arrow_back_black_24dp : R.drawable.ic_arrow_back_white_24dp);
|
||||
toolbar.setNavigationIcon(
|
||||
ThemeHelper.resolveResourceIdFromAttr(requireContext(), R.attr.ic_arrow_back));
|
||||
toolbar.inflateMenu(R.menu.dialog_url);
|
||||
toolbar.setNavigationOnClickListener(v -> getDialog().dismiss());
|
||||
toolbar.setNavigationOnClickListener(v -> requireDialog().dismiss());
|
||||
toolbar.setNavigationContentDescription(R.string.cancel);
|
||||
|
||||
okButton = toolbar.findViewById(R.id.okay);
|
||||
okButton.setEnabled(false);// disable until the download service connection is done
|
||||
okButton.setEnabled(false); // disable until the download service connection is done
|
||||
|
||||
toolbar.setOnMenuItemClickListener(item -> {
|
||||
if (item.getItemId() == R.id.okay) {
|
||||
@@ -381,8 +420,14 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck
|
||||
});
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Utils
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
private void setupAudioSpinner() {
|
||||
if (getContext() == null) return;
|
||||
if (getContext() == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
streamsSpinner.setAdapter(audioStreamsAdapter);
|
||||
streamsSpinner.setSelection(selectedAudioIndex);
|
||||
@@ -390,7 +435,9 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck
|
||||
}
|
||||
|
||||
private void setupVideoSpinner() {
|
||||
if (getContext() == null) return;
|
||||
if (getContext() == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
streamsSpinner.setAdapter(videoStreamsAdapter);
|
||||
streamsSpinner.setSelection(selectedVideoIndex);
|
||||
@@ -398,21 +445,21 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck
|
||||
}
|
||||
|
||||
private void setupSubtitleSpinner() {
|
||||
if (getContext() == null) return;
|
||||
if (getContext() == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
streamsSpinner.setAdapter(subtitleStreamsAdapter);
|
||||
streamsSpinner.setSelection(selectedSubtitleIndex);
|
||||
setRadioButtonsState(true);
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Radio group Video&Audio options - Listener
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
@Override
|
||||
public void onCheckedChanged(RadioGroup group, @IdRes int checkedId) {
|
||||
if (DEBUG)
|
||||
Log.d(TAG, "onCheckedChanged() called with: group = [" + group + "], checkedId = [" + checkedId + "]");
|
||||
public void onCheckedChanged(final RadioGroup group, @IdRes final int checkedId) {
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "onCheckedChanged() called with: "
|
||||
+ "group = [" + group + "], checkedId = [" + checkedId + "]");
|
||||
}
|
||||
boolean flag = true;
|
||||
|
||||
switch (checkedId) {
|
||||
@@ -431,14 +478,14 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck
|
||||
threadsSeekBar.setEnabled(flag);
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Streams Spinner Listener
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
@Override
|
||||
public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
|
||||
if (DEBUG)
|
||||
Log.d(TAG, "onItemSelected() called with: parent = [" + parent + "], view = [" + view + "], position = [" + position + "], id = [" + id + "]");
|
||||
public void onItemSelected(final AdapterView<?> parent, final View view,
|
||||
final int position, final long id) {
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "onItemSelected() called with: "
|
||||
+ "parent = [" + parent + "], view = [" + view + "], "
|
||||
+ "position = [" + position + "], id = [" + id + "]");
|
||||
}
|
||||
switch (radioStreamsGroup.getCheckedRadioButtonId()) {
|
||||
case R.id.audio_button:
|
||||
selectedAudioIndex = position;
|
||||
@@ -453,13 +500,9 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onNothingSelected(AdapterView<?> parent) {
|
||||
public void onNothingSelected(final AdapterView<?> parent) {
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Utils
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
protected void setupDownloadOptions() {
|
||||
setRadioButtonsState(false);
|
||||
|
||||
@@ -474,7 +517,23 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck
|
||||
videoButton.setVisibility(isVideoStreamsAvailable ? View.VISIBLE : View.GONE);
|
||||
subtitleButton.setVisibility(isSubtitleStreamsAvailable ? View.VISIBLE : View.GONE);
|
||||
|
||||
if (isVideoStreamsAvailable) {
|
||||
prefs = PreferenceManager.getDefaultSharedPreferences(getContext());
|
||||
final String defaultMedia = prefs.getString(getString(R.string.last_used_download_type),
|
||||
getString(R.string.last_download_type_video_key));
|
||||
|
||||
if (isVideoStreamsAvailable
|
||||
&& (defaultMedia.equals(getString(R.string.last_download_type_video_key)))) {
|
||||
videoButton.setChecked(true);
|
||||
setupVideoSpinner();
|
||||
} else if (isAudioStreamsAvailable
|
||||
&& (defaultMedia.equals(getString(R.string.last_download_type_audio_key)))) {
|
||||
audioButton.setChecked(true);
|
||||
setupAudioSpinner();
|
||||
} else if (isSubtitleStreamsAvailable
|
||||
&& (defaultMedia.equals(getString(R.string.last_download_type_subtitle_key)))) {
|
||||
subtitleButton.setChecked(true);
|
||||
setupSubtitleSpinner();
|
||||
} else if (isVideoStreamsAvailable) {
|
||||
videoButton.setChecked(true);
|
||||
setupVideoSpinner();
|
||||
} else if (isAudioStreamsAvailable) {
|
||||
@@ -484,30 +543,36 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck
|
||||
subtitleButton.setChecked(true);
|
||||
setupSubtitleSpinner();
|
||||
} else {
|
||||
Toast.makeText(getContext(), R.string.no_streams_available_download, Toast.LENGTH_SHORT).show();
|
||||
Toast.makeText(getContext(), R.string.no_streams_available_download,
|
||||
Toast.LENGTH_SHORT).show();
|
||||
getDialog().dismiss();
|
||||
}
|
||||
}
|
||||
|
||||
private void setRadioButtonsState(boolean enabled) {
|
||||
private void setRadioButtonsState(final boolean enabled) {
|
||||
radioStreamsGroup.findViewById(R.id.audio_button).setEnabled(enabled);
|
||||
radioStreamsGroup.findViewById(R.id.video_button).setEnabled(enabled);
|
||||
radioStreamsGroup.findViewById(R.id.subtitle_button).setEnabled(enabled);
|
||||
}
|
||||
|
||||
private int getSubtitleIndexBy(List<SubtitlesStream> streams) {
|
||||
private int getSubtitleIndexBy(final List<SubtitlesStream> streams) {
|
||||
final Localization preferredLocalization = NewPipe.getPreferredLocalization();
|
||||
|
||||
int candidate = 0;
|
||||
for (int i = 0; i < streams.size(); 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());
|
||||
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;
|
||||
if (countryEquals) {
|
||||
return i;
|
||||
}
|
||||
|
||||
candidate = i;
|
||||
}
|
||||
@@ -516,20 +581,13 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck
|
||||
return candidate;
|
||||
}
|
||||
|
||||
StoredDirectoryHelper mainStorageAudio = null;
|
||||
StoredDirectoryHelper mainStorageVideo = null;
|
||||
DownloadManager downloadManager = null;
|
||||
ActionMenuItemView okButton = null;
|
||||
Context context;
|
||||
boolean askForSavePath;
|
||||
|
||||
private String getNameEditText() {
|
||||
String str = nameEditText.getText().toString().trim();
|
||||
final String str = nameEditText.getText().toString().trim();
|
||||
|
||||
return FilenameUtils.createFilename(context, str.isEmpty() ? currentInfo.getName() : str);
|
||||
}
|
||||
|
||||
private void showFailedDialog(@StringRes int msg) {
|
||||
private void showFailedDialog(@StringRes final int msg) {
|
||||
assureCorrectAppLanguage(getContext());
|
||||
new AlertDialog.Builder(context)
|
||||
.setTitle(R.string.general_error)
|
||||
@@ -539,20 +597,22 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck
|
||||
.show();
|
||||
}
|
||||
|
||||
private void showErrorActivity(Exception e) {
|
||||
private void showErrorActivity(final Exception e) {
|
||||
ErrorActivity.reportError(
|
||||
context,
|
||||
Collections.singletonList(e),
|
||||
null,
|
||||
null,
|
||||
ErrorActivity.ErrorInfo.make(UserAction.SOMETHING_ELSE, "-", "-", R.string.general_error)
|
||||
ErrorInfo
|
||||
.make(UserAction.SOMETHING_ELSE, "-", "-", R.string.general_error)
|
||||
);
|
||||
}
|
||||
|
||||
private void prepareSelectedDownload() {
|
||||
StoredDirectoryHelper mainStorage;
|
||||
MediaFormat format;
|
||||
String mime;
|
||||
final StoredDirectoryHelper mainStorage;
|
||||
final MediaFormat format;
|
||||
final String mime;
|
||||
final String selectedMediaType;
|
||||
|
||||
// first, build the filename and get the output folder (if possible)
|
||||
// later, run a very very very large file checking logic
|
||||
@@ -561,9 +621,10 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck
|
||||
|
||||
switch (radioStreamsGroup.getCheckedRadioButtonId()) {
|
||||
case R.id.audio_button:
|
||||
selectedMediaType = getString(R.string.last_download_type_audio_key);
|
||||
mainStorage = mainStorageAudio;
|
||||
format = audioStreamsAdapter.getItem(selectedAudioIndex).getFormat();
|
||||
switch(format) {
|
||||
switch (format) {
|
||||
case WEBMA_OPUS:
|
||||
mime = "audio/ogg";
|
||||
filename += "opus";
|
||||
@@ -575,16 +636,18 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck
|
||||
}
|
||||
break;
|
||||
case R.id.video_button:
|
||||
selectedMediaType = getString(R.string.last_download_type_video_key);
|
||||
mainStorage = mainStorageVideo;
|
||||
format = videoStreamsAdapter.getItem(selectedVideoIndex).getFormat();
|
||||
mime = format.mimeType;
|
||||
filename += format.suffix;
|
||||
break;
|
||||
case R.id.subtitle_button:
|
||||
mainStorage = mainStorageVideo;// subtitle & video files go together
|
||||
selectedMediaType = getString(R.string.last_download_type_subtitle_key);
|
||||
mainStorage = mainStorageVideo; // subtitle & video files go together
|
||||
format = subtitleStreamsAdapter.getItem(selectedSubtitleIndex).getFormat();
|
||||
mime = format.mimeType;
|
||||
filename += format == MediaFormat.TTML ? MediaFormat.SRT.suffix : format.suffix;
|
||||
filename += (format == MediaFormat.TTML ? MediaFormat.SRT : format).suffix;
|
||||
break;
|
||||
default:
|
||||
throw new RuntimeException("No stream selected");
|
||||
@@ -596,23 +659,25 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck
|
||||
// * save path not defined (via download settings)
|
||||
// * the user checked the "ask where to download" option
|
||||
|
||||
if (!askForSavePath)
|
||||
Toast.makeText(context, getString(R.string.no_available_dir), Toast.LENGTH_LONG).show();
|
||||
if (!askForSavePath) {
|
||||
Toast.makeText(context, getString(R.string.no_available_dir),
|
||||
Toast.LENGTH_LONG).show();
|
||||
}
|
||||
|
||||
if (NewPipeSettings.useStorageAccessFramework(context)) {
|
||||
StoredFileHelper.requestSafWithFileCreation(this, REQUEST_DOWNLOAD_SAVE_AS, filename, mime);
|
||||
StoredFileHelper.requestSafWithFileCreation(this, REQUEST_DOWNLOAD_SAVE_AS,
|
||||
filename, mime);
|
||||
} else {
|
||||
File initialSavePath;
|
||||
if (radioStreamsGroup.getCheckedRadioButtonId() == R.id.audio_button)
|
||||
if (radioStreamsGroup.getCheckedRadioButtonId() == R.id.audio_button) {
|
||||
initialSavePath = NewPipeSettings.getDir(Environment.DIRECTORY_MUSIC);
|
||||
else
|
||||
} else {
|
||||
initialSavePath = NewPipeSettings.getDir(Environment.DIRECTORY_MOVIES);
|
||||
}
|
||||
|
||||
initialSavePath = new File(initialSavePath, filename);
|
||||
startActivityForResult(
|
||||
FilePickerActivityHelper.chooseFileToSave(context, initialSavePath.getAbsolutePath()),
|
||||
REQUEST_DOWNLOAD_SAVE_AS
|
||||
);
|
||||
startActivityForResult(FilePickerActivityHelper.chooseFileToSave(context,
|
||||
initialSavePath.getAbsolutePath()), REQUEST_DOWNLOAD_SAVE_AS);
|
||||
}
|
||||
|
||||
return;
|
||||
@@ -620,9 +685,16 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck
|
||||
|
||||
// check for existing file with the same name
|
||||
checkSelectedDownload(mainStorage, mainStorage.findFile(filename), filename, mime);
|
||||
|
||||
// remember the last media type downloaded by the user
|
||||
prefs.edit()
|
||||
.putString(getString(R.string.last_used_download_type), selectedMediaType)
|
||||
.apply();
|
||||
}
|
||||
|
||||
private void checkSelectedDownload(StoredDirectoryHelper mainStorage, Uri targetFile, String filename, String mime) {
|
||||
private void checkSelectedDownload(final StoredDirectoryHelper mainStorage,
|
||||
final Uri targetFile, final String filename,
|
||||
final String mime) {
|
||||
StoredFileHelper storage;
|
||||
|
||||
try {
|
||||
@@ -631,20 +703,24 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck
|
||||
storage = new StoredFileHelper(context, null, targetFile, "");
|
||||
} else if (targetFile == null) {
|
||||
// the file does not exist, but it is probably used in a pending download
|
||||
storage = new StoredFileHelper(mainStorage.getUri(), filename, mime, mainStorage.getTag());
|
||||
storage = new StoredFileHelper(mainStorage.getUri(), filename, mime,
|
||||
mainStorage.getTag());
|
||||
} else {
|
||||
// the target filename is already use, attempt to use it
|
||||
storage = new StoredFileHelper(context, mainStorage.getUri(), targetFile, mainStorage.getTag());
|
||||
storage = new StoredFileHelper(context, mainStorage.getUri(), targetFile,
|
||||
mainStorage.getTag());
|
||||
}
|
||||
} catch (Exception e) {
|
||||
} catch (final Exception e) {
|
||||
showErrorActivity(e);
|
||||
return;
|
||||
}
|
||||
|
||||
// check if is our file
|
||||
MissionState state = downloadManager.checkForExistingMission(storage);
|
||||
@StringRes int msgBtn;
|
||||
@StringRes int msgBody;
|
||||
final MissionState state = downloadManager.checkForExistingMission(storage);
|
||||
@StringRes
|
||||
final int msgBtn;
|
||||
@StringRes
|
||||
final int msgBody;
|
||||
|
||||
switch (state) {
|
||||
case Finished:
|
||||
@@ -697,8 +773,7 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
AlertDialog.Builder askDialog = new AlertDialog.Builder(context)
|
||||
final AlertDialog.Builder askDialog = new AlertDialog.Builder(context)
|
||||
.setTitle(R.string.download_dialog_title)
|
||||
.setMessage(msgBody)
|
||||
.setNegativeButton(android.R.string.cancel, null);
|
||||
@@ -738,24 +813,28 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck
|
||||
} else {
|
||||
try {
|
||||
// try take (or steal) the file
|
||||
storageNew = new StoredFileHelper(context, mainStorage.getUri(), targetFile, mainStorage.getTag());
|
||||
} catch (IOException e) {
|
||||
Log.e(TAG, "Failed to take (or steal) the file in " + targetFile.toString());
|
||||
storageNew = new StoredFileHelper(context, mainStorage.getUri(),
|
||||
targetFile, mainStorage.getTag());
|
||||
} catch (final IOException e) {
|
||||
Log.e(TAG, "Failed to take (or steal) the file in "
|
||||
+ targetFile.toString());
|
||||
storageNew = null;
|
||||
}
|
||||
}
|
||||
|
||||
if (storageNew != null && storageNew.canWrite())
|
||||
if (storageNew != null && storageNew.canWrite()) {
|
||||
continueSelectedDownload(storageNew);
|
||||
else
|
||||
} else {
|
||||
showFailedDialog(R.string.error_file_creation);
|
||||
}
|
||||
break;
|
||||
case PendingRunning:
|
||||
storageNew = mainStorage.createUniqueFile(filename, mime);
|
||||
if (storageNew == null)
|
||||
if (storageNew == null) {
|
||||
showFailedDialog(R.string.error_file_creation);
|
||||
else
|
||||
} else {
|
||||
continueSelectedDownload(storageNew);
|
||||
}
|
||||
break;
|
||||
}
|
||||
});
|
||||
@@ -763,7 +842,7 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck
|
||||
askDialog.create().show();
|
||||
}
|
||||
|
||||
private void continueSelectedDownload(@NonNull StoredFileHelper storage) {
|
||||
private void continueSelectedDownload(@NonNull final StoredFileHelper storage) {
|
||||
if (!storage.canWrite()) {
|
||||
showFailedDialog(R.string.permission_denied);
|
||||
return;
|
||||
@@ -771,19 +850,21 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck
|
||||
|
||||
// check if the selected file has to be overwritten, by simply checking its length
|
||||
try {
|
||||
if (storage.length() > 0) storage.truncate();
|
||||
} catch (IOException e) {
|
||||
if (storage.length() > 0) {
|
||||
storage.truncate();
|
||||
}
|
||||
} catch (final IOException e) {
|
||||
Log.e(TAG, "failed to truncate the file: " + storage.getUri().toString(), e);
|
||||
showFailedDialog(R.string.overwrite_failed);
|
||||
return;
|
||||
}
|
||||
|
||||
Stream selectedStream;
|
||||
final Stream selectedStream;
|
||||
Stream secondaryStream = null;
|
||||
char kind;
|
||||
final char kind;
|
||||
int threads = threadsSeekBar.getProgress() + 1;
|
||||
String[] urls;
|
||||
MissionRecoveryInfo[] recoveryInfo;
|
||||
final String[] urls;
|
||||
final MissionRecoveryInfo[] recoveryInfo;
|
||||
String psName = null;
|
||||
String[] psArgs = null;
|
||||
long nearLength = 0;
|
||||
@@ -804,20 +885,22 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck
|
||||
kind = 'v';
|
||||
selectedStream = videoStreamsAdapter.getItem(selectedVideoIndex);
|
||||
|
||||
SecondaryStreamHelper<AudioStream> secondary = videoStreamsAdapter
|
||||
final SecondaryStreamHelper<AudioStream> secondary = videoStreamsAdapter
|
||||
.getAllSecondary()
|
||||
.get(wrappedVideoStreams.getStreamsList().indexOf(selectedStream));
|
||||
|
||||
if (secondary != null) {
|
||||
secondaryStream = secondary.getStream();
|
||||
|
||||
if (selectedStream.getFormat() == MediaFormat.MPEG_4)
|
||||
if (selectedStream.getFormat() == MediaFormat.MPEG_4) {
|
||||
psName = Postprocessing.ALGORITHM_MP4_FROM_DASH_MUXER;
|
||||
else
|
||||
} else {
|
||||
psName = Postprocessing.ALGORITHM_WEBM_MUXER;
|
||||
}
|
||||
|
||||
psArgs = null;
|
||||
long videoSize = wrappedVideoStreams.getSizeInBytes((VideoStream) selectedStream);
|
||||
final long videoSize = wrappedVideoStreams
|
||||
.getSizeInBytes((VideoStream) selectedStream);
|
||||
|
||||
// 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
|
||||
@@ -827,7 +910,7 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck
|
||||
}
|
||||
break;
|
||||
case R.id.subtitle_button:
|
||||
threads = 1;// use unique thread for subtitles due small file size
|
||||
threads = 1; // use unique thread for subtitles due small file size
|
||||
kind = 's';
|
||||
selectedStream = subtitleStreamsAdapter.getItem(selectedSubtitleIndex);
|
||||
|
||||
@@ -835,7 +918,7 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck
|
||||
psName = Postprocessing.ALGORITHM_TTML_CONVERTER;
|
||||
psArgs = new String[]{
|
||||
selectedStream.getFormat().getSuffix(),
|
||||
"false",// ignore empty frames
|
||||
"false" // ignore empty frames
|
||||
};
|
||||
}
|
||||
break;
|
||||
@@ -854,14 +937,12 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck
|
||||
urls = new String[]{
|
||||
selectedStream.getUrl(), secondaryStream.getUrl()
|
||||
};
|
||||
recoveryInfo = new MissionRecoveryInfo[]{
|
||||
new MissionRecoveryInfo(selectedStream), new MissionRecoveryInfo(secondaryStream)
|
||||
};
|
||||
recoveryInfo = new MissionRecoveryInfo[]{new MissionRecoveryInfo(selectedStream),
|
||||
new MissionRecoveryInfo(secondaryStream)};
|
||||
}
|
||||
|
||||
DownloadManagerService.startMission(
|
||||
context, urls, storage, kind, threads, currentInfo.getUrl(), psName, psArgs, nearLength, recoveryInfo
|
||||
);
|
||||
DownloadManagerService.startMission(context, urls, storage, kind, threads,
|
||||
currentInfo.getUrl(), psName, psArgs, nearLength, recoveryInfo);
|
||||
|
||||
dismiss();
|
||||
}
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
package org.schabi.newpipe.fragments;
|
||||
|
||||
/**
|
||||
* Indicates that the current fragment can handle back presses
|
||||
* Indicates that the current fragment can handle back presses.
|
||||
*/
|
||||
public interface BackPressable {
|
||||
/**
|
||||
* A back press was delegated to this fragment
|
||||
* A back press was delegated to this fragment.
|
||||
*
|
||||
* @return if the back press was handled
|
||||
*/
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
package org.schabi.newpipe.fragments;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.os.Bundle;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.StringRes;
|
||||
import android.util.Log;
|
||||
import android.view.View;
|
||||
import android.widget.Button;
|
||||
@@ -11,47 +10,53 @@ import android.widget.ProgressBar;
|
||||
import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
|
||||
import com.jakewharton.rxbinding2.view.RxView;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.StringRes;
|
||||
|
||||
import com.jakewharton.rxbinding4.view.RxView;
|
||||
|
||||
import org.schabi.newpipe.BaseFragment;
|
||||
import org.schabi.newpipe.MainActivity;
|
||||
import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.ReCaptchaActivity;
|
||||
import org.schabi.newpipe.extractor.exceptions.ContentNotAvailableException;
|
||||
import org.schabi.newpipe.extractor.exceptions.ContentNotSupportedException;
|
||||
import org.schabi.newpipe.extractor.exceptions.ReCaptchaException;
|
||||
import org.schabi.newpipe.report.ErrorActivity;
|
||||
import org.schabi.newpipe.report.ErrorInfo;
|
||||
import org.schabi.newpipe.report.UserAction;
|
||||
import org.schabi.newpipe.util.ExtractorHelper;
|
||||
import org.schabi.newpipe.util.ExceptionUtils;
|
||||
import org.schabi.newpipe.util.InfoCache;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
|
||||
import icepick.State;
|
||||
import io.reactivex.android.schedulers.AndroidSchedulers;
|
||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
|
||||
import io.reactivex.rxjava3.disposables.Disposable;
|
||||
|
||||
import static org.schabi.newpipe.util.AnimationUtils.animateView;
|
||||
|
||||
public abstract class BaseStateFragment<I> extends BaseFragment implements ViewContract<I> {
|
||||
|
||||
@State
|
||||
protected AtomicBoolean wasLoading = new AtomicBoolean();
|
||||
protected AtomicBoolean isLoading = new AtomicBoolean();
|
||||
|
||||
@Nullable
|
||||
protected View emptyStateView;
|
||||
private View emptyStateView;
|
||||
@Nullable
|
||||
protected ProgressBar loadingProgressBar;
|
||||
private ProgressBar loadingProgressBar;
|
||||
|
||||
private Disposable errorDisposable;
|
||||
|
||||
protected View errorPanelRoot;
|
||||
protected Button errorButtonRetry;
|
||||
protected TextView errorTextView;
|
||||
private Button errorButtonRetry;
|
||||
private TextView errorTextView;
|
||||
|
||||
@Override
|
||||
public void onViewCreated(View rootView, Bundle savedInstanceState) {
|
||||
public void onViewCreated(final View rootView, final Bundle savedInstanceState) {
|
||||
super.onViewCreated(rootView, savedInstanceState);
|
||||
doInitialLoadLogic();
|
||||
}
|
||||
@@ -62,14 +67,20 @@ public abstract class BaseStateFragment<I> extends BaseFragment implements ViewC
|
||||
wasLoading.set(isLoading.get());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroy() {
|
||||
super.onDestroy();
|
||||
if (errorDisposable != null) {
|
||||
errorDisposable.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Init
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
|
||||
@Override
|
||||
protected void initViews(View rootView, Bundle savedInstanceState) {
|
||||
protected void initViews(final View rootView, final Bundle savedInstanceState) {
|
||||
super.initViews(rootView, savedInstanceState);
|
||||
|
||||
emptyStateView = rootView.findViewById(R.id.empty_state_view);
|
||||
@@ -83,7 +94,7 @@ public abstract class BaseStateFragment<I> extends BaseFragment implements ViewC
|
||||
@Override
|
||||
protected void initListeners() {
|
||||
super.initListeners();
|
||||
RxView.clicks(errorButtonRetry)
|
||||
errorDisposable = RxView.clicks(errorButtonRetry)
|
||||
.debounce(300, TimeUnit.MILLISECONDS)
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(o -> onRetryButtonClicked());
|
||||
@@ -105,8 +116,10 @@ public abstract class BaseStateFragment<I> extends BaseFragment implements ViewC
|
||||
startLoading(true);
|
||||
}
|
||||
|
||||
protected void startLoading(boolean forceLoad) {
|
||||
if (DEBUG) Log.d(TAG, "startLoading() called with: forceLoad = [" + forceLoad + "]");
|
||||
protected void startLoading(final boolean forceLoad) {
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "startLoading() called with: forceLoad = [" + forceLoad + "]");
|
||||
}
|
||||
showLoading();
|
||||
isLoading.set(true);
|
||||
}
|
||||
@@ -117,42 +130,62 @@ public abstract class BaseStateFragment<I> extends BaseFragment implements ViewC
|
||||
|
||||
@Override
|
||||
public void showLoading() {
|
||||
if (emptyStateView != null) animateView(emptyStateView, false, 150);
|
||||
if (loadingProgressBar != null) animateView(loadingProgressBar, true, 400);
|
||||
if (emptyStateView != null) {
|
||||
animateView(emptyStateView, false, 150);
|
||||
}
|
||||
if (loadingProgressBar != null) {
|
||||
animateView(loadingProgressBar, true, 400);
|
||||
}
|
||||
animateView(errorPanelRoot, false, 150);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void hideLoading() {
|
||||
if (emptyStateView != null) animateView(emptyStateView, false, 150);
|
||||
if (loadingProgressBar != null) animateView(loadingProgressBar, false, 0);
|
||||
if (emptyStateView != null) {
|
||||
animateView(emptyStateView, false, 150);
|
||||
}
|
||||
if (loadingProgressBar != null) {
|
||||
animateView(loadingProgressBar, false, 0);
|
||||
}
|
||||
animateView(errorPanelRoot, false, 150);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void showEmptyState() {
|
||||
isLoading.set(false);
|
||||
if (emptyStateView != null) animateView(emptyStateView, true, 200);
|
||||
if (loadingProgressBar != null) animateView(loadingProgressBar, false, 0);
|
||||
if (emptyStateView != null) {
|
||||
animateView(emptyStateView, true, 200);
|
||||
}
|
||||
if (loadingProgressBar != null) {
|
||||
animateView(loadingProgressBar, false, 0);
|
||||
}
|
||||
animateView(errorPanelRoot, false, 150);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void showError(String message, boolean showRetryButton) {
|
||||
if (DEBUG) Log.d(TAG, "showError() called with: message = [" + message + "], showRetryButton = [" + showRetryButton + "]");
|
||||
public void showError(final String message, final boolean showRetryButton) {
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "showError() called with: "
|
||||
+ "message = [" + message + "], showRetryButton = [" + showRetryButton + "]");
|
||||
}
|
||||
isLoading.set(false);
|
||||
InfoCache.getInstance().clearCache();
|
||||
hideLoading();
|
||||
|
||||
errorTextView.setText(message);
|
||||
if (showRetryButton) animateView(errorButtonRetry, true, 600);
|
||||
else animateView(errorButtonRetry, false, 0);
|
||||
if (showRetryButton) {
|
||||
animateView(errorButtonRetry, true, 600);
|
||||
} else {
|
||||
animateView(errorButtonRetry, false, 0);
|
||||
}
|
||||
animateView(errorPanelRoot, true, 300);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleResult(I result) {
|
||||
if (DEBUG) Log.d(TAG, "handleResult() called with: result = [" + result + "]");
|
||||
public void handleResult(final I result) {
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "handleResult() called with: result = [" + result + "]");
|
||||
}
|
||||
hideLoading();
|
||||
}
|
||||
|
||||
@@ -161,21 +194,28 @@ public abstract class BaseStateFragment<I> extends BaseFragment implements ViewC
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
/**
|
||||
* Default implementation handles some general exceptions
|
||||
* Default implementation handles some general exceptions.
|
||||
*
|
||||
* @return if the exception was handled
|
||||
* @param exception The exception that should be handled
|
||||
* @return If the exception was handled
|
||||
*/
|
||||
protected boolean onError(Throwable exception) {
|
||||
if (DEBUG) Log.d(TAG, "onError() called with: exception = [" + exception + "]");
|
||||
protected boolean onError(final Throwable exception) {
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "onError() called with: exception = [" + exception + "]");
|
||||
}
|
||||
isLoading.set(false);
|
||||
|
||||
if (isDetached() || isRemoving()) {
|
||||
if (DEBUG) Log.w(TAG, "onError() is detached or removing = [" + exception + "]");
|
||||
if (DEBUG) {
|
||||
Log.w(TAG, "onError() is detached or removing = [" + exception + "]");
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
if (ExtractorHelper.isInterruptedCaused(exception)) {
|
||||
if (DEBUG) Log.w(TAG, "onError() isInterruptedCaused! = [" + exception + "]");
|
||||
if (ExceptionUtils.isInterruptedCaused(exception)) {
|
||||
if (DEBUG) {
|
||||
Log.w(TAG, "onError() isInterruptedCaused! = [" + exception + "]");
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -185,54 +225,84 @@ public abstract class BaseStateFragment<I> extends BaseFragment implements ViewC
|
||||
} else if (exception instanceof ContentNotAvailableException) {
|
||||
showError(getString(R.string.content_not_available), false);
|
||||
return true;
|
||||
} else if (exception instanceof IOException) {
|
||||
} else if (ExceptionUtils.isNetworkRelated(exception)) {
|
||||
showError(getString(R.string.network_error), true);
|
||||
return true;
|
||||
} else if (exception instanceof ContentNotSupportedException) {
|
||||
showError(getString(R.string.content_not_supported), false);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public void onReCaptchaException(ReCaptchaException exception) {
|
||||
if (DEBUG) Log.d(TAG, "onReCaptchaException() called");
|
||||
public void onReCaptchaException(final ReCaptchaException exception) {
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "onReCaptchaException() called");
|
||||
}
|
||||
Toast.makeText(activity, R.string.recaptcha_request_toast, Toast.LENGTH_LONG).show();
|
||||
// Starting ReCaptcha Challenge Activity
|
||||
Intent intent = new Intent(activity, ReCaptchaActivity.class);
|
||||
final Intent intent = new Intent(activity, ReCaptchaActivity.class);
|
||||
intent.putExtra(ReCaptchaActivity.RECAPTCHA_URL_EXTRA, exception.getUrl());
|
||||
startActivityForResult(intent, ReCaptchaActivity.RECAPTCHA_REQUEST);
|
||||
|
||||
showError(getString(R.string.recaptcha_request_toast), false);
|
||||
}
|
||||
|
||||
public void onUnrecoverableError(Throwable exception, UserAction userAction, String serviceName, String request, @StringRes int errorId) {
|
||||
onUnrecoverableError(Collections.singletonList(exception), userAction, serviceName, request, errorId);
|
||||
public void onUnrecoverableError(final Throwable exception, final UserAction userAction,
|
||||
final String serviceName, final String request,
|
||||
@StringRes final int errorId) {
|
||||
onUnrecoverableError(Collections.singletonList(exception), userAction, serviceName,
|
||||
request, errorId);
|
||||
}
|
||||
|
||||
public void onUnrecoverableError(List<Throwable> exception, UserAction userAction, String serviceName, String request, @StringRes int errorId) {
|
||||
if (DEBUG) Log.d(TAG, "onUnrecoverableError() called with: exception = [" + exception + "]");
|
||||
public void onUnrecoverableError(final List<Throwable> exception, final UserAction userAction,
|
||||
final String serviceName, final String request,
|
||||
@StringRes final int errorId) {
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "onUnrecoverableError() called with: exception = [" + exception + "]");
|
||||
}
|
||||
|
||||
if (serviceName == null) serviceName = "none";
|
||||
if (request == null) request = "none";
|
||||
|
||||
ErrorActivity.reportError(getContext(), exception, MainActivity.class, null, ErrorActivity.ErrorInfo.make(userAction, serviceName, request, errorId));
|
||||
ErrorActivity.reportError(getContext(), exception, MainActivity.class, null,
|
||||
ErrorInfo.make(userAction, serviceName == null ? "none" : serviceName,
|
||||
request == null ? "none" : request, errorId));
|
||||
}
|
||||
|
||||
public void showSnackBarError(Throwable exception, UserAction userAction, String serviceName, String request, @StringRes int errorId) {
|
||||
showSnackBarError(Collections.singletonList(exception), userAction, serviceName, request, errorId);
|
||||
public void showSnackBarError(final Throwable exception, final UserAction userAction,
|
||||
final String serviceName, final String request,
|
||||
@StringRes final int errorId) {
|
||||
showSnackBarError(Collections.singletonList(exception), userAction, serviceName, request,
|
||||
errorId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Show a SnackBar and only call ErrorActivity#reportError IF we a find a valid view (otherwise the error screen appears)
|
||||
* Show a SnackBar and only call
|
||||
* {@link ErrorActivity#reportError(Context, List, Class, View, ErrorInfo)}
|
||||
* IF we a find a valid view (otherwise the error screen appears).
|
||||
*
|
||||
* @param exception List of the exceptions to show
|
||||
* @param userAction The user action that caused the exception
|
||||
* @param serviceName The service where the exception happened
|
||||
* @param request The page that was requested
|
||||
* @param errorId The ID of the error
|
||||
*/
|
||||
public void showSnackBarError(List<Throwable> exception, UserAction userAction, String serviceName, String request, @StringRes int errorId) {
|
||||
public void showSnackBarError(final List<Throwable> exception, final UserAction userAction,
|
||||
final String serviceName, final String request,
|
||||
@StringRes final int errorId) {
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "showSnackBarError() called with: exception = [" + exception + "], userAction = [" + userAction + "], request = [" + request + "], errorId = [" + errorId + "]");
|
||||
Log.d(TAG, "showSnackBarError() called with: "
|
||||
+ "exception = [" + exception + "], userAction = [" + userAction + "], "
|
||||
+ "request = [" + request + "], errorId = [" + errorId + "]");
|
||||
}
|
||||
View rootView = activity != null ? activity.findViewById(android.R.id.content) : null;
|
||||
if (rootView == null && getView() != null) rootView = getView();
|
||||
if (rootView == null) return;
|
||||
if (rootView == null && getView() != null) {
|
||||
rootView = getView();
|
||||
}
|
||||
if (rootView == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
ErrorActivity.reportError(getContext(), exception, MainActivity.class, rootView,
|
||||
ErrorActivity.ErrorInfo.make(userAction, serviceName, request, errorId));
|
||||
ErrorInfo.make(userAction, serviceName, request, errorId));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,24 +1,26 @@
|
||||
package org.schabi.newpipe.fragments;
|
||||
|
||||
import android.os.Bundle;
|
||||
import androidx.annotation.Nullable;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import org.schabi.newpipe.BaseFragment;
|
||||
import org.schabi.newpipe.R;
|
||||
|
||||
public class BlankFragment extends BaseFragment {
|
||||
@Nullable
|
||||
@Override
|
||||
public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, Bundle savedInstanceState) {
|
||||
public View onCreateView(final LayoutInflater inflater, @Nullable final ViewGroup container,
|
||||
final Bundle savedInstanceState) {
|
||||
setTitle("NewPipe");
|
||||
return inflater.inflate(R.layout.fragment_blank, container, false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setUserVisibleHint(boolean isVisibleToUser) {
|
||||
public void setUserVisibleHint(final boolean isVisibleToUser) {
|
||||
super.setUserVisibleHint(isVisibleToUser);
|
||||
setTitle("NewPipe");
|
||||
// leave this inline. Will make it harder for copy cats.
|
||||
|
||||
@@ -1,17 +1,19 @@
|
||||
package org.schabi.newpipe.fragments;
|
||||
|
||||
import android.os.Bundle;
|
||||
import androidx.annotation.Nullable;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import org.schabi.newpipe.BaseFragment;
|
||||
import org.schabi.newpipe.R;
|
||||
|
||||
public class EmptyFragment extends BaseFragment {
|
||||
@Override
|
||||
public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, Bundle savedInstanceState) {
|
||||
public View onCreateView(final LayoutInflater inflater, @Nullable final ViewGroup container,
|
||||
final Bundle savedInstanceState) {
|
||||
return inflater.inflate(R.layout.fragment_empty, container, false);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
package org.schabi.newpipe.fragments;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.res.ColorStateList;
|
||||
import android.os.Bundle;
|
||||
import androidx.preference.PreferenceManager;
|
||||
import android.util.Log;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.Menu;
|
||||
@@ -25,11 +27,13 @@ import org.schabi.newpipe.BaseFragment;
|
||||
import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.extractor.exceptions.ExtractionException;
|
||||
import org.schabi.newpipe.report.ErrorActivity;
|
||||
import org.schabi.newpipe.report.ErrorInfo;
|
||||
import org.schabi.newpipe.report.UserAction;
|
||||
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.util.ThemeHelper;
|
||||
import org.schabi.newpipe.views.ScrollableTabLayout;
|
||||
|
||||
import java.util.ArrayList;
|
||||
@@ -40,24 +44,27 @@ public class MainFragment extends BaseFragment implements TabLayout.OnTabSelecte
|
||||
private SelectedTabsPagerAdapter pagerAdapter;
|
||||
private ScrollableTabLayout tabLayout;
|
||||
|
||||
private List<Tab> tabsList = new ArrayList<>();
|
||||
private final List<Tab> tabsList = new ArrayList<>();
|
||||
private TabsManager tabsManager;
|
||||
|
||||
private boolean hasTabsChanged = false;
|
||||
|
||||
private boolean previousYoutubeRestrictedModeEnabled;
|
||||
private String youtubeRestrictedModeEnabledKey;
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Fragment's LifeCycle
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState) {
|
||||
public void onCreate(final Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
setHasOptionsMenu(true);
|
||||
|
||||
tabsManager = TabsManager.getManager(activity);
|
||||
tabsManager.setSavedTabsListener(() -> {
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "TabsManager.SavedTabsChangeListener: onTabsChanged called, isResumed = " + isResumed());
|
||||
Log.d(TAG, "TabsManager.SavedTabsChangeListener: "
|
||||
+ "onTabsChanged called, isResumed = " + isResumed());
|
||||
}
|
||||
if (isResumed()) {
|
||||
setupTabs();
|
||||
@@ -65,20 +72,29 @@ public class MainFragment extends BaseFragment implements TabLayout.OnTabSelecte
|
||||
hasTabsChanged = true;
|
||||
}
|
||||
});
|
||||
|
||||
youtubeRestrictedModeEnabledKey = getString(R.string.youtube_restricted_mode_enabled);
|
||||
previousYoutubeRestrictedModeEnabled =
|
||||
PreferenceManager.getDefaultSharedPreferences(requireContext())
|
||||
.getBoolean(youtubeRestrictedModeEnabledKey, false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
|
||||
public View onCreateView(@NonNull final LayoutInflater inflater,
|
||||
@Nullable final ViewGroup container,
|
||||
@Nullable final Bundle savedInstanceState) {
|
||||
return inflater.inflate(R.layout.fragment_main, container, false);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void initViews(View rootView, Bundle savedInstanceState) {
|
||||
protected void initViews(final View rootView, final Bundle savedInstanceState) {
|
||||
super.initViews(rootView, savedInstanceState);
|
||||
|
||||
tabLayout = rootView.findViewById(R.id.main_tab_layout);
|
||||
viewPager = rootView.findViewById(R.id.pager);
|
||||
|
||||
tabLayout.setTabIconTint(ColorStateList.valueOf(
|
||||
ThemeHelper.resolveColorFromAttr(requireContext(), R.attr.colorAccent)));
|
||||
tabLayout.setupWithViewPager(viewPager);
|
||||
tabLayout.addOnTabSelectedListener(this);
|
||||
|
||||
@@ -89,14 +105,24 @@ public class MainFragment extends BaseFragment implements TabLayout.OnTabSelecte
|
||||
public void onResume() {
|
||||
super.onResume();
|
||||
|
||||
if (hasTabsChanged) setupTabs();
|
||||
final boolean youtubeRestrictedModeEnabled =
|
||||
PreferenceManager.getDefaultSharedPreferences(requireContext())
|
||||
.getBoolean(youtubeRestrictedModeEnabledKey, false);
|
||||
if (previousYoutubeRestrictedModeEnabled != youtubeRestrictedModeEnabled) {
|
||||
previousYoutubeRestrictedModeEnabled = youtubeRestrictedModeEnabled;
|
||||
setupTabs();
|
||||
} else if (hasTabsChanged) {
|
||||
setupTabs();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroy() {
|
||||
super.onDestroy();
|
||||
tabsManager.unsetSavedTabsListener();
|
||||
if (viewPager != null) viewPager.setAdapter(null);
|
||||
if (viewPager != null) {
|
||||
viewPager.setAdapter(null);
|
||||
}
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
@@ -104,27 +130,28 @@ public class MainFragment extends BaseFragment implements TabLayout.OnTabSelecte
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
@Override
|
||||
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
|
||||
public void onCreateOptionsMenu(final Menu menu, final MenuInflater inflater) {
|
||||
super.onCreateOptionsMenu(menu, inflater);
|
||||
if (DEBUG) Log.d(TAG, "onCreateOptionsMenu() called with: menu = [" + menu + "], inflater = [" + inflater + "]");
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "onCreateOptionsMenu() called with: "
|
||||
+ "menu = [" + menu + "], inflater = [" + inflater + "]");
|
||||
}
|
||||
inflater.inflate(R.menu.main_fragment_menu, menu);
|
||||
|
||||
ActionBar supportActionBar = activity.getSupportActionBar();
|
||||
final ActionBar supportActionBar = activity.getSupportActionBar();
|
||||
if (supportActionBar != null) {
|
||||
supportActionBar.setDisplayHomeAsUpEnabled(false);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(MenuItem item) {
|
||||
public boolean onOptionsItemSelected(final MenuItem item) {
|
||||
switch (item.getItemId()) {
|
||||
case R.id.action_search:
|
||||
try {
|
||||
NavigationHelper.openSearchFragment(
|
||||
getFragmentManager(),
|
||||
ServiceHelper.getSelectedServiceId(activity),
|
||||
"");
|
||||
} catch (Exception e) {
|
||||
NavigationHelper.openSearchFragment(getFM(),
|
||||
ServiceHelper.getSelectedServiceId(activity), "");
|
||||
} catch (final Exception e) {
|
||||
ErrorActivity.reportUiError((AppCompatActivity) getActivity(), e);
|
||||
}
|
||||
return true;
|
||||
@@ -141,7 +168,8 @@ public class MainFragment extends BaseFragment implements TabLayout.OnTabSelecte
|
||||
tabsList.addAll(tabsManager.getTabs());
|
||||
|
||||
if (pagerAdapter == null || !pagerAdapter.sameTabs(tabsList)) {
|
||||
pagerAdapter = new SelectedTabsPagerAdapter(requireContext(), getChildFragmentManager(), tabsList);
|
||||
pagerAdapter = new SelectedTabsPagerAdapter(requireContext(),
|
||||
getChildFragmentManager(), tabsList);
|
||||
}
|
||||
|
||||
viewPager.setAdapter(null);
|
||||
@@ -165,31 +193,37 @@ public class MainFragment extends BaseFragment implements TabLayout.OnTabSelecte
|
||||
}
|
||||
}
|
||||
|
||||
private void updateTitleForTab(int tabPosition) {
|
||||
private void updateTitleForTab(final 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 + "]");
|
||||
public void onTabSelected(final TabLayout.Tab selectedTab) {
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "onTabSelected() called with: selectedTab = [" + selectedTab + "]");
|
||||
}
|
||||
updateTitleForTab(selectedTab.getPosition());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTabUnselected(TabLayout.Tab tab) {
|
||||
}
|
||||
public void onTabUnselected(final TabLayout.Tab tab) { }
|
||||
|
||||
@Override
|
||||
public void onTabReselected(TabLayout.Tab tab) {
|
||||
if (DEBUG) Log.d(TAG, "onTabReselected() called with: tab = [" + tab + "]");
|
||||
public void onTabReselected(final TabLayout.Tab tab) {
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "onTabReselected() called with: tab = [" + tab + "]");
|
||||
}
|
||||
updateTitleForTab(tab.getPosition());
|
||||
}
|
||||
|
||||
private static class SelectedTabsPagerAdapter extends FragmentStatePagerAdapterMenuWorkaround {
|
||||
private static final class SelectedTabsPagerAdapter
|
||||
extends FragmentStatePagerAdapterMenuWorkaround {
|
||||
private final Context context;
|
||||
private final List<Tab> internalTabsList;
|
||||
|
||||
private SelectedTabsPagerAdapter(Context context, FragmentManager fragmentManager, List<Tab> tabsList) {
|
||||
private SelectedTabsPagerAdapter(final Context context,
|
||||
final FragmentManager fragmentManager,
|
||||
final List<Tab> tabsList) {
|
||||
super(fragmentManager, BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT);
|
||||
this.context = context;
|
||||
this.internalTabsList = new ArrayList<>(tabsList);
|
||||
@@ -197,20 +231,20 @@ public class MainFragment extends BaseFragment implements TabLayout.OnTabSelecte
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public Fragment getItem(int position) {
|
||||
public Fragment getItem(final int position) {
|
||||
final Tab tab = internalTabsList.get(position);
|
||||
|
||||
Throwable throwable = null;
|
||||
Fragment fragment = null;
|
||||
try {
|
||||
fragment = tab.getFragment(context);
|
||||
} catch (ExtractionException e) {
|
||||
} catch (final ExtractionException e) {
|
||||
throwable = e;
|
||||
}
|
||||
|
||||
if (throwable != null) {
|
||||
ErrorActivity.reportError(context, throwable, null, null,
|
||||
ErrorActivity.ErrorInfo.make(UserAction.UI_ERROR, "none", "", R.string.app_ui_crash));
|
||||
ErrorActivity.reportError(context, throwable, null, null, ErrorInfo
|
||||
.make(UserAction.UI_ERROR, "none", "", R.string.app_ui_crash));
|
||||
return new BlankFragment();
|
||||
}
|
||||
|
||||
@@ -222,7 +256,7 @@ public class MainFragment extends BaseFragment implements TabLayout.OnTabSelecte
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getItemPosition(Object object) {
|
||||
public int getItemPosition(final Object object) {
|
||||
// Causes adapter to reload all Fragments when
|
||||
// notifyDataSetChanged is called
|
||||
return POSITION_NONE;
|
||||
@@ -233,7 +267,7 @@ public class MainFragment extends BaseFragment implements TabLayout.OnTabSelecte
|
||||
return internalTabsList.size();
|
||||
}
|
||||
|
||||
public boolean sameTabs(List<Tab> tabsToCompare) {
|
||||
public boolean sameTabs(final List<Tab> tabsToCompare) {
|
||||
return internalTabsList.equals(tabsToCompare);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,23 +9,26 @@ import androidx.recyclerview.widget.StaggeredGridLayoutManager;
|
||||
* if the view is scrolled below the last item.
|
||||
*/
|
||||
public abstract class OnScrollBelowItemsListener extends RecyclerView.OnScrollListener {
|
||||
|
||||
@Override
|
||||
public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
|
||||
public void onScrolled(final RecyclerView recyclerView, final int dx, final int dy) {
|
||||
super.onScrolled(recyclerView, dx, dy);
|
||||
if (dy > 0) {
|
||||
int pastVisibleItems = 0, visibleItemCount, totalItemCount;
|
||||
RecyclerView.LayoutManager layoutManager = recyclerView.getLayoutManager();
|
||||
int pastVisibleItems = 0;
|
||||
final RecyclerView.LayoutManager layoutManager = recyclerView.getLayoutManager();
|
||||
|
||||
visibleItemCount = layoutManager.getChildCount();
|
||||
totalItemCount = layoutManager.getItemCount();
|
||||
final int visibleItemCount = layoutManager.getChildCount();
|
||||
final int totalItemCount = layoutManager.getItemCount();
|
||||
|
||||
// Already covers the GridLayoutManager case
|
||||
if (layoutManager instanceof LinearLayoutManager) {
|
||||
pastVisibleItems = ((LinearLayoutManager) layoutManager).findFirstVisibleItemPosition();
|
||||
pastVisibleItems = ((LinearLayoutManager) layoutManager)
|
||||
.findFirstVisibleItemPosition();
|
||||
} else if (layoutManager instanceof StaggeredGridLayoutManager) {
|
||||
int[] positions = ((StaggeredGridLayoutManager) layoutManager).findFirstVisibleItemPositions(null);
|
||||
if (positions != null && positions.length > 0) pastVisibleItems = positions[0];
|
||||
final int[] positions = ((StaggeredGridLayoutManager) layoutManager)
|
||||
.findFirstVisibleItemPositions(null);
|
||||
if (positions != null && positions.length > 0) {
|
||||
pastVisibleItems = positions[0];
|
||||
}
|
||||
}
|
||||
|
||||
if ((visibleItemCount + pastVisibleItems) >= totalItemCount) {
|
||||
|
||||
@@ -2,8 +2,11 @@ package org.schabi.newpipe.fragments;
|
||||
|
||||
public interface ViewContract<I> {
|
||||
void showLoading();
|
||||
|
||||
void hideLoading();
|
||||
|
||||
void showEmptyState();
|
||||
|
||||
void showError(String message, boolean showRetryButton);
|
||||
|
||||
void handleResult(I result);
|
||||
|
||||
@@ -1,20 +1,29 @@
|
||||
package org.schabi.newpipe.fragments.detail;
|
||||
|
||||
import org.schabi.newpipe.player.playqueue.PlayQueue;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
class StackItem implements Serializable {
|
||||
private final int serviceId;
|
||||
private String url;
|
||||
private String title;
|
||||
private final String url;
|
||||
private PlayQueue playQueue;
|
||||
|
||||
StackItem(int serviceId, String url, String title) {
|
||||
StackItem(final int serviceId, final String url,
|
||||
final String title, final PlayQueue playQueue) {
|
||||
this.serviceId = serviceId;
|
||||
this.url = url;
|
||||
this.title = title;
|
||||
this.playQueue = playQueue;
|
||||
}
|
||||
|
||||
public void setTitle(String title) {
|
||||
this.title = title;
|
||||
public void setUrl(final String url) {
|
||||
this.url = url;
|
||||
}
|
||||
|
||||
public void setPlayQueue(final PlayQueue queue) {
|
||||
this.playQueue = queue;
|
||||
}
|
||||
|
||||
public int getServiceId() {
|
||||
@@ -25,10 +34,18 @@ class StackItem implements Serializable {
|
||||
return title;
|
||||
}
|
||||
|
||||
public void setTitle(final String title) {
|
||||
this.title = title;
|
||||
}
|
||||
|
||||
public String getUrl() {
|
||||
return url;
|
||||
}
|
||||
|
||||
public PlayQueue getPlayQueue() {
|
||||
return playQueue;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return getServiceId() + ":" + getUrl() + " > " + getTitle();
|
||||
|
||||
@@ -1,27 +1,32 @@
|
||||
package org.schabi.newpipe.fragments.detail;
|
||||
|
||||
import android.view.ViewGroup;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.fragment.app.FragmentManager;
|
||||
import androidx.fragment.app.FragmentPagerAdapter;
|
||||
import android.view.ViewGroup;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
public class TabAdaptor extends FragmentPagerAdapter {
|
||||
|
||||
public class TabAdapter extends FragmentPagerAdapter {
|
||||
private final List<Fragment> mFragmentList = new ArrayList<>();
|
||||
private final List<String> mFragmentTitleList = new ArrayList<>();
|
||||
private final FragmentManager fragmentManager;
|
||||
|
||||
public TabAdaptor(FragmentManager fm) {
|
||||
super(fm);
|
||||
public TabAdapter(final FragmentManager fm) {
|
||||
// if changed to BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT => crash if enqueueing stream in
|
||||
// the background and then clicking on it to open VideoDetailFragment:
|
||||
// "Cannot setMaxLifecycle for Fragment not attached to FragmentManager"
|
||||
super(fm, BEHAVIOR_SET_USER_VISIBLE_HINT);
|
||||
this.fragmentManager = fm;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public Fragment getItem(int position) {
|
||||
public Fragment getItem(final int position) {
|
||||
return mFragmentList.get(position);
|
||||
}
|
||||
|
||||
@@ -30,7 +35,7 @@ public class TabAdaptor extends FragmentPagerAdapter {
|
||||
return mFragmentList.size();
|
||||
}
|
||||
|
||||
public void addFragment(Fragment fragment, String title) {
|
||||
public void addFragment(final Fragment fragment, final String title) {
|
||||
mFragmentList.add(fragment);
|
||||
mFragmentTitleList.add(title);
|
||||
}
|
||||
@@ -40,46 +45,51 @@ public class TabAdaptor extends FragmentPagerAdapter {
|
||||
mFragmentTitleList.clear();
|
||||
}
|
||||
|
||||
public void removeItem(int position){
|
||||
public void removeItem(final int position) {
|
||||
mFragmentList.remove(position == 0 ? 0 : position - 1);
|
||||
mFragmentTitleList.remove(position == 0 ? 0 : position - 1);
|
||||
}
|
||||
|
||||
public void updateItem(int position, Fragment fragment){
|
||||
public void updateItem(final int position, final Fragment fragment) {
|
||||
mFragmentList.set(position, fragment);
|
||||
}
|
||||
|
||||
public void updateItem(String title, Fragment fragment){
|
||||
int index = mFragmentTitleList.indexOf(title);
|
||||
if(index != -1){
|
||||
public void updateItem(final String title, final Fragment fragment) {
|
||||
final int index = mFragmentTitleList.indexOf(title);
|
||||
if (index != -1) {
|
||||
updateItem(index, fragment);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getItemPosition(Object object) {
|
||||
if (mFragmentList.contains(object)) return mFragmentList.indexOf(object);
|
||||
else return POSITION_NONE;
|
||||
public int getItemPosition(@NonNull final Object object) {
|
||||
if (mFragmentList.contains(object)) {
|
||||
return mFragmentList.indexOf(object);
|
||||
} else {
|
||||
return POSITION_NONE;
|
||||
}
|
||||
}
|
||||
|
||||
public int getItemPositionByTitle(String title) {
|
||||
public int getItemPositionByTitle(final String title) {
|
||||
return mFragmentTitleList.indexOf(title);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public String getItemTitle(int position) {
|
||||
public String getItemTitle(final int position) {
|
||||
if (position < 0 || position >= mFragmentTitleList.size()) {
|
||||
return null;
|
||||
}
|
||||
return mFragmentTitleList.get(position);
|
||||
}
|
||||
|
||||
public void notifyDataSetUpdate(){
|
||||
public void notifyDataSetUpdate() {
|
||||
notifyDataSetChanged();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void destroyItem(ViewGroup container, int position, Object object) {
|
||||
public void destroyItem(@NonNull final ViewGroup container,
|
||||
final int position,
|
||||
@NonNull final Object object) {
|
||||
fragmentManager.beginTransaction().remove((Fragment) object).commitNowAllowingStateLoss();
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -6,18 +6,18 @@ import android.content.SharedPreferences;
|
||||
import android.content.res.Configuration;
|
||||
import android.content.res.Resources;
|
||||
import android.os.Bundle;
|
||||
import android.preference.PreferenceManager;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.appcompat.app.ActionBar;
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
import androidx.recyclerview.widget.GridLayoutManager;
|
||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
import android.util.Log;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuInflater;
|
||||
import android.view.View;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.appcompat.app.ActionBar;
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
import androidx.preference.PreferenceManager;
|
||||
import androidx.recyclerview.widget.GridLayoutManager;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.extractor.InfoItem;
|
||||
import org.schabi.newpipe.extractor.channel.ChannelInfoItem;
|
||||
@@ -29,18 +29,29 @@ import org.schabi.newpipe.fragments.BaseStateFragment;
|
||||
import org.schabi.newpipe.fragments.OnScrollBelowItemsListener;
|
||||
import org.schabi.newpipe.info_list.InfoItemDialog;
|
||||
import org.schabi.newpipe.info_list.InfoListAdapter;
|
||||
import org.schabi.newpipe.player.helper.PlayerHolder;
|
||||
import org.schabi.newpipe.report.ErrorActivity;
|
||||
import org.schabi.newpipe.util.NavigationHelper;
|
||||
import org.schabi.newpipe.util.OnClickGesture;
|
||||
import org.schabi.newpipe.util.StateSaver;
|
||||
import org.schabi.newpipe.util.StreamDialogEntry;
|
||||
import org.schabi.newpipe.views.SuperScrollLayoutManager;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Queue;
|
||||
|
||||
import static org.schabi.newpipe.util.AnimationUtils.animateView;
|
||||
|
||||
public abstract class BaseListFragment<I, N> extends BaseStateFragment<I> implements ListViewContract<I, N>, StateSaver.WriteRead, SharedPreferences.OnSharedPreferenceChangeListener {
|
||||
public abstract class BaseListFragment<I, N> extends BaseStateFragment<I>
|
||||
implements ListViewContract<I, N>, StateSaver.WriteRead,
|
||||
SharedPreferences.OnSharedPreferenceChangeListener {
|
||||
private static final int LIST_MODE_UPDATE_FLAG = 0x32;
|
||||
protected org.schabi.newpipe.util.SavedState savedState;
|
||||
|
||||
private boolean useDefaultStateSaving = true;
|
||||
private int updateFlags = 0;
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Views
|
||||
@@ -48,16 +59,14 @@ public abstract class BaseListFragment<I, N> extends BaseStateFragment<I> implem
|
||||
|
||||
protected InfoListAdapter infoListAdapter;
|
||||
protected RecyclerView itemsList;
|
||||
private int updateFlags = 0;
|
||||
|
||||
private static final int LIST_MODE_UPDATE_FLAG = 0x32;
|
||||
private int focusedPosition = -1;
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// LifeCycle
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
@Override
|
||||
public void onAttach(Context context) {
|
||||
public void onAttach(final Context context) {
|
||||
super.onAttach(context);
|
||||
|
||||
if (infoListAdapter == null) {
|
||||
@@ -71,7 +80,7 @@ public abstract class BaseListFragment<I, N> extends BaseStateFragment<I> implem
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState) {
|
||||
public void onCreate(final Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
setHasOptionsMenu(true);
|
||||
PreferenceManager.getDefaultSharedPreferences(activity)
|
||||
@@ -81,7 +90,9 @@ public abstract class BaseListFragment<I, N> extends BaseStateFragment<I> implem
|
||||
@Override
|
||||
public void onDestroy() {
|
||||
super.onDestroy();
|
||||
if (useDefaultStateSaving) StateSaver.onDestroy(savedState);
|
||||
if (useDefaultStateSaving) {
|
||||
StateSaver.onDestroy(savedState);
|
||||
}
|
||||
PreferenceManager.getDefaultSharedPreferences(activity)
|
||||
.unregisterOnSharedPreferenceChangeListener(this);
|
||||
}
|
||||
@@ -93,8 +104,9 @@ public abstract class BaseListFragment<I, N> extends BaseStateFragment<I> implem
|
||||
if (updateFlags != 0) {
|
||||
if ((updateFlags & LIST_MODE_UPDATE_FLAG) != 0) {
|
||||
final boolean useGrid = isGridLayout();
|
||||
itemsList.setLayoutManager(useGrid ? getGridLayoutManager() : getListLayoutManager());
|
||||
infoListAdapter.setGridItemVariants(useGrid);
|
||||
itemsList.setLayoutManager(useGrid
|
||||
? getGridLayoutManager() : getListLayoutManager());
|
||||
infoListAdapter.setUseGridVariant(useGrid);
|
||||
infoListAdapter.notifyDataSetChanged();
|
||||
}
|
||||
updateFlags = 0;
|
||||
@@ -105,16 +117,14 @@ public abstract class BaseListFragment<I, N> extends BaseStateFragment<I> implem
|
||||
// State Saving
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
protected StateSaver.SavedState savedState;
|
||||
protected boolean useDefaultStateSaving = true;
|
||||
|
||||
/**
|
||||
* If the default implementation of {@link StateSaver.WriteRead} should be used.
|
||||
*
|
||||
* @see StateSaver
|
||||
* @param useDefaultStateSaving Whether the default implementation should be used
|
||||
*/
|
||||
public void useDefaultStateSaving(boolean useDefault) {
|
||||
this.useDefaultStateSaving = useDefault;
|
||||
public void setUseDefaultStateSaving(final boolean useDefaultStateSaving) {
|
||||
this.useDefaultStateSaving = useDefaultStateSaving;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -123,30 +133,81 @@ public abstract class BaseListFragment<I, N> extends BaseStateFragment<I> implem
|
||||
return "." + infoListAdapter.getItemsList().size() + ".list";
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeTo(Queue<Object> objectsToSave) {
|
||||
if (useDefaultStateSaving) objectsToSave.add(infoListAdapter.getItemsList());
|
||||
}
|
||||
|
||||
@Override
|
||||
@SuppressWarnings("unchecked")
|
||||
public void readFrom(@NonNull Queue<Object> savedObjects) throws Exception {
|
||||
if (useDefaultStateSaving) {
|
||||
infoListAdapter.getItemsList().clear();
|
||||
infoListAdapter.getItemsList().addAll((List<InfoItem>) savedObjects.poll());
|
||||
private int getFocusedPosition() {
|
||||
try {
|
||||
final View focusedItem = itemsList.getFocusedChild();
|
||||
final RecyclerView.ViewHolder itemHolder =
|
||||
itemsList.findContainingViewHolder(focusedItem);
|
||||
return itemHolder.getAdapterPosition();
|
||||
} catch (final NullPointerException e) {
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSaveInstanceState(Bundle bundle) {
|
||||
super.onSaveInstanceState(bundle);
|
||||
if (useDefaultStateSaving) savedState = StateSaver.tryToSave(activity.isChangingConfigurations(), savedState, bundle, this);
|
||||
public void writeTo(final Queue<Object> objectsToSave) {
|
||||
if (!useDefaultStateSaving) {
|
||||
return;
|
||||
}
|
||||
|
||||
objectsToSave.add(infoListAdapter.getItemsList());
|
||||
objectsToSave.add(getFocusedPosition());
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onRestoreInstanceState(@NonNull Bundle bundle) {
|
||||
@SuppressWarnings("unchecked")
|
||||
public void readFrom(@NonNull final Queue<Object> savedObjects) throws Exception {
|
||||
if (!useDefaultStateSaving) {
|
||||
return;
|
||||
}
|
||||
|
||||
infoListAdapter.getItemsList().clear();
|
||||
infoListAdapter.getItemsList().addAll((List<InfoItem>) savedObjects.poll());
|
||||
restoreFocus((Integer) savedObjects.poll());
|
||||
}
|
||||
|
||||
private void restoreFocus(final Integer position) {
|
||||
if (position == null || position < 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
itemsList.post(() -> {
|
||||
final RecyclerView.ViewHolder focusedHolder =
|
||||
itemsList.findViewHolderForAdapterPosition(position);
|
||||
|
||||
if (focusedHolder != null) {
|
||||
focusedHolder.itemView.requestFocus();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSaveInstanceState(final Bundle bundle) {
|
||||
super.onSaveInstanceState(bundle);
|
||||
if (useDefaultStateSaving) {
|
||||
savedState = StateSaver
|
||||
.tryToSave(activity.isChangingConfigurations(), savedState, bundle, this);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onRestoreInstanceState(@NonNull final Bundle bundle) {
|
||||
super.onRestoreInstanceState(bundle);
|
||||
if (useDefaultStateSaving) savedState = StateSaver.tryToRestore(bundle, this);
|
||||
if (useDefaultStateSaving) {
|
||||
savedState = StateSaver.tryToRestore(bundle, this);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStop() {
|
||||
focusedPosition = getFocusedPosition();
|
||||
super.onStop();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStart() {
|
||||
super.onStart();
|
||||
restoreFocus(focusedPosition);
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
@@ -162,36 +223,39 @@ public abstract class BaseListFragment<I, N> extends BaseStateFragment<I> implem
|
||||
}
|
||||
|
||||
protected RecyclerView.LayoutManager getListLayoutManager() {
|
||||
return new LinearLayoutManager(activity);
|
||||
return new SuperScrollLayoutManager(activity);
|
||||
}
|
||||
|
||||
protected RecyclerView.LayoutManager getGridLayoutManager() {
|
||||
final Resources resources = activity.getResources();
|
||||
int width = resources.getDimensionPixelSize(R.dimen.video_item_grid_thumbnail_image_width);
|
||||
width += (24 * resources.getDisplayMetrics().density);
|
||||
final int spanCount = (int) Math.floor(resources.getDisplayMetrics().widthPixels / (double)width);
|
||||
final int spanCount = (int) Math.floor(resources.getDisplayMetrics().widthPixels
|
||||
/ (double) width);
|
||||
final GridLayoutManager lm = new GridLayoutManager(activity, spanCount);
|
||||
lm.setSpanSizeLookup(infoListAdapter.getSpanSizeLookup(spanCount));
|
||||
return lm;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void initViews(View rootView, Bundle savedInstanceState) {
|
||||
protected void initViews(final View rootView, final Bundle savedInstanceState) {
|
||||
super.initViews(rootView, savedInstanceState);
|
||||
|
||||
final boolean useGrid = isGridLayout();
|
||||
itemsList = rootView.findViewById(R.id.items_list);
|
||||
itemsList.setLayoutManager(useGrid ? getGridLayoutManager() : getListLayoutManager());
|
||||
|
||||
infoListAdapter.setGridItemVariants(useGrid);
|
||||
infoListAdapter.setUseGridVariant(useGrid);
|
||||
infoListAdapter.setFooter(getListFooter());
|
||||
infoListAdapter.setHeader(getListHeader());
|
||||
|
||||
itemsList.setAdapter(infoListAdapter);
|
||||
}
|
||||
|
||||
protected void onItemSelected(InfoItem selectedItem) {
|
||||
if (DEBUG) Log.d(TAG, "onItemSelected() called with: selectedItem = [" + selectedItem + "]");
|
||||
protected void onItemSelected(final InfoItem selectedItem) {
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "onItemSelected() called with: selectedItem = [" + selectedItem + "]");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -199,26 +263,26 @@ public abstract class BaseListFragment<I, N> extends BaseStateFragment<I> implem
|
||||
super.initListeners();
|
||||
infoListAdapter.setOnStreamSelectedListener(new OnClickGesture<StreamInfoItem>() {
|
||||
@Override
|
||||
public void selected(StreamInfoItem selectedItem) {
|
||||
public void selected(final StreamInfoItem selectedItem) {
|
||||
onStreamSelected(selectedItem);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void held(StreamInfoItem selectedItem) {
|
||||
public void held(final StreamInfoItem selectedItem) {
|
||||
showStreamDialog(selectedItem);
|
||||
}
|
||||
});
|
||||
|
||||
infoListAdapter.setOnChannelSelectedListener(new OnClickGesture<ChannelInfoItem>() {
|
||||
@Override
|
||||
public void selected(ChannelInfoItem selectedItem) {
|
||||
public void selected(final ChannelInfoItem selectedItem) {
|
||||
try {
|
||||
onItemSelected(selectedItem);
|
||||
NavigationHelper.openChannelFragment(getFM(),
|
||||
selectedItem.getServiceId(),
|
||||
selectedItem.getUrl(),
|
||||
selectedItem.getName());
|
||||
} catch (Exception e) {
|
||||
} catch (final Exception e) {
|
||||
ErrorActivity.reportUiError((AppCompatActivity) getActivity(), e);
|
||||
}
|
||||
}
|
||||
@@ -226,14 +290,14 @@ public abstract class BaseListFragment<I, N> extends BaseStateFragment<I> implem
|
||||
|
||||
infoListAdapter.setOnPlaylistSelectedListener(new OnClickGesture<PlaylistInfoItem>() {
|
||||
@Override
|
||||
public void selected(PlaylistInfoItem selectedItem) {
|
||||
public void selected(final PlaylistInfoItem selectedItem) {
|
||||
try {
|
||||
onItemSelected(selectedItem);
|
||||
NavigationHelper.openPlaylistFragment(getFM(),
|
||||
selectedItem.getServiceId(),
|
||||
selectedItem.getUrl(),
|
||||
selectedItem.getName());
|
||||
} catch (Exception e) {
|
||||
} catch (final Exception e) {
|
||||
ErrorActivity.reportUiError((AppCompatActivity) getActivity(), e);
|
||||
}
|
||||
}
|
||||
@@ -241,7 +305,7 @@ public abstract class BaseListFragment<I, N> extends BaseStateFragment<I> implem
|
||||
|
||||
infoListAdapter.setOnCommentsSelectedListener(new OnClickGesture<CommentsInfoItem>() {
|
||||
@Override
|
||||
public void selected(CommentsInfoItem selectedItem) {
|
||||
public void selected(final CommentsInfoItem selectedItem) {
|
||||
onItemSelected(selectedItem);
|
||||
}
|
||||
});
|
||||
@@ -249,16 +313,17 @@ public abstract class BaseListFragment<I, N> extends BaseStateFragment<I> implem
|
||||
itemsList.clearOnScrollListeners();
|
||||
itemsList.addOnScrollListener(new OnScrollBelowItemsListener() {
|
||||
@Override
|
||||
public void onScrolledDown(RecyclerView recyclerView) {
|
||||
public void onScrolledDown(final RecyclerView recyclerView) {
|
||||
onScrollToBottom();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void onStreamSelected(StreamInfoItem selectedItem) {
|
||||
private void onStreamSelected(final StreamInfoItem selectedItem) {
|
||||
onItemSelected(selectedItem);
|
||||
NavigationHelper.openVideoDetailFragment(getFM(),
|
||||
selectedItem.getServiceId(), selectedItem.getUrl(), selectedItem.getName());
|
||||
NavigationHelper.openVideoDetailFragment(requireContext(), getFM(),
|
||||
selectedItem.getServiceId(), selectedItem.getUrl(), selectedItem.getName(),
|
||||
null, false);
|
||||
}
|
||||
|
||||
protected void onScrollToBottom() {
|
||||
@@ -268,31 +333,36 @@ public abstract class BaseListFragment<I, N> extends BaseStateFragment<I> implem
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
protected void showStreamDialog(final StreamInfoItem item) {
|
||||
final Context context = getContext();
|
||||
final Activity activity = getActivity();
|
||||
if (context == null || context.getResources() == null || activity == null) return;
|
||||
if (context == null || context.getResources() == null || activity == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
final ArrayList<StreamDialogEntry> entries = new ArrayList<>();
|
||||
|
||||
if (PlayerHolder.getType() != null) {
|
||||
entries.add(StreamDialogEntry.enqueue);
|
||||
}
|
||||
if (item.getStreamType() == StreamType.AUDIO_STREAM) {
|
||||
StreamDialogEntry.setEnabledEntries(
|
||||
StreamDialogEntry.enqueue_on_background,
|
||||
entries.addAll(Arrays.asList(
|
||||
StreamDialogEntry.start_here_on_background,
|
||||
StreamDialogEntry.append_playlist,
|
||||
StreamDialogEntry.share);
|
||||
} else {
|
||||
StreamDialogEntry.setEnabledEntries(
|
||||
StreamDialogEntry.enqueue_on_background,
|
||||
StreamDialogEntry.enqueue_on_popup,
|
||||
StreamDialogEntry.share
|
||||
));
|
||||
} else {
|
||||
entries.addAll(Arrays.asList(
|
||||
StreamDialogEntry.start_here_on_background,
|
||||
StreamDialogEntry.start_here_on_popup,
|
||||
StreamDialogEntry.append_playlist,
|
||||
StreamDialogEntry.share);
|
||||
StreamDialogEntry.share
|
||||
));
|
||||
}
|
||||
StreamDialogEntry.setEnabledEntries(entries);
|
||||
|
||||
new InfoItemDialog(activity, item, StreamDialogEntry.getCommands(context), (dialog, which) ->
|
||||
StreamDialogEntry.clickOn(which, this, item)).show();
|
||||
new InfoItemDialog(activity, item, StreamDialogEntry.getCommands(context),
|
||||
(dialog, which) -> StreamDialogEntry.clickOn(which, this, item)).show();
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
@@ -300,17 +370,16 @@ public abstract class BaseListFragment<I, N> extends BaseStateFragment<I> implem
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
@Override
|
||||
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
|
||||
if (DEBUG) Log.d(TAG, "onCreateOptionsMenu() called with: menu = [" + menu + "], inflater = [" + inflater + "]");
|
||||
public void onCreateOptionsMenu(final Menu menu, final MenuInflater inflater) {
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "onCreateOptionsMenu() called with: "
|
||||
+ "menu = [" + menu + "], inflater = [" + inflater + "]");
|
||||
}
|
||||
super.onCreateOptionsMenu(menu, inflater);
|
||||
ActionBar supportActionBar = activity.getSupportActionBar();
|
||||
final ActionBar supportActionBar = activity.getSupportActionBar();
|
||||
if (supportActionBar != null) {
|
||||
supportActionBar.setDisplayShowTitleEnabled(true);
|
||||
if (useAsFrontPage) {
|
||||
supportActionBar.setDisplayHomeAsUpEnabled(false);
|
||||
} else {
|
||||
supportActionBar.setDisplayHomeAsUpEnabled(true);
|
||||
}
|
||||
supportActionBar.setDisplayHomeAsUpEnabled(!useAsFrontPage);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -339,7 +408,7 @@ public abstract class BaseListFragment<I, N> extends BaseStateFragment<I> implem
|
||||
}
|
||||
|
||||
@Override
|
||||
public void showError(String message, boolean showRetryButton) {
|
||||
public void showError(final String message, final boolean showRetryButton) {
|
||||
super.showError(message, showRetryButton);
|
||||
showListFooter(false);
|
||||
animateView(itemsList, false, 200);
|
||||
@@ -361,25 +430,28 @@ public abstract class BaseListFragment<I, N> extends BaseStateFragment<I> implem
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleNextItems(N result) {
|
||||
public void handleNextItems(final N result) {
|
||||
isLoading.set(false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) {
|
||||
public void onSharedPreferenceChanged(final SharedPreferences sharedPreferences,
|
||||
final String key) {
|
||||
if (key.equals(getString(R.string.list_view_mode_key))) {
|
||||
updateFlags |= LIST_MODE_UPDATE_FLAG;
|
||||
}
|
||||
}
|
||||
|
||||
protected boolean isGridLayout() {
|
||||
final String list_mode = PreferenceManager.getDefaultSharedPreferences(activity).getString(getString(R.string.list_view_mode_key), getString(R.string.list_view_mode_value));
|
||||
if ("auto".equals(list_mode)) {
|
||||
final String listMode = PreferenceManager.getDefaultSharedPreferences(activity)
|
||||
.getString(getString(R.string.list_view_mode_key),
|
||||
getString(R.string.list_view_mode_value));
|
||||
if ("auto".equals(listMode)) {
|
||||
final Configuration configuration = getResources().getConfiguration();
|
||||
return configuration.orientation == Configuration.ORIENTATION_LANDSCAPE
|
||||
&& configuration.isLayoutSizeAtLeast(Configuration.SCREENLAYOUT_SIZE_LARGE);
|
||||
} else {
|
||||
return "grid".equals(list_mode);
|
||||
return "grid".equals(listMode);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,19 +9,20 @@ import androidx.annotation.NonNull;
|
||||
|
||||
import org.schabi.newpipe.extractor.ListExtractor;
|
||||
import org.schabi.newpipe.extractor.ListInfo;
|
||||
import org.schabi.newpipe.extractor.Page;
|
||||
import org.schabi.newpipe.util.Constants;
|
||||
import org.schabi.newpipe.views.NewPipeRecyclerView;
|
||||
|
||||
import java.util.Queue;
|
||||
|
||||
import icepick.State;
|
||||
import io.reactivex.Single;
|
||||
import io.reactivex.android.schedulers.AndroidSchedulers;
|
||||
import io.reactivex.disposables.Disposable;
|
||||
import io.reactivex.schedulers.Schedulers;
|
||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
|
||||
import io.reactivex.rxjava3.core.Single;
|
||||
import io.reactivex.rxjava3.disposables.Disposable;
|
||||
import io.reactivex.rxjava3.schedulers.Schedulers;
|
||||
|
||||
public abstract class BaseListInfoFragment<I extends ListInfo>
|
||||
extends BaseListFragment<I, ListExtractor.InfoItemsPage> {
|
||||
|
||||
@State
|
||||
protected int serviceId = Constants.NO_SERVICE_ID;
|
||||
@State
|
||||
@@ -30,11 +31,11 @@ public abstract class BaseListInfoFragment<I extends ListInfo>
|
||||
protected String url;
|
||||
|
||||
protected I currentInfo;
|
||||
protected String currentNextPageUrl;
|
||||
protected Page currentNextPage;
|
||||
protected Disposable currentWorker;
|
||||
|
||||
@Override
|
||||
protected void initViews(View rootView, Bundle savedInstanceState) {
|
||||
protected void initViews(final View rootView, final Bundle savedInstanceState) {
|
||||
super.initViews(rootView, savedInstanceState);
|
||||
setTitle(name);
|
||||
showListFooter(hasMoreItems());
|
||||
@@ -43,7 +44,9 @@ public abstract class BaseListInfoFragment<I extends ListInfo>
|
||||
@Override
|
||||
public void onPause() {
|
||||
super.onPause();
|
||||
if (currentWorker != null) currentWorker.dispose();
|
||||
if (currentWorker != null) {
|
||||
currentWorker.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -73,18 +76,18 @@ public abstract class BaseListInfoFragment<I extends ListInfo>
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
@Override
|
||||
public void writeTo(Queue<Object> objectsToSave) {
|
||||
public void writeTo(final Queue<Object> objectsToSave) {
|
||||
super.writeTo(objectsToSave);
|
||||
objectsToSave.add(currentInfo);
|
||||
objectsToSave.add(currentNextPageUrl);
|
||||
objectsToSave.add(currentNextPage);
|
||||
}
|
||||
|
||||
@Override
|
||||
@SuppressWarnings("unchecked")
|
||||
public void readFrom(@NonNull Queue<Object> savedObjects) throws Exception {
|
||||
public void readFrom(@NonNull final Queue<Object> savedObjects) throws Exception {
|
||||
super.readFrom(savedObjects);
|
||||
currentInfo = (I) savedObjects.poll();
|
||||
currentNextPageUrl = (String) savedObjects.poll();
|
||||
currentNextPage = (Page) savedObjects.poll();
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
@@ -92,10 +95,14 @@ public abstract class BaseListInfoFragment<I extends ListInfo>
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
protected void doInitialLoadLogic() {
|
||||
if (DEBUG) Log.d(TAG, "doInitialLoadLogic() called");
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "doInitialLoadLogic() called");
|
||||
}
|
||||
if (currentInfo == null) {
|
||||
startLoading(false);
|
||||
} else handleResult(currentInfo);
|
||||
} else {
|
||||
handleResult(currentInfo);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -103,55 +110,79 @@ public abstract class BaseListInfoFragment<I extends ListInfo>
|
||||
* You can use the default implementations from {@link org.schabi.newpipe.util.ExtractorHelper}.
|
||||
*
|
||||
* @param forceLoad allow or disallow the result to come from the cache
|
||||
* @return Rx {@link Single} containing the {@link ListInfo}
|
||||
*/
|
||||
protected abstract Single<I> loadResult(boolean forceLoad);
|
||||
|
||||
@Override
|
||||
public void startLoading(boolean forceLoad) {
|
||||
public void startLoading(final boolean forceLoad) {
|
||||
super.startLoading(forceLoad);
|
||||
|
||||
showListFooter(false);
|
||||
infoListAdapter.clearStreamItemList();
|
||||
|
||||
currentInfo = null;
|
||||
if (currentWorker != null) currentWorker.dispose();
|
||||
if (currentWorker != null) {
|
||||
currentWorker.dispose();
|
||||
}
|
||||
currentWorker = loadResult(forceLoad)
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe((@NonNull I result) -> {
|
||||
isLoading.set(false);
|
||||
currentInfo = result;
|
||||
currentNextPageUrl = result.getNextPageUrl();
|
||||
currentNextPage = result.getNextPage();
|
||||
handleResult(result);
|
||||
}, (@NonNull Throwable throwable) -> onError(throwable));
|
||||
}, this::onError);
|
||||
}
|
||||
|
||||
/**
|
||||
* Implement the logic to load more items<br/>
|
||||
* You can use the default implementations from {@link org.schabi.newpipe.util.ExtractorHelper}
|
||||
* Implement the logic to load more items.
|
||||
* <p>You can use the default implementations
|
||||
* from {@link org.schabi.newpipe.util.ExtractorHelper}.</p>
|
||||
*
|
||||
* @return Rx {@link Single} containing the {@link ListExtractor.InfoItemsPage}
|
||||
*/
|
||||
protected abstract Single<ListExtractor.InfoItemsPage> loadMoreItemsLogic();
|
||||
|
||||
protected void loadMoreItems() {
|
||||
isLoading.set(true);
|
||||
|
||||
if (currentWorker != null) currentWorker.dispose();
|
||||
if (currentWorker != null) {
|
||||
currentWorker.dispose();
|
||||
}
|
||||
|
||||
forbidDownwardFocusScroll();
|
||||
|
||||
currentWorker = loadMoreItemsLogic()
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe((@io.reactivex.annotations.NonNull ListExtractor.InfoItemsPage InfoItemsPage) -> {
|
||||
.doFinally(this::allowDownwardFocusScroll)
|
||||
.subscribe((@NonNull ListExtractor.InfoItemsPage InfoItemsPage) -> {
|
||||
isLoading.set(false);
|
||||
handleNextItems(InfoItemsPage);
|
||||
}, (@io.reactivex.annotations.NonNull Throwable throwable) -> {
|
||||
}, (@NonNull Throwable throwable) -> {
|
||||
isLoading.set(false);
|
||||
onError(throwable);
|
||||
});
|
||||
}
|
||||
|
||||
private void forbidDownwardFocusScroll() {
|
||||
if (itemsList instanceof NewPipeRecyclerView) {
|
||||
((NewPipeRecyclerView) itemsList).setFocusScrollAllowed(false);
|
||||
}
|
||||
}
|
||||
|
||||
private void allowDownwardFocusScroll() {
|
||||
if (itemsList instanceof NewPipeRecyclerView) {
|
||||
((NewPipeRecyclerView) itemsList).setFocusScrollAllowed(true);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleNextItems(ListExtractor.InfoItemsPage result) {
|
||||
public void handleNextItems(final ListExtractor.InfoItemsPage result) {
|
||||
super.handleNextItems(result);
|
||||
currentNextPageUrl = result.getNextPageUrl();
|
||||
currentNextPage = result.getNextPage();
|
||||
infoListAdapter.addInfoItemList(result.getItems());
|
||||
|
||||
showListFooter(hasMoreItems());
|
||||
@@ -159,7 +190,7 @@ public abstract class BaseListInfoFragment<I extends ListInfo>
|
||||
|
||||
@Override
|
||||
protected boolean hasMoreItems() {
|
||||
return !TextUtils.isEmpty(currentNextPageUrl);
|
||||
return Page.isValid(currentNextPage);
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
@@ -167,13 +198,13 @@ public abstract class BaseListInfoFragment<I extends ListInfo>
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
@Override
|
||||
public void handleResult(@NonNull I result) {
|
||||
public void handleResult(@NonNull final I result) {
|
||||
super.handleResult(result);
|
||||
|
||||
name = result.getName();
|
||||
setTitle(name);
|
||||
|
||||
if (infoListAdapter.getItemsList().size() == 0) {
|
||||
if (infoListAdapter.getItemsList().isEmpty()) {
|
||||
if (result.getRelatedItems().size() > 0) {
|
||||
infoListAdapter.addInfoItemList(result.getRelatedItems());
|
||||
showListFooter(hasMoreItems());
|
||||
@@ -188,9 +219,9 @@ public abstract class BaseListInfoFragment<I extends ListInfo>
|
||||
// Utils
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
protected void setInitialData(int serviceId, String url, String name) {
|
||||
this.serviceId = serviceId;
|
||||
this.url = url;
|
||||
this.name = !TextUtils.isEmpty(name) ? name : "";
|
||||
protected void setInitialData(final int sid, final String u, final String title) {
|
||||
this.serviceId = sid;
|
||||
this.url = u;
|
||||
this.name = !TextUtils.isEmpty(title) ? title : "";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,12 +4,9 @@ import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.core.content.ContextCompat;
|
||||
import androidx.appcompat.app.ActionBar;
|
||||
import android.text.TextUtils;
|
||||
import android.util.Log;
|
||||
import android.util.TypedValue;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuInflater;
|
||||
@@ -21,7 +18,13 @@ import android.widget.ImageView;
|
||||
import android.widget.LinearLayout;
|
||||
import android.widget.TextView;
|
||||
|
||||
import com.jakewharton.rxbinding2.view.RxView;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.appcompat.app.ActionBar;
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
import androidx.core.content.ContextCompat;
|
||||
|
||||
import com.jakewharton.rxbinding4.view.RxView;
|
||||
|
||||
import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.database.subscription.SubscriptionEntity;
|
||||
@@ -29,13 +32,14 @@ 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.ContentNotSupportedException;
|
||||
import org.schabi.newpipe.extractor.exceptions.ExtractionException;
|
||||
import org.schabi.newpipe.extractor.stream.StreamInfoItem;
|
||||
import org.schabi.newpipe.fragments.list.BaseListInfoFragment;
|
||||
import org.schabi.newpipe.local.subscription.SubscriptionManager;
|
||||
import org.schabi.newpipe.player.playqueue.ChannelPlayQueue;
|
||||
import org.schabi.newpipe.player.playqueue.PlayQueue;
|
||||
import org.schabi.newpipe.report.ErrorActivity;
|
||||
import org.schabi.newpipe.report.UserAction;
|
||||
import org.schabi.newpipe.util.AnimationUtils;
|
||||
import org.schabi.newpipe.util.ExtractorHelper;
|
||||
@@ -43,61 +47,63 @@ import org.schabi.newpipe.util.ImageDisplayConstants;
|
||||
import org.schabi.newpipe.util.Localization;
|
||||
import org.schabi.newpipe.util.NavigationHelper;
|
||||
import org.schabi.newpipe.util.ShareUtils;
|
||||
import org.schabi.newpipe.util.ThemeHelper;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import io.reactivex.Observable;
|
||||
import io.reactivex.Single;
|
||||
import io.reactivex.android.schedulers.AndroidSchedulers;
|
||||
import io.reactivex.disposables.CompositeDisposable;
|
||||
import io.reactivex.disposables.Disposable;
|
||||
import io.reactivex.functions.Action;
|
||||
import io.reactivex.functions.Consumer;
|
||||
import io.reactivex.functions.Function;
|
||||
import io.reactivex.schedulers.Schedulers;
|
||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
|
||||
import io.reactivex.rxjava3.core.Observable;
|
||||
import io.reactivex.rxjava3.core.Single;
|
||||
import io.reactivex.rxjava3.disposables.CompositeDisposable;
|
||||
import io.reactivex.rxjava3.disposables.Disposable;
|
||||
import io.reactivex.rxjava3.functions.Action;
|
||||
import io.reactivex.rxjava3.functions.Consumer;
|
||||
import io.reactivex.rxjava3.functions.Function;
|
||||
import io.reactivex.rxjava3.schedulers.Schedulers;
|
||||
|
||||
import static org.schabi.newpipe.util.AnimationUtils.animateBackgroundColor;
|
||||
import static org.schabi.newpipe.util.AnimationUtils.animateTextColor;
|
||||
import static org.schabi.newpipe.util.AnimationUtils.animateView;
|
||||
|
||||
public class ChannelFragment extends BaseListInfoFragment<ChannelInfo> {
|
||||
|
||||
public class ChannelFragment extends BaseListInfoFragment<ChannelInfo>
|
||||
implements View.OnClickListener {
|
||||
private static final int BUTTON_DEBOUNCE_INTERVAL = 100;
|
||||
private final CompositeDisposable disposables = new CompositeDisposable();
|
||||
private Disposable subscribeButtonMonitor;
|
||||
private SubscriptionManager subscriptionManager;
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Views
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
private SubscriptionManager subscriptionManager;
|
||||
private View headerRootLayout;
|
||||
private ImageView headerChannelBanner;
|
||||
private ImageView headerAvatarView;
|
||||
private TextView headerTitleView;
|
||||
private ImageView headerSubChannelAvatarView;
|
||||
private TextView headerSubChannelTitleView;
|
||||
private TextView headerSubscribersTextView;
|
||||
private Button headerSubscribeButton;
|
||||
private View playlistCtrl;
|
||||
|
||||
private LinearLayout headerPlayAllButton;
|
||||
private LinearLayout headerPopupButton;
|
||||
private LinearLayout headerBackgroundButton;
|
||||
|
||||
private MenuItem menuRssButton;
|
||||
private TextView contentNotSupportedTextView;
|
||||
private TextView kaomojiTextView;
|
||||
private TextView noVideosTextView;
|
||||
|
||||
public static ChannelFragment getInstance(int serviceId, String url, String name) {
|
||||
ChannelFragment instance = new ChannelFragment();
|
||||
public static ChannelFragment getInstance(final int serviceId, final String url,
|
||||
final String name) {
|
||||
final ChannelFragment instance = new ChannelFragment();
|
||||
instance.setInitialData(serviceId, url, name);
|
||||
return instance;
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// LifeCycle
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
@Override
|
||||
public void setUserVisibleHint(boolean isVisibleToUser) {
|
||||
public void setUserVisibleHint(final boolean isVisibleToUser) {
|
||||
super.setUserVisibleHint(isVisibleToUser);
|
||||
if (activity != null
|
||||
&& useAsFrontPage
|
||||
@@ -106,22 +112,40 @@ public class ChannelFragment extends BaseListInfoFragment<ChannelInfo> {
|
||||
}
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// LifeCycle
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
@Override
|
||||
public void onAttach(Context context) {
|
||||
public void onAttach(final Context context) {
|
||||
super.onAttach(context);
|
||||
subscriptionManager = new SubscriptionManager(activity);
|
||||
}
|
||||
|
||||
@Override
|
||||
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
|
||||
public View onCreateView(@NonNull final LayoutInflater inflater,
|
||||
@Nullable final ViewGroup container,
|
||||
@Nullable final Bundle savedInstanceState) {
|
||||
return inflater.inflate(R.layout.fragment_channel, container, false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onViewCreated(final View rootView, final Bundle savedInstanceState) {
|
||||
super.onViewCreated(rootView, savedInstanceState);
|
||||
contentNotSupportedTextView = rootView.findViewById(R.id.error_content_not_supported);
|
||||
kaomojiTextView = rootView.findViewById(R.id.channel_kaomoji);
|
||||
noVideosTextView = rootView.findViewById(R.id.channel_no_videos);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroy() {
|
||||
super.onDestroy();
|
||||
if (disposables != null) disposables.clear();
|
||||
if (subscribeButtonMonitor != null) subscribeButtonMonitor.dispose();
|
||||
if (disposables != null) {
|
||||
disposables.clear();
|
||||
}
|
||||
if (subscribeButtonMonitor != null) {
|
||||
subscribeButtonMonitor.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
@@ -129,14 +153,18 @@ public class ChannelFragment extends BaseListInfoFragment<ChannelInfo> {
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
protected View getListHeader() {
|
||||
headerRootLayout = activity.getLayoutInflater().inflate(R.layout.channel_header, itemsList, false);
|
||||
headerRootLayout = activity.getLayoutInflater()
|
||||
.inflate(R.layout.channel_header, itemsList, false);
|
||||
headerChannelBanner = headerRootLayout.findViewById(R.id.channel_banner_image);
|
||||
headerAvatarView = headerRootLayout.findViewById(R.id.channel_avatar_view);
|
||||
headerTitleView = headerRootLayout.findViewById(R.id.channel_title_view);
|
||||
headerSubscribersTextView = headerRootLayout.findViewById(R.id.channel_subscriber_view);
|
||||
headerSubscribeButton = headerRootLayout.findViewById(R.id.channel_subscribe_button);
|
||||
playlistCtrl = headerRootLayout.findViewById(R.id.playlist_control);
|
||||
|
||||
headerSubChannelAvatarView =
|
||||
headerRootLayout.findViewById(R.id.sub_channel_avatar_view);
|
||||
headerSubChannelTitleView =
|
||||
headerRootLayout.findViewById(R.id.sub_channel_title_view);
|
||||
|
||||
headerPlayAllButton = headerRootLayout.findViewById(R.id.playlist_ctrl_play_all_button);
|
||||
headerPopupButton = headerRootLayout.findViewById(R.id.playlist_ctrl_play_popup_button);
|
||||
@@ -145,21 +173,31 @@ public class ChannelFragment extends BaseListInfoFragment<ChannelInfo> {
|
||||
return headerRootLayout;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void initListeners() {
|
||||
super.initListeners();
|
||||
|
||||
headerSubChannelTitleView.setOnClickListener(this);
|
||||
headerSubChannelAvatarView.setOnClickListener(this);
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Menu
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
@Override
|
||||
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
|
||||
public void onCreateOptionsMenu(final Menu menu, final MenuInflater inflater) {
|
||||
super.onCreateOptionsMenu(menu, inflater);
|
||||
ActionBar supportActionBar = activity.getSupportActionBar();
|
||||
final ActionBar supportActionBar = activity.getSupportActionBar();
|
||||
if (useAsFrontPage && supportActionBar != null) {
|
||||
supportActionBar.setDisplayHomeAsUpEnabled(false);
|
||||
} else {
|
||||
inflater.inflate(R.menu.menu_channel, menu);
|
||||
|
||||
if (DEBUG) Log.d(TAG, "onCreateOptionsMenu() called with: menu = [" + menu +
|
||||
"], inflater = [" + inflater + "]");
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "onCreateOptionsMenu() called with: "
|
||||
+ "menu = [" + menu + "], inflater = [" + inflater + "]");
|
||||
}
|
||||
menuRssButton = menu.findItem(R.id.menu_item_rss);
|
||||
}
|
||||
}
|
||||
@@ -167,13 +205,13 @@ public class ChannelFragment extends BaseListInfoFragment<ChannelInfo> {
|
||||
private void openRssFeed() {
|
||||
final ChannelInfo info = currentInfo;
|
||||
if (info != null) {
|
||||
Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(info.getFeedUrl()));
|
||||
final Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(info.getFeedUrl()));
|
||||
startActivity(intent);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(MenuItem item) {
|
||||
public boolean onOptionsItemSelected(final MenuItem item) {
|
||||
switch (item.getItemId()) {
|
||||
case R.id.action_settings:
|
||||
NavigationHelper.openSettings(requireContext());
|
||||
@@ -201,18 +239,16 @@ public class ChannelFragment extends BaseListInfoFragment<ChannelInfo> {
|
||||
// Channel Subscription
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
private static final int BUTTON_DEBOUNCE_INTERVAL = 100;
|
||||
|
||||
private void monitorSubscription(final ChannelInfo info) {
|
||||
final Consumer<Throwable> onError = (Throwable throwable) -> {
|
||||
animateView(headerSubscribeButton, false, 100);
|
||||
showSnackBarError(throwable, UserAction.SUBSCRIPTION,
|
||||
NewPipe.getNameOfService(currentInfo.getServiceId()),
|
||||
"Get subscription status",
|
||||
0);
|
||||
animateView(headerSubscribeButton, false, 100);
|
||||
showSnackBarError(throwable, UserAction.SUBSCRIPTION,
|
||||
NewPipe.getNameOfService(currentInfo.getServiceId()),
|
||||
"Get subscription status", 0);
|
||||
};
|
||||
|
||||
final Observable<List<SubscriptionEntity>> observable = subscriptionManager.subscriptionTable()
|
||||
final Observable<List<SubscriptionEntity>> observable = subscriptionManager
|
||||
.subscriptionTable()
|
||||
.getSubscriptionFlowable(info.getServiceId(), info.getUrl())
|
||||
.toObservable();
|
||||
|
||||
@@ -221,17 +257,19 @@ public class ChannelFragment extends BaseListInfoFragment<ChannelInfo> {
|
||||
.subscribe(getSubscribeUpdateMonitor(info), onError));
|
||||
|
||||
disposables.add(observable
|
||||
// Some updates are very rapid (when calling the updateSubscription(info), for example)
|
||||
// so only update the UI for the latest emission ("sync" the subscribe button's state)
|
||||
// Some updates are very rapid
|
||||
// (for example when calling the updateSubscription(info))
|
||||
// so only update the UI for the latest emission
|
||||
// ("sync" the subscribe button's state)
|
||||
.debounce(100, TimeUnit.MILLISECONDS)
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe((List<SubscriptionEntity> subscriptionEntities) ->
|
||||
updateSubscribeButton(!subscriptionEntities.isEmpty())
|
||||
, onError));
|
||||
updateSubscribeButton(!subscriptionEntities.isEmpty()), onError));
|
||||
|
||||
}
|
||||
|
||||
private Function<Object, Object> mapOnSubscribe(final SubscriptionEntity subscription, ChannelInfo info) {
|
||||
private Function<Object, Object> mapOnSubscribe(final SubscriptionEntity subscription,
|
||||
final ChannelInfo info) {
|
||||
return (@NonNull Object o) -> {
|
||||
subscriptionManager.insertSubscription(subscription, info);
|
||||
return o;
|
||||
@@ -246,9 +284,13 @@ public class ChannelFragment extends BaseListInfoFragment<ChannelInfo> {
|
||||
}
|
||||
|
||||
private void updateSubscription(final ChannelInfo info) {
|
||||
if (DEBUG) Log.d(TAG, "updateSubscription() called with: info = [" + info + "]");
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "updateSubscription() called with: info = [" + info + "]");
|
||||
}
|
||||
final Action onComplete = () -> {
|
||||
if (DEBUG) Log.d(TAG, "Updated subscription: " + info.getUrl());
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "Updated subscription: " + info.getUrl());
|
||||
}
|
||||
};
|
||||
|
||||
final Consumer<Throwable> onError = (@NonNull Throwable throwable) ->
|
||||
@@ -264,9 +306,12 @@ public class ChannelFragment extends BaseListInfoFragment<ChannelInfo> {
|
||||
.subscribe(onComplete, onError));
|
||||
}
|
||||
|
||||
private Disposable monitorSubscribeButton(final Button subscribeButton, final Function<Object, Object> action) {
|
||||
private Disposable monitorSubscribeButton(final Button subscribeButton,
|
||||
final Function<Object, Object> action) {
|
||||
final Consumer<Object> onNext = (@NonNull Object o) -> {
|
||||
if (DEBUG) Log.d(TAG, "Changed subscription status to this channel!");
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "Changed subscription status to this channel!");
|
||||
}
|
||||
};
|
||||
|
||||
final Consumer<Throwable> onError = (@NonNull Throwable throwable) ->
|
||||
@@ -287,47 +332,64 @@ public class ChannelFragment extends BaseListInfoFragment<ChannelInfo> {
|
||||
|
||||
private Consumer<List<SubscriptionEntity>> getSubscribeUpdateMonitor(final ChannelInfo info) {
|
||||
return (List<SubscriptionEntity> subscriptionEntities) -> {
|
||||
if (DEBUG)
|
||||
Log.d(TAG, "subscriptionManager.subscriptionTable.doOnNext() called with: subscriptionEntities = [" + subscriptionEntities + "]");
|
||||
if (subscribeButtonMonitor != null) subscribeButtonMonitor.dispose();
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "subscriptionManager.subscriptionTable.doOnNext() called with: "
|
||||
+ "subscriptionEntities = [" + subscriptionEntities + "]");
|
||||
}
|
||||
if (subscribeButtonMonitor != null) {
|
||||
subscribeButtonMonitor.dispose();
|
||||
}
|
||||
|
||||
if (subscriptionEntities.isEmpty()) {
|
||||
if (DEBUG) Log.d(TAG, "No subscription to this channel!");
|
||||
SubscriptionEntity channel = new SubscriptionEntity();
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "No subscription to this channel!");
|
||||
}
|
||||
final SubscriptionEntity channel = new SubscriptionEntity();
|
||||
channel.setServiceId(info.getServiceId());
|
||||
channel.setUrl(info.getUrl());
|
||||
channel.setData(info.getName(),
|
||||
info.getAvatarUrl(),
|
||||
info.getDescription(),
|
||||
info.getSubscriberCount());
|
||||
subscribeButtonMonitor = monitorSubscribeButton(headerSubscribeButton, mapOnSubscribe(channel, info));
|
||||
subscribeButtonMonitor = monitorSubscribeButton(headerSubscribeButton,
|
||||
mapOnSubscribe(channel, info));
|
||||
} else {
|
||||
if (DEBUG) Log.d(TAG, "Found subscription to this channel!");
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "Found subscription to this channel!");
|
||||
}
|
||||
final SubscriptionEntity subscription = subscriptionEntities.get(0);
|
||||
subscribeButtonMonitor = monitorSubscribeButton(headerSubscribeButton, mapOnUnsubscribe(subscription));
|
||||
subscribeButtonMonitor = monitorSubscribeButton(headerSubscribeButton,
|
||||
mapOnUnsubscribe(subscription));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private void updateSubscribeButton(boolean isSubscribed) {
|
||||
if (DEBUG) Log.d(TAG, "updateSubscribeButton() called with: isSubscribed = [" + isSubscribed + "]");
|
||||
private void updateSubscribeButton(final boolean isSubscribed) {
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "updateSubscribeButton() called with: "
|
||||
+ "isSubscribed = [" + isSubscribed + "]");
|
||||
}
|
||||
|
||||
boolean isButtonVisible = headerSubscribeButton.getVisibility() == View.VISIBLE;
|
||||
int backgroundDuration = isButtonVisible ? 300 : 0;
|
||||
int textDuration = isButtonVisible ? 200 : 0;
|
||||
final boolean isButtonVisible = headerSubscribeButton.getVisibility() == View.VISIBLE;
|
||||
final int backgroundDuration = isButtonVisible ? 300 : 0;
|
||||
final int textDuration = isButtonVisible ? 200 : 0;
|
||||
|
||||
int subscribeBackground = ContextCompat.getColor(activity, R.color.subscribe_background_color);
|
||||
int subscribeText = ContextCompat.getColor(activity, R.color.subscribe_text_color);
|
||||
int subscribedBackground = ContextCompat.getColor(activity, R.color.subscribed_background_color);
|
||||
int subscribedText = ContextCompat.getColor(activity, R.color.subscribed_text_color);
|
||||
final int subscribeBackground = ThemeHelper
|
||||
.resolveColorFromAttr(activity, R.attr.colorPrimary);
|
||||
final int subscribeText = ContextCompat.getColor(activity, R.color.subscribe_text_color);
|
||||
final int subscribedBackground = ContextCompat
|
||||
.getColor(activity, R.color.subscribed_background_color);
|
||||
final int subscribedText = ContextCompat.getColor(activity, R.color.subscribed_text_color);
|
||||
|
||||
if (!isSubscribed) {
|
||||
headerSubscribeButton.setText(R.string.subscribe_button_title);
|
||||
animateBackgroundColor(headerSubscribeButton, backgroundDuration, subscribedBackground, subscribeBackground);
|
||||
animateBackgroundColor(headerSubscribeButton, backgroundDuration, subscribedBackground,
|
||||
subscribeBackground);
|
||||
animateTextColor(headerSubscribeButton, textDuration, subscribedText, subscribeText);
|
||||
} else {
|
||||
headerSubscribeButton.setText(R.string.subscribed_button_title);
|
||||
animateBackgroundColor(headerSubscribeButton, backgroundDuration, subscribeBackground, subscribedBackground);
|
||||
animateBackgroundColor(headerSubscribeButton, backgroundDuration, subscribeBackground,
|
||||
subscribedBackground);
|
||||
animateTextColor(headerSubscribeButton, textDuration, subscribeText, subscribedText);
|
||||
}
|
||||
|
||||
@@ -340,14 +402,42 @@ public class ChannelFragment extends BaseListInfoFragment<ChannelInfo> {
|
||||
|
||||
@Override
|
||||
protected Single<ListExtractor.InfoItemsPage> loadMoreItemsLogic() {
|
||||
return ExtractorHelper.getMoreChannelItems(serviceId, url, currentNextPageUrl);
|
||||
return ExtractorHelper.getMoreChannelItems(serviceId, url, currentNextPage);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Single<ChannelInfo> loadResult(boolean forceLoad) {
|
||||
protected Single<ChannelInfo> loadResult(final boolean forceLoad) {
|
||||
return ExtractorHelper.getChannelInfo(serviceId, url, forceLoad);
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// OnClick
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
@Override
|
||||
public void onClick(final View v) {
|
||||
if (isLoading.get() || currentInfo == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
switch (v.getId()) {
|
||||
case R.id.sub_channel_avatar_view:
|
||||
case R.id.sub_channel_title_view:
|
||||
if (!TextUtils.isEmpty(currentInfo.getParentChannelUrl())) {
|
||||
try {
|
||||
NavigationHelper.openChannelFragment(getFM(), currentInfo.getServiceId(),
|
||||
currentInfo.getParentChannelUrl(),
|
||||
currentInfo.getParentChannelName());
|
||||
} catch (final Exception e) {
|
||||
ErrorActivity.reportUiError((AppCompatActivity) getActivity(), e);
|
||||
}
|
||||
} else if (DEBUG) {
|
||||
Log.i(TAG, "Can't open parent channel because we got no channel URL");
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Contract
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
@@ -356,47 +446,99 @@ public class ChannelFragment extends BaseListInfoFragment<ChannelInfo> {
|
||||
public void showLoading() {
|
||||
super.showLoading();
|
||||
|
||||
imageLoader.cancelDisplayTask(headerChannelBanner);
|
||||
imageLoader.cancelDisplayTask(headerAvatarView);
|
||||
IMAGE_LOADER.cancelDisplayTask(headerChannelBanner);
|
||||
IMAGE_LOADER.cancelDisplayTask(headerAvatarView);
|
||||
IMAGE_LOADER.cancelDisplayTask(headerSubChannelAvatarView);
|
||||
animateView(headerSubscribeButton, false, 100);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleResult(@NonNull ChannelInfo result) {
|
||||
public void handleResult(@NonNull final ChannelInfo result) {
|
||||
super.handleResult(result);
|
||||
|
||||
headerRootLayout.setVisibility(View.VISIBLE);
|
||||
imageLoader.displayImage(result.getBannerUrl(), headerChannelBanner,
|
||||
IMAGE_LOADER.displayImage(result.getBannerUrl(), headerChannelBanner,
|
||||
ImageDisplayConstants.DISPLAY_BANNER_OPTIONS);
|
||||
imageLoader.displayImage(result.getAvatarUrl(), headerAvatarView,
|
||||
IMAGE_LOADER.displayImage(result.getAvatarUrl(), headerAvatarView,
|
||||
ImageDisplayConstants.DISPLAY_AVATAR_OPTIONS);
|
||||
IMAGE_LOADER.displayImage(result.getParentChannelAvatarUrl(), headerSubChannelAvatarView,
|
||||
ImageDisplayConstants.DISPLAY_AVATAR_OPTIONS);
|
||||
|
||||
headerSubscribersTextView.setVisibility(View.VISIBLE);
|
||||
if (result.getSubscriberCount() >= 0) {
|
||||
headerSubscribersTextView.setText(Localization.shortSubscriberCount(activity, result.getSubscriberCount()));
|
||||
headerSubscribersTextView.setText(Localization
|
||||
.shortSubscriberCount(activity, result.getSubscriberCount()));
|
||||
} else {
|
||||
headerSubscribersTextView.setText(R.string.subscribers_count_not_available);
|
||||
}
|
||||
|
||||
if (menuRssButton != null) menuRssButton.setVisible(!TextUtils.isEmpty(result.getFeedUrl()));
|
||||
if (!TextUtils.isEmpty(currentInfo.getParentChannelName())) {
|
||||
headerSubChannelTitleView.setText(String.format(
|
||||
getString(R.string.channel_created_by),
|
||||
currentInfo.getParentChannelName())
|
||||
);
|
||||
headerSubChannelTitleView.setVisibility(View.VISIBLE);
|
||||
headerSubChannelAvatarView.setVisibility(View.VISIBLE);
|
||||
} else {
|
||||
headerSubChannelTitleView.setVisibility(View.GONE);
|
||||
}
|
||||
|
||||
if (menuRssButton != null) {
|
||||
menuRssButton.setVisible(!TextUtils.isEmpty(result.getFeedUrl()));
|
||||
}
|
||||
|
||||
playlistCtrl.setVisibility(View.VISIBLE);
|
||||
|
||||
if (!result.getErrors().isEmpty()) {
|
||||
showSnackBarError(result.getErrors(), UserAction.REQUESTED_CHANNEL, NewPipe.getNameOfService(result.getServiceId()), result.getUrl(), 0);
|
||||
final List<Throwable> errors = new ArrayList<>(result.getErrors());
|
||||
if (!errors.isEmpty()) {
|
||||
|
||||
// handling ContentNotSupportedException not to show the error but an appropriate string
|
||||
// so that crashes won't be sent uselessly and the user will understand what happened
|
||||
errors.removeIf(throwable -> {
|
||||
if (throwable instanceof ContentNotSupportedException) {
|
||||
showContentNotSupported();
|
||||
}
|
||||
return throwable instanceof ContentNotSupportedException;
|
||||
});
|
||||
|
||||
if (!errors.isEmpty()) {
|
||||
showSnackBarError(errors, UserAction.REQUESTED_CHANNEL,
|
||||
NewPipe.getNameOfService(result.getServiceId()), result.getUrl(), 0);
|
||||
}
|
||||
}
|
||||
|
||||
if (disposables != null) disposables.clear();
|
||||
if (subscribeButtonMonitor != null) subscribeButtonMonitor.dispose();
|
||||
if (disposables != null) {
|
||||
disposables.clear();
|
||||
}
|
||||
if (subscribeButtonMonitor != null) {
|
||||
subscribeButtonMonitor.dispose();
|
||||
}
|
||||
updateSubscription(result);
|
||||
monitorSubscription(result);
|
||||
|
||||
headerPlayAllButton.setOnClickListener(
|
||||
view -> NavigationHelper.playOnMainPlayer(activity, getPlayQueue(), false));
|
||||
headerPopupButton.setOnClickListener(
|
||||
view -> NavigationHelper.playOnPopupPlayer(activity, getPlayQueue(), false));
|
||||
headerBackgroundButton.setOnClickListener(
|
||||
view -> NavigationHelper.playOnBackgroundPlayer(activity, getPlayQueue(), false));
|
||||
headerPlayAllButton.setOnClickListener(view -> NavigationHelper
|
||||
.playOnMainPlayer(activity, getPlayQueue()));
|
||||
headerPopupButton.setOnClickListener(view -> NavigationHelper
|
||||
.playOnPopupPlayer(activity, getPlayQueue(), false));
|
||||
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;
|
||||
});
|
||||
}
|
||||
|
||||
private void showContentNotSupported() {
|
||||
contentNotSupportedTextView.setVisibility(View.VISIBLE);
|
||||
kaomojiTextView.setText("(︶︹︺)");
|
||||
kaomojiTextView.setTextSize(TypedValue.COMPLEX_UNIT_SP, 45f);
|
||||
noVideosTextView.setVisibility(View.GONE);
|
||||
}
|
||||
|
||||
private PlayQueue getPlayQueue() {
|
||||
@@ -405,22 +547,17 @@ public class ChannelFragment extends BaseListInfoFragment<ChannelInfo> {
|
||||
|
||||
private PlayQueue getPlayQueue(final int index) {
|
||||
final List<StreamInfoItem> streamItems = new ArrayList<>();
|
||||
for (InfoItem i : infoListAdapter.getItemsList()) {
|
||||
for (final InfoItem i : infoListAdapter.getItemsList()) {
|
||||
if (i instanceof StreamInfoItem) {
|
||||
streamItems.add((StreamInfoItem) i);
|
||||
}
|
||||
}
|
||||
return new ChannelPlayQueue(
|
||||
currentInfo.getServiceId(),
|
||||
currentInfo.getUrl(),
|
||||
currentInfo.getNextPageUrl(),
|
||||
streamItems,
|
||||
index
|
||||
);
|
||||
return new ChannelPlayQueue(currentInfo.getServiceId(), currentInfo.getUrl(),
|
||||
currentInfo.getNextPage(), streamItems, index);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleNextItems(ListExtractor.InfoItemsPage result) {
|
||||
public void handleNextItems(final ListExtractor.InfoItemsPage result) {
|
||||
super.handleNextItems(result);
|
||||
|
||||
if (!result.getErrors().isEmpty()) {
|
||||
@@ -437,10 +574,12 @@ public class ChannelFragment extends BaseListInfoFragment<ChannelInfo> {
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
@Override
|
||||
protected boolean onError(Throwable exception) {
|
||||
if (super.onError(exception)) return true;
|
||||
protected boolean onError(final Throwable exception) {
|
||||
if (super.onError(exception)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
int errorId = exception instanceof ExtractionException
|
||||
final int errorId = exception instanceof ExtractionException
|
||||
? R.string.parsing_error : R.string.general_error;
|
||||
|
||||
onUnrecoverableError(exception, UserAction.REQUESTED_CHANNEL,
|
||||
@@ -454,8 +593,10 @@ public class ChannelFragment extends BaseListInfoFragment<ChannelInfo> {
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
@Override
|
||||
public void setTitle(String title) {
|
||||
public void setTitle(final String title) {
|
||||
super.setTitle(title);
|
||||
if (!useAsFrontPage) headerTitleView.setText(title);
|
||||
if (!useAsFrontPage) {
|
||||
headerTitleView.setText(title);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,14 +2,15 @@ package org.schabi.newpipe.fragments.list.comments;
|
||||
|
||||
import android.content.Context;
|
||||
import android.os.Bundle;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.extractor.ListExtractor;
|
||||
import org.schabi.newpipe.extractor.NewPipe;
|
||||
@@ -19,22 +20,15 @@ import org.schabi.newpipe.report.UserAction;
|
||||
import org.schabi.newpipe.util.AnimationUtils;
|
||||
import org.schabi.newpipe.util.ExtractorHelper;
|
||||
|
||||
import io.reactivex.Single;
|
||||
import io.reactivex.disposables.CompositeDisposable;
|
||||
import io.reactivex.rxjava3.core.Single;
|
||||
import io.reactivex.rxjava3.disposables.CompositeDisposable;
|
||||
|
||||
public class CommentsFragment extends BaseListInfoFragment<CommentsInfo> {
|
||||
private final CompositeDisposable disposables = new CompositeDisposable();
|
||||
|
||||
private CompositeDisposable disposables = new CompositeDisposable();
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Views
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
|
||||
|
||||
private boolean mIsVisibleToUser = false;
|
||||
|
||||
public static CommentsFragment getInstance(int serviceId, String url, String name) {
|
||||
CommentsFragment instance = new CommentsFragment();
|
||||
public static CommentsFragment getInstance(final int serviceId, final String url,
|
||||
final String name) {
|
||||
final CommentsFragment instance = new CommentsFragment();
|
||||
instance.setInitialData(serviceId, url, name);
|
||||
return instance;
|
||||
}
|
||||
@@ -44,39 +38,36 @@ public class CommentsFragment extends BaseListInfoFragment<CommentsInfo> {
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
@Override
|
||||
public void setUserVisibleHint(boolean isVisibleToUser) {
|
||||
super.setUserVisibleHint(isVisibleToUser);
|
||||
mIsVisibleToUser = isVisibleToUser;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAttach(Context context) {
|
||||
public void onAttach(final Context context) {
|
||||
super.onAttach(context);
|
||||
}
|
||||
|
||||
@Override
|
||||
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
|
||||
public View onCreateView(@NonNull final LayoutInflater inflater,
|
||||
@Nullable final ViewGroup container,
|
||||
@Nullable final Bundle savedInstanceState) {
|
||||
return inflater.inflate(R.layout.fragment_comments, container, false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroy() {
|
||||
super.onDestroy();
|
||||
if (disposables != null) disposables.clear();
|
||||
if (disposables != null) {
|
||||
disposables.clear();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Load and handle
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
@Override
|
||||
protected Single<ListExtractor.InfoItemsPage> loadMoreItemsLogic() {
|
||||
return ExtractorHelper.getMoreCommentItems(serviceId, currentInfo, currentNextPageUrl);
|
||||
return ExtractorHelper.getMoreCommentItems(serviceId, currentInfo, currentNextPage);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Single<CommentsInfo> loadResult(boolean forceLoad) {
|
||||
protected Single<CommentsInfo> loadResult(final boolean forceLoad) {
|
||||
return ExtractorHelper.getCommentsInfo(serviceId, url, forceLoad);
|
||||
}
|
||||
|
||||
@@ -90,27 +81,28 @@ public class CommentsFragment extends BaseListInfoFragment<CommentsInfo> {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleResult(@NonNull CommentsInfo result) {
|
||||
public void handleResult(@NonNull final CommentsInfo result) {
|
||||
super.handleResult(result);
|
||||
|
||||
AnimationUtils.slideUp(getView(),120, 150, 0.06f);
|
||||
AnimationUtils.slideUp(requireView(), 120, 150, 0.06f);
|
||||
|
||||
if (!result.getErrors().isEmpty()) {
|
||||
showSnackBarError(result.getErrors(), UserAction.REQUESTED_COMMENTS, NewPipe.getNameOfService(result.getServiceId()), result.getUrl(), 0);
|
||||
showSnackBarError(result.getErrors(), UserAction.REQUESTED_COMMENTS,
|
||||
NewPipe.getNameOfService(result.getServiceId()), result.getUrl(), 0);
|
||||
}
|
||||
|
||||
if (disposables != null) disposables.clear();
|
||||
if (disposables != null) {
|
||||
disposables.clear();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleNextItems(ListExtractor.InfoItemsPage result) {
|
||||
public void handleNextItems(final ListExtractor.InfoItemsPage result) {
|
||||
super.handleNextItems(result);
|
||||
|
||||
if (!result.getErrors().isEmpty()) {
|
||||
showSnackBarError(result.getErrors(),
|
||||
UserAction.REQUESTED_COMMENTS,
|
||||
NewPipe.getNameOfService(serviceId),
|
||||
"Get next page of: " + url,
|
||||
showSnackBarError(result.getErrors(), UserAction.REQUESTED_COMMENTS,
|
||||
NewPipe.getNameOfService(serviceId), "Get next page of: " + url,
|
||||
R.string.general_error);
|
||||
}
|
||||
}
|
||||
@@ -120,11 +112,14 @@ public class CommentsFragment extends BaseListInfoFragment<CommentsInfo> {
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
@Override
|
||||
protected boolean onError(Throwable exception) {
|
||||
if (super.onError(exception)) return true;
|
||||
protected boolean onError(final Throwable exception) {
|
||||
if (super.onError(exception)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
hideLoading();
|
||||
showSnackBarError(exception, UserAction.REQUESTED_COMMENTS, NewPipe.getNameOfService(serviceId), url, R.string.error_unable_to_load_comments);
|
||||
showSnackBarError(exception, UserAction.REQUESTED_COMMENTS,
|
||||
NewPipe.getNameOfService(serviceId), url, R.string.error_unable_to_load_comments);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -133,14 +128,10 @@ public class CommentsFragment extends BaseListInfoFragment<CommentsInfo> {
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
@Override
|
||||
public void setTitle(String title) {
|
||||
return;
|
||||
}
|
||||
public void setTitle(final String title) { }
|
||||
|
||||
@Override
|
||||
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
|
||||
return;
|
||||
}
|
||||
public void onCreateOptionsMenu(final Menu menu, final MenuInflater inflater) { }
|
||||
|
||||
@Override
|
||||
protected boolean isGridLayout() {
|
||||
|
||||
@@ -10,9 +10,8 @@ import org.schabi.newpipe.util.KioskTranslator;
|
||||
import org.schabi.newpipe.util.ServiceHelper;
|
||||
|
||||
public class DefaultKioskFragment extends KioskFragment {
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState) {
|
||||
public void onCreate(final Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
if (serviceId < 0) {
|
||||
@@ -25,7 +24,9 @@ public class DefaultKioskFragment extends KioskFragment {
|
||||
super.onResume();
|
||||
|
||||
if (serviceId != ServiceHelper.getSelectedServiceId(requireContext())) {
|
||||
if (currentWorker != null) currentWorker.dispose();
|
||||
if (currentWorker != null) {
|
||||
currentWorker.dispose();
|
||||
}
|
||||
updateSelectedDefaultKiosk();
|
||||
reloadContent();
|
||||
}
|
||||
@@ -43,9 +44,10 @@ public class DefaultKioskFragment extends KioskFragment {
|
||||
name = kioskTranslatedName;
|
||||
|
||||
currentInfo = null;
|
||||
currentNextPageUrl = null;
|
||||
} catch (ExtractionException e) {
|
||||
onUnrecoverableError(e, UserAction.REQUESTED_KIOSK, "none", "Loading default kiosk from selected service", 0);
|
||||
currentNextPage = null;
|
||||
} catch (final ExtractionException e) {
|
||||
onUnrecoverableError(e, UserAction.REQUESTED_KIOSK, "none",
|
||||
"Loading default kiosk from selected service", 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,17 +1,16 @@
|
||||
package org.schabi.newpipe.fragments.list.kiosk;
|
||||
|
||||
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;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.appcompat.app.ActionBar;
|
||||
|
||||
import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.extractor.ListExtractor;
|
||||
import org.schabi.newpipe.extractor.NewPipe;
|
||||
@@ -27,55 +26,55 @@ import org.schabi.newpipe.util.KioskTranslator;
|
||||
import org.schabi.newpipe.util.Localization;
|
||||
|
||||
import icepick.State;
|
||||
import io.reactivex.Single;
|
||||
import io.reactivex.rxjava3.core.Single;
|
||||
|
||||
import static org.schabi.newpipe.util.AnimationUtils.animateView;
|
||||
|
||||
/**
|
||||
* Created by Christian Schabesberger on 23.09.17.
|
||||
*
|
||||
* <p>
|
||||
* Copyright (C) Christian Schabesberger 2017 <chris.schabesberger@mailbox.org>
|
||||
* KioskFragment.java is part of NewPipe.
|
||||
*
|
||||
* </p>
|
||||
* <p>
|
||||
* 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.
|
||||
*
|
||||
* </p>
|
||||
* <p>
|
||||
* 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.
|
||||
*
|
||||
* </p>
|
||||
* <p>
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with NewPipe. If not, see <http://www.gnu.org/licenses/>.
|
||||
* along with NewPipe. If not, see <http://www.gnu.org/licenses/>.
|
||||
* </p>
|
||||
*/
|
||||
|
||||
public class KioskFragment extends BaseListInfoFragment<KioskInfo> {
|
||||
|
||||
@State
|
||||
protected String kioskId = "";
|
||||
protected String kioskTranslatedName;
|
||||
String kioskId = "";
|
||||
String kioskTranslatedName;
|
||||
@State
|
||||
protected ContentCountry contentCountry;
|
||||
|
||||
ContentCountry contentCountry;
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Views
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
public static KioskFragment getInstance(int serviceId)
|
||||
throws ExtractionException {
|
||||
public static KioskFragment getInstance(final int serviceId) throws ExtractionException {
|
||||
return getInstance(serviceId, NewPipe.getService(serviceId)
|
||||
.getKioskList()
|
||||
.getDefaultKioskId());
|
||||
.getKioskList().getDefaultKioskId());
|
||||
}
|
||||
|
||||
public static KioskFragment getInstance(int serviceId, String kioskId)
|
||||
public static KioskFragment getInstance(final int serviceId, final String kioskId)
|
||||
throws ExtractionException {
|
||||
KioskFragment instance = new KioskFragment();
|
||||
StreamingService service = NewPipe.getService(serviceId);
|
||||
ListLinkHandlerFactory kioskLinkHandlerFactory = service.getKioskList()
|
||||
final KioskFragment instance = new KioskFragment();
|
||||
final StreamingService service = NewPipe.getService(serviceId);
|
||||
final ListLinkHandlerFactory kioskLinkHandlerFactory = service.getKioskList()
|
||||
.getListLinkHandlerFactoryByType(kioskId);
|
||||
instance.setInitialData(serviceId,
|
||||
kioskLinkHandlerFactory.fromId(kioskId).getUrl(), kioskId);
|
||||
@@ -88,7 +87,7 @@ public class KioskFragment extends BaseListInfoFragment<KioskInfo> {
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState) {
|
||||
public void onCreate(final Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
kioskTranslatedName = KioskTranslator.getTranslatedKioskName(kioskId, activity);
|
||||
@@ -97,12 +96,12 @@ public class KioskFragment extends BaseListInfoFragment<KioskInfo> {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setUserVisibleHint(boolean isVisibleToUser) {
|
||||
public void setUserVisibleHint(final boolean isVisibleToUser) {
|
||||
super.setUserVisibleHint(isVisibleToUser);
|
||||
if(useAsFrontPage && isVisibleToUser && activity != null) {
|
||||
if (useAsFrontPage && isVisibleToUser && activity != null) {
|
||||
try {
|
||||
setTitle(kioskTranslatedName);
|
||||
} catch (Exception e) {
|
||||
} catch (final Exception e) {
|
||||
onUnrecoverableError(e, UserAction.UI_ERROR,
|
||||
"none",
|
||||
"none", R.string.app_ui_crash);
|
||||
@@ -111,7 +110,9 @@ public class KioskFragment extends BaseListInfoFragment<KioskInfo> {
|
||||
}
|
||||
|
||||
@Override
|
||||
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
|
||||
public View onCreateView(@NonNull final LayoutInflater inflater,
|
||||
@Nullable final ViewGroup container,
|
||||
@Nullable final Bundle savedInstanceState) {
|
||||
return inflater.inflate(R.layout.fragment_kiosk, container, false);
|
||||
}
|
||||
|
||||
@@ -129,9 +130,9 @@ public class KioskFragment extends BaseListInfoFragment<KioskInfo> {
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
@Override
|
||||
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
|
||||
public void onCreateOptionsMenu(final Menu menu, final MenuInflater inflater) {
|
||||
super.onCreateOptionsMenu(menu, inflater);
|
||||
ActionBar supportActionBar = activity.getSupportActionBar();
|
||||
final ActionBar supportActionBar = activity.getSupportActionBar();
|
||||
if (supportActionBar != null && useAsFrontPage) {
|
||||
supportActionBar.setDisplayHomeAsUpEnabled(false);
|
||||
}
|
||||
@@ -142,18 +143,14 @@ public class KioskFragment extends BaseListInfoFragment<KioskInfo> {
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
@Override
|
||||
public Single<KioskInfo> loadResult(boolean forceReload) {
|
||||
public Single<KioskInfo> loadResult(final boolean forceReload) {
|
||||
contentCountry = Localization.getPreferredContentCountry(requireContext());
|
||||
return ExtractorHelper.getKioskInfo(serviceId,
|
||||
url,
|
||||
forceReload);
|
||||
return ExtractorHelper.getKioskInfo(serviceId, url, forceReload);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Single<ListExtractor.InfoItemsPage> loadMoreItemsLogic() {
|
||||
return ExtractorHelper.getMoreKioskItems(serviceId,
|
||||
url,
|
||||
currentNextPageUrl);
|
||||
return ExtractorHelper.getMoreKioskItems(serviceId, url, currentNextPage);
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
@@ -181,13 +178,13 @@ public class KioskFragment extends BaseListInfoFragment<KioskInfo> {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleNextItems(ListExtractor.InfoItemsPage result) {
|
||||
public void handleNextItems(final ListExtractor.InfoItemsPage result) {
|
||||
super.handleNextItems(result);
|
||||
|
||||
if (!result.getErrors().isEmpty()) {
|
||||
showSnackBarError(result.getErrors(),
|
||||
UserAction.REQUESTED_PLAYLIST, NewPipe.getNameOfService(serviceId)
|
||||
, "Get next page of: " + url, 0);
|
||||
UserAction.REQUESTED_PLAYLIST, NewPipe.getNameOfService(serviceId),
|
||||
"Get next page of: " + url, 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,9 +3,6 @@ package org.schabi.newpipe.fragments.list.playlist;
|
||||
import android.app.Activity;
|
||||
import android.content.Context;
|
||||
import android.os.Bundle;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
import android.text.TextUtils;
|
||||
import android.util.Log;
|
||||
import android.view.LayoutInflater;
|
||||
@@ -17,6 +14,10 @@ import android.view.ViewGroup;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
|
||||
import org.reactivestreams.Subscriber;
|
||||
import org.reactivestreams.Subscription;
|
||||
import org.schabi.newpipe.NewPipeDatabase;
|
||||
@@ -32,32 +33,33 @@ import org.schabi.newpipe.extractor.stream.StreamType;
|
||||
import org.schabi.newpipe.fragments.list.BaseListInfoFragment;
|
||||
import org.schabi.newpipe.info_list.InfoItemDialog;
|
||||
import org.schabi.newpipe.local.playlist.RemotePlaylistManager;
|
||||
import org.schabi.newpipe.player.helper.PlayerHolder;
|
||||
import org.schabi.newpipe.player.playqueue.PlayQueue;
|
||||
import org.schabi.newpipe.player.playqueue.PlaylistPlayQueue;
|
||||
import org.schabi.newpipe.report.ErrorActivity;
|
||||
import org.schabi.newpipe.report.UserAction;
|
||||
import org.schabi.newpipe.util.ExtractorHelper;
|
||||
import org.schabi.newpipe.util.ImageDisplayConstants;
|
||||
import org.schabi.newpipe.util.Localization;
|
||||
import org.schabi.newpipe.util.NavigationHelper;
|
||||
import org.schabi.newpipe.util.ShareUtils;
|
||||
import org.schabi.newpipe.util.StreamDialogEntry;
|
||||
import org.schabi.newpipe.util.ThemeHelper;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
|
||||
import io.reactivex.Flowable;
|
||||
import io.reactivex.Single;
|
||||
import io.reactivex.android.schedulers.AndroidSchedulers;
|
||||
import io.reactivex.disposables.CompositeDisposable;
|
||||
import io.reactivex.disposables.Disposable;
|
||||
import io.reactivex.disposables.Disposables;
|
||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
|
||||
import io.reactivex.rxjava3.core.Flowable;
|
||||
import io.reactivex.rxjava3.core.Single;
|
||||
import io.reactivex.rxjava3.disposables.CompositeDisposable;
|
||||
import io.reactivex.rxjava3.disposables.Disposable;
|
||||
|
||||
import static org.schabi.newpipe.util.AnimationUtils.animateView;
|
||||
|
||||
public class PlaylistFragment extends BaseListInfoFragment<PlaylistInfo> {
|
||||
|
||||
private CompositeDisposable disposables;
|
||||
private Subscription bookmarkReactor;
|
||||
private AtomicBoolean isBookmarkButtonReady;
|
||||
@@ -82,8 +84,9 @@ public class PlaylistFragment extends BaseListInfoFragment<PlaylistInfo> {
|
||||
|
||||
private MenuItem playlistBookmarkButton;
|
||||
|
||||
public static PlaylistFragment getInstance(int serviceId, String url, String name) {
|
||||
PlaylistFragment instance = new PlaylistFragment();
|
||||
public static PlaylistFragment getInstance(final int serviceId, final String url,
|
||||
final String name) {
|
||||
final PlaylistFragment instance = new PlaylistFragment();
|
||||
instance.setInitialData(serviceId, url, name);
|
||||
return instance;
|
||||
}
|
||||
@@ -93,17 +96,18 @@ public class PlaylistFragment extends BaseListInfoFragment<PlaylistInfo> {
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState) {
|
||||
public void onCreate(final Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
disposables = new CompositeDisposable();
|
||||
isBookmarkButtonReady = new AtomicBoolean(false);
|
||||
remotePlaylistManager = new RemotePlaylistManager(NewPipeDatabase.getInstance(
|
||||
requireContext()));
|
||||
remotePlaylistManager = new RemotePlaylistManager(NewPipeDatabase
|
||||
.getInstance(requireContext()));
|
||||
}
|
||||
|
||||
@Override
|
||||
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container,
|
||||
@Nullable Bundle savedInstanceState) {
|
||||
public View onCreateView(@NonNull final LayoutInflater inflater,
|
||||
@Nullable final ViewGroup container,
|
||||
@Nullable final Bundle savedInstanceState) {
|
||||
return inflater.inflate(R.layout.fragment_playlist, container, false);
|
||||
}
|
||||
|
||||
@@ -112,7 +116,8 @@ public class PlaylistFragment extends BaseListInfoFragment<PlaylistInfo> {
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
protected View getListHeader() {
|
||||
headerRootLayout = activity.getLayoutInflater().inflate(R.layout.playlist_header, itemsList, false);
|
||||
headerRootLayout = activity.getLayoutInflater()
|
||||
.inflate(R.layout.playlist_header, itemsList, false);
|
||||
headerTitleView = headerRootLayout.findViewById(R.id.playlist_title_view);
|
||||
headerUploaderLayout = headerRootLayout.findViewById(R.id.uploader_layout);
|
||||
headerUploaderName = headerRootLayout.findViewById(R.id.uploader_name);
|
||||
@@ -129,52 +134,59 @@ public class PlaylistFragment extends BaseListInfoFragment<PlaylistInfo> {
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void initViews(View rootView, Bundle savedInstanceState) {
|
||||
protected void initViews(final View rootView, final Bundle savedInstanceState) {
|
||||
super.initViews(rootView, savedInstanceState);
|
||||
|
||||
infoListAdapter.useMiniItemVariants(true);
|
||||
infoListAdapter.setUseMiniVariant(true);
|
||||
}
|
||||
|
||||
private PlayQueue getPlayQueueStartingAt(StreamInfoItem infoItem) {
|
||||
private PlayQueue getPlayQueueStartingAt(final StreamInfoItem infoItem) {
|
||||
return getPlayQueue(Math.max(infoListAdapter.getItemsList().indexOf(infoItem), 0));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void showStreamDialog(StreamInfoItem item) {
|
||||
protected void showStreamDialog(final StreamInfoItem item) {
|
||||
final Context context = getContext();
|
||||
final Activity activity = getActivity();
|
||||
if (context == null || context.getResources() == null || activity == null) return;
|
||||
if (context == null || context.getResources() == null || activity == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
final ArrayList<StreamDialogEntry> entries = new ArrayList<>();
|
||||
|
||||
if (PlayerHolder.getType() != null) {
|
||||
entries.add(StreamDialogEntry.enqueue);
|
||||
}
|
||||
if (item.getStreamType() == StreamType.AUDIO_STREAM) {
|
||||
StreamDialogEntry.setEnabledEntries(
|
||||
StreamDialogEntry.enqueue_on_background,
|
||||
entries.addAll(Arrays.asList(
|
||||
StreamDialogEntry.start_here_on_background,
|
||||
StreamDialogEntry.append_playlist,
|
||||
StreamDialogEntry.share);
|
||||
} else {
|
||||
StreamDialogEntry.setEnabledEntries(
|
||||
StreamDialogEntry.enqueue_on_background,
|
||||
StreamDialogEntry.enqueue_on_popup,
|
||||
StreamDialogEntry.share
|
||||
));
|
||||
} else {
|
||||
entries.addAll(Arrays.asList(
|
||||
StreamDialogEntry.start_here_on_background,
|
||||
StreamDialogEntry.start_here_on_popup,
|
||||
StreamDialogEntry.append_playlist,
|
||||
StreamDialogEntry.share);
|
||||
|
||||
StreamDialogEntry.start_here_on_popup.setCustomAction(
|
||||
(fragment, infoItem) -> NavigationHelper.playOnPopupPlayer(context, getPlayQueueStartingAt(infoItem), true));
|
||||
StreamDialogEntry.share
|
||||
));
|
||||
}
|
||||
StreamDialogEntry.setEnabledEntries(entries);
|
||||
|
||||
StreamDialogEntry.start_here_on_background.setCustomAction(
|
||||
(fragment, infoItem) -> NavigationHelper.playOnBackgroundPlayer(context, getPlayQueueStartingAt(infoItem), true));
|
||||
StreamDialogEntry.start_here_on_background.setCustomAction((fragment, infoItem) ->
|
||||
NavigationHelper.playOnBackgroundPlayer(context,
|
||||
getPlayQueueStartingAt(infoItem), true));
|
||||
|
||||
new InfoItemDialog(activity, item, StreamDialogEntry.getCommands(context), (dialog, which) ->
|
||||
StreamDialogEntry.clickOn(which, this, item)).show();
|
||||
new InfoItemDialog(activity, item, StreamDialogEntry.getCommands(context),
|
||||
(dialog, which) -> StreamDialogEntry.clickOn(which, this, item)).show();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
|
||||
if (DEBUG) Log.d(TAG, "onCreateOptionsMenu() called with: menu = [" + menu +
|
||||
"], inflater = [" + inflater + "]");
|
||||
public void onCreateOptionsMenu(final Menu menu, final MenuInflater inflater) {
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "onCreateOptionsMenu() called with: "
|
||||
+ "menu = [" + menu + "], inflater = [" + inflater + "]");
|
||||
}
|
||||
super.onCreateOptionsMenu(menu, inflater);
|
||||
inflater.inflate(R.menu.menu_playlist, menu);
|
||||
|
||||
@@ -185,10 +197,16 @@ public class PlaylistFragment extends BaseListInfoFragment<PlaylistInfo> {
|
||||
@Override
|
||||
public void onDestroyView() {
|
||||
super.onDestroyView();
|
||||
if (isBookmarkButtonReady != null) isBookmarkButtonReady.set(false);
|
||||
if (isBookmarkButtonReady != null) {
|
||||
isBookmarkButtonReady.set(false);
|
||||
}
|
||||
|
||||
if (disposables != null) disposables.clear();
|
||||
if (bookmarkReactor != null) bookmarkReactor.cancel();
|
||||
if (disposables != null) {
|
||||
disposables.clear();
|
||||
}
|
||||
if (bookmarkReactor != null) {
|
||||
bookmarkReactor.cancel();
|
||||
}
|
||||
|
||||
bookmarkReactor = null;
|
||||
}
|
||||
@@ -197,7 +215,9 @@ public class PlaylistFragment extends BaseListInfoFragment<PlaylistInfo> {
|
||||
public void onDestroy() {
|
||||
super.onDestroy();
|
||||
|
||||
if (disposables != null) disposables.dispose();
|
||||
if (disposables != null) {
|
||||
disposables.dispose();
|
||||
}
|
||||
|
||||
disposables = null;
|
||||
remotePlaylistManager = null;
|
||||
@@ -211,16 +231,16 @@ public class PlaylistFragment extends BaseListInfoFragment<PlaylistInfo> {
|
||||
|
||||
@Override
|
||||
protected Single<ListExtractor.InfoItemsPage> loadMoreItemsLogic() {
|
||||
return ExtractorHelper.getMorePlaylistItems(serviceId, url, currentNextPageUrl);
|
||||
return ExtractorHelper.getMorePlaylistItems(serviceId, url, currentNextPage);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Single<PlaylistInfo> loadResult(boolean forceLoad) {
|
||||
protected Single<PlaylistInfo> loadResult(final boolean forceLoad) {
|
||||
return ExtractorHelper.getPlaylistInfo(serviceId, url, forceLoad);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(MenuItem item) {
|
||||
public boolean onOptionsItemSelected(final MenuItem item) {
|
||||
switch (item.getItemId()) {
|
||||
case R.id.action_settings:
|
||||
NavigationHelper.openSettings(requireContext());
|
||||
@@ -251,7 +271,7 @@ public class PlaylistFragment extends BaseListInfoFragment<PlaylistInfo> {
|
||||
animateView(headerRootLayout, false, 200);
|
||||
animateView(itemsList, false, 100);
|
||||
|
||||
imageLoader.cancelDisplayTask(headerUploaderAvatar);
|
||||
IMAGE_LOADER.cancelDisplayTask(headerUploaderAvatar);
|
||||
animateView(headerUploaderLayout, false, 200);
|
||||
}
|
||||
|
||||
@@ -262,33 +282,33 @@ public class PlaylistFragment extends BaseListInfoFragment<PlaylistInfo> {
|
||||
animateView(headerRootLayout, true, 100);
|
||||
animateView(headerUploaderLayout, true, 300);
|
||||
headerUploaderLayout.setOnClickListener(null);
|
||||
if (!TextUtils.isEmpty(result.getUploaderName())) { // If we have an uploader : Put them into the ui
|
||||
// If we have an uploader put them into the UI
|
||||
if (!TextUtils.isEmpty(result.getUploaderName())) {
|
||||
headerUploaderName.setText(result.getUploaderName());
|
||||
if (!TextUtils.isEmpty(result.getUploaderUrl())) {
|
||||
headerUploaderLayout.setOnClickListener(v -> {
|
||||
try {
|
||||
NavigationHelper.openChannelFragment(getFragmentManager(),
|
||||
result.getServiceId(),
|
||||
result.getUploaderUrl(),
|
||||
result.getUploaderName());
|
||||
} catch (Exception e) {
|
||||
NavigationHelper.openChannelFragment(getFM(), result.getServiceId(),
|
||||
result.getUploaderUrl(), result.getUploaderName());
|
||||
} catch (final Exception e) {
|
||||
ErrorActivity.reportUiError((AppCompatActivity) getActivity(), e);
|
||||
}
|
||||
});
|
||||
}
|
||||
} else { // Else : say we have no uploader
|
||||
} else { // Otherwise say we have no uploader
|
||||
headerUploaderName.setText(R.string.playlist_no_uploader);
|
||||
}
|
||||
|
||||
playlistCtrl.setVisibility(View.VISIBLE);
|
||||
|
||||
imageLoader.displayImage(result.getUploaderAvatarUrl(), headerUploaderAvatar,
|
||||
IMAGE_LOADER.displayImage(result.getUploaderAvatarUrl(), headerUploaderAvatar,
|
||||
ImageDisplayConstants.DISPLAY_AVATAR_OPTIONS);
|
||||
headerStreamCount.setText(getResources().getQuantityString(R.plurals.videos,
|
||||
(int) result.getStreamCount(), (int) result.getStreamCount()));
|
||||
headerStreamCount.setText(Localization
|
||||
.localizeStreamCount(getContext(), result.getStreamCount()));
|
||||
|
||||
if (!result.getErrors().isEmpty()) {
|
||||
showSnackBarError(result.getErrors(), UserAction.REQUESTED_PLAYLIST, NewPipe.getNameOfService(result.getServiceId()), result.getUrl(), 0);
|
||||
showSnackBarError(result.getErrors(), UserAction.REQUESTED_PLAYLIST,
|
||||
NewPipe.getNameOfService(result.getServiceId()), result.getUrl(), 0);
|
||||
}
|
||||
|
||||
remotePlaylistManager.getPlaylist(result)
|
||||
@@ -298,7 +318,7 @@ public class PlaylistFragment extends BaseListInfoFragment<PlaylistInfo> {
|
||||
.subscribe(getPlaylistBookmarkSubscriber());
|
||||
|
||||
headerPlayAllButton.setOnClickListener(view ->
|
||||
NavigationHelper.playOnMainPlayer(activity, getPlayQueue(), false));
|
||||
NavigationHelper.playOnMainPlayer(activity, getPlayQueue()));
|
||||
headerPopupButton.setOnClickListener(view ->
|
||||
NavigationHelper.playOnPopupPlayer(activity, getPlayQueue(), false));
|
||||
headerBackgroundButton.setOnClickListener(view ->
|
||||
@@ -321,27 +341,27 @@ public class PlaylistFragment extends BaseListInfoFragment<PlaylistInfo> {
|
||||
|
||||
private PlayQueue getPlayQueue(final int index) {
|
||||
final List<StreamInfoItem> infoItems = new ArrayList<>();
|
||||
for(InfoItem i : infoListAdapter.getItemsList()) {
|
||||
if(i instanceof StreamInfoItem) {
|
||||
for (final InfoItem i : infoListAdapter.getItemsList()) {
|
||||
if (i instanceof StreamInfoItem) {
|
||||
infoItems.add((StreamInfoItem) i);
|
||||
}
|
||||
}
|
||||
return new PlaylistPlayQueue(
|
||||
currentInfo.getServiceId(),
|
||||
currentInfo.getUrl(),
|
||||
currentInfo.getNextPageUrl(),
|
||||
currentInfo.getNextPage(),
|
||||
infoItems,
|
||||
index
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleNextItems(ListExtractor.InfoItemsPage result) {
|
||||
public void handleNextItems(final ListExtractor.InfoItemsPage result) {
|
||||
super.handleNextItems(result);
|
||||
|
||||
if (!result.getErrors().isEmpty()) {
|
||||
showSnackBarError(result.getErrors(), UserAction.REQUESTED_PLAYLIST, NewPipe.getNameOfService(serviceId)
|
||||
, "Get next page of: " + url, 0);
|
||||
showSnackBarError(result.getErrors(), UserAction.REQUESTED_PLAYLIST,
|
||||
NewPipe.getNameOfService(serviceId), "Get next page of: " + url, 0);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -350,15 +370,15 @@ public class PlaylistFragment extends BaseListInfoFragment<PlaylistInfo> {
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
@Override
|
||||
protected boolean onError(Throwable exception) {
|
||||
if (super.onError(exception)) return true;
|
||||
protected boolean onError(final 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_PLAYLIST,
|
||||
NewPipe.getNameOfService(serviceId),
|
||||
url,
|
||||
errorId);
|
||||
final int errorId = exception instanceof ExtractionException
|
||||
? R.string.parsing_error : R.string.general_error;
|
||||
onUnrecoverableError(exception, UserAction.REQUESTED_PLAYLIST,
|
||||
NewPipe.getNameOfService(serviceId), url, errorId);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -366,13 +386,18 @@ public class PlaylistFragment extends BaseListInfoFragment<PlaylistInfo> {
|
||||
// Utils
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
private Flowable<Integer> getUpdateProcessor(@NonNull List<PlaylistRemoteEntity> playlists,
|
||||
@NonNull PlaylistInfo result) {
|
||||
private Flowable<Integer> getUpdateProcessor(
|
||||
@NonNull final List<PlaylistRemoteEntity> playlists,
|
||||
@NonNull final PlaylistInfo result) {
|
||||
final Flowable<Integer> noItemToUpdate = Flowable.just(/*noItemToUpdate=*/-1);
|
||||
if (playlists.isEmpty()) return noItemToUpdate;
|
||||
if (playlists.isEmpty()) {
|
||||
return noItemToUpdate;
|
||||
}
|
||||
|
||||
final PlaylistRemoteEntity playlistEntity = playlists.get(0);
|
||||
if (playlistEntity.isIdenticalTo(result)) return noItemToUpdate;
|
||||
final PlaylistRemoteEntity playlistRemoteEntity = playlists.get(0);
|
||||
if (playlistRemoteEntity.isIdenticalTo(result)) {
|
||||
return noItemToUpdate;
|
||||
}
|
||||
|
||||
return remotePlaylistManager.onUpdate(playlists.get(0).getUid(), result).toFlowable();
|
||||
}
|
||||
@@ -380,71 +405,76 @@ public class PlaylistFragment extends BaseListInfoFragment<PlaylistInfo> {
|
||||
private Subscriber<List<PlaylistRemoteEntity>> getPlaylistBookmarkSubscriber() {
|
||||
return new Subscriber<List<PlaylistRemoteEntity>>() {
|
||||
@Override
|
||||
public void onSubscribe(Subscription s) {
|
||||
if (bookmarkReactor != null) bookmarkReactor.cancel();
|
||||
public void onSubscribe(final Subscription s) {
|
||||
if (bookmarkReactor != null) {
|
||||
bookmarkReactor.cancel();
|
||||
}
|
||||
bookmarkReactor = s;
|
||||
bookmarkReactor.request(1);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onNext(List<PlaylistRemoteEntity> playlist) {
|
||||
public void onNext(final List<PlaylistRemoteEntity> playlist) {
|
||||
playlistEntity = playlist.isEmpty() ? null : playlist.get(0);
|
||||
|
||||
updateBookmarkButtons();
|
||||
isBookmarkButtonReady.set(true);
|
||||
|
||||
if (bookmarkReactor != null) bookmarkReactor.request(1);
|
||||
if (bookmarkReactor != null) {
|
||||
bookmarkReactor.request(1);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(Throwable t) {
|
||||
public void onError(final Throwable t) {
|
||||
PlaylistFragment.this.onError(t);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onComplete() {
|
||||
|
||||
}
|
||||
public void onComplete() { }
|
||||
};
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setTitle(String title) {
|
||||
public void setTitle(final String title) {
|
||||
super.setTitle(title);
|
||||
headerTitleView.setText(title);
|
||||
}
|
||||
|
||||
private void onBookmarkClicked() {
|
||||
if (isBookmarkButtonReady == null || !isBookmarkButtonReady.get() ||
|
||||
remotePlaylistManager == null)
|
||||
if (isBookmarkButtonReady == null || !isBookmarkButtonReady.get()
|
||||
|| remotePlaylistManager == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
final Disposable action;
|
||||
|
||||
if (currentInfo != null && playlistEntity == null) {
|
||||
action = remotePlaylistManager.onBookmark(currentInfo)
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(ignored -> {/* Do nothing */}, this::onError);
|
||||
.subscribe(ignored -> { /* Do nothing */ }, this::onError);
|
||||
} else if (playlistEntity != null) {
|
||||
action = remotePlaylistManager.deletePlaylist(playlistEntity.getUid())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.doFinally(() -> playlistEntity = null)
|
||||
.subscribe(ignored -> {/* Do nothing */}, this::onError);
|
||||
.subscribe(ignored -> { /* Do nothing */ }, this::onError);
|
||||
} else {
|
||||
action = Disposables.empty();
|
||||
action = Disposable.empty();
|
||||
}
|
||||
|
||||
disposables.add(action);
|
||||
}
|
||||
|
||||
private void updateBookmarkButtons() {
|
||||
if (playlistBookmarkButton == null || activity == null) return;
|
||||
if (playlistBookmarkButton == null || activity == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
final int iconAttr = playlistEntity == null ?
|
||||
R.attr.ic_playlist_add : R.attr.ic_playlist_check;
|
||||
final int iconAttr = playlistEntity == null
|
||||
? R.attr.ic_playlist_add : R.attr.ic_playlist_check;
|
||||
|
||||
final int titleRes = playlistEntity == null ?
|
||||
R.string.bookmark_playlist : R.string.unbookmark_playlist;
|
||||
final int titleRes = playlistEntity == null
|
||||
? R.string.bookmark_playlist : R.string.unbookmark_playlist;
|
||||
|
||||
playlistBookmarkButton.setIcon(ThemeHelper.resolveResourceIdFromAttr(activity, iconAttr));
|
||||
playlistBookmarkButton.setTitle(titleRes);
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,10 +1,10 @@
|
||||
package org.schabi.newpipe.fragments.list.search;
|
||||
|
||||
public class SuggestionItem {
|
||||
public final boolean fromHistory;
|
||||
final boolean fromHistory;
|
||||
public final String query;
|
||||
|
||||
public SuggestionItem(boolean fromHistory, String query) {
|
||||
public SuggestionItem(final boolean fromHistory, final String query) {
|
||||
this.fromHistory = fromHistory;
|
||||
this.query = query;
|
||||
}
|
||||
|
||||
@@ -2,42 +2,38 @@ package org.schabi.newpipe.fragments.list.search;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.res.TypedArray;
|
||||
import androidx.annotation.AttrRes;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.annotation.AttrRes;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import org.schabi.newpipe.R;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
public class SuggestionListAdapter extends RecyclerView.Adapter<SuggestionListAdapter.SuggestionItemHolder> {
|
||||
public class SuggestionListAdapter
|
||||
extends RecyclerView.Adapter<SuggestionListAdapter.SuggestionItemHolder> {
|
||||
private final ArrayList<SuggestionItem> items = new ArrayList<>();
|
||||
private final Context context;
|
||||
private OnSuggestionItemSelected listener;
|
||||
private boolean showSuggestionHistory = true;
|
||||
|
||||
public interface OnSuggestionItemSelected {
|
||||
void onSuggestionItemSelected(SuggestionItem item);
|
||||
void onSuggestionItemInserted(SuggestionItem item);
|
||||
void onSuggestionItemLongClick(SuggestionItem item);
|
||||
}
|
||||
|
||||
public SuggestionListAdapter(Context context) {
|
||||
public SuggestionListAdapter(final Context context) {
|
||||
this.context = context;
|
||||
}
|
||||
|
||||
public void setItems(List<SuggestionItem> items) {
|
||||
public void setItems(final List<SuggestionItem> items) {
|
||||
this.items.clear();
|
||||
if (showSuggestionHistory) {
|
||||
this.items.addAll(items);
|
||||
} else {
|
||||
// remove history items if history is disabled
|
||||
for (SuggestionItem item : items) {
|
||||
for (final SuggestionItem item : items) {
|
||||
if (!item.fromHistory) {
|
||||
this.items.add(item);
|
||||
}
|
||||
@@ -46,36 +42,43 @@ public class SuggestionListAdapter extends RecyclerView.Adapter<SuggestionListAd
|
||||
notifyDataSetChanged();
|
||||
}
|
||||
|
||||
public void setListener(OnSuggestionItemSelected listener) {
|
||||
public void setListener(final OnSuggestionItemSelected listener) {
|
||||
this.listener = listener;
|
||||
}
|
||||
|
||||
public void setShowSuggestionHistory(boolean v) {
|
||||
public void setShowSuggestionHistory(final boolean v) {
|
||||
showSuggestionHistory = v;
|
||||
}
|
||||
|
||||
@Override
|
||||
public SuggestionItemHolder onCreateViewHolder(ViewGroup parent, int viewType) {
|
||||
return new SuggestionItemHolder(LayoutInflater.from(context).inflate(R.layout.item_search_suggestion, parent, false));
|
||||
public SuggestionItemHolder onCreateViewHolder(final ViewGroup parent, final int viewType) {
|
||||
return new SuggestionItemHolder(LayoutInflater.from(context)
|
||||
.inflate(R.layout.item_search_suggestion, parent, false));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindViewHolder(SuggestionItemHolder holder, int position) {
|
||||
public void onBindViewHolder(final SuggestionItemHolder holder, final int position) {
|
||||
final SuggestionItem currentItem = getItem(position);
|
||||
holder.updateFrom(currentItem);
|
||||
holder.queryView.setOnClickListener(v -> {
|
||||
if (listener != null) listener.onSuggestionItemSelected(currentItem);
|
||||
if (listener != null) {
|
||||
listener.onSuggestionItemSelected(currentItem);
|
||||
}
|
||||
});
|
||||
holder.queryView.setOnLongClickListener(v -> {
|
||||
if (listener != null) listener.onSuggestionItemLongClick(currentItem);
|
||||
return true;
|
||||
if (listener != null) {
|
||||
listener.onSuggestionItemLongClick(currentItem);
|
||||
}
|
||||
return true;
|
||||
});
|
||||
holder.insertView.setOnClickListener(v -> {
|
||||
if (listener != null) listener.onSuggestionItemInserted(currentItem);
|
||||
if (listener != null) {
|
||||
listener.onSuggestionItemInserted(currentItem);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
SuggestionItem getItem(int position) {
|
||||
SuggestionItem getItem(final int position) {
|
||||
return items.get(position);
|
||||
}
|
||||
|
||||
@@ -88,7 +91,15 @@ public class SuggestionListAdapter extends RecyclerView.Adapter<SuggestionListAd
|
||||
return getItemCount() == 0;
|
||||
}
|
||||
|
||||
public static class SuggestionItemHolder extends RecyclerView.ViewHolder {
|
||||
public interface OnSuggestionItemSelected {
|
||||
void onSuggestionItemSelected(SuggestionItem item);
|
||||
|
||||
void onSuggestionItemInserted(SuggestionItem item);
|
||||
|
||||
void onSuggestionItemLongClick(SuggestionItem item);
|
||||
}
|
||||
|
||||
public static final class SuggestionItemHolder extends RecyclerView.ViewHolder {
|
||||
private final TextView itemSuggestionQuery;
|
||||
private final ImageView suggestionIcon;
|
||||
private final View queryView;
|
||||
@@ -98,7 +109,7 @@ public class SuggestionListAdapter extends RecyclerView.Adapter<SuggestionListAd
|
||||
private final int historyResId;
|
||||
private final int searchResId;
|
||||
|
||||
private SuggestionItemHolder(View rootView) {
|
||||
private SuggestionItemHolder(final View rootView) {
|
||||
super(rootView);
|
||||
suggestionIcon = rootView.findViewById(R.id.item_suggestion_icon);
|
||||
itemSuggestionQuery = rootView.findViewById(R.id.item_suggestion_query);
|
||||
@@ -106,20 +117,21 @@ public class SuggestionListAdapter extends RecyclerView.Adapter<SuggestionListAd
|
||||
queryView = rootView.findViewById(R.id.suggestion_search);
|
||||
insertView = rootView.findViewById(R.id.suggestion_insert);
|
||||
|
||||
historyResId = resolveResourceIdFromAttr(rootView.getContext(), R.attr.history);
|
||||
searchResId = resolveResourceIdFromAttr(rootView.getContext(), R.attr.search);
|
||||
historyResId = resolveResourceIdFromAttr(rootView.getContext(), R.attr.ic_history);
|
||||
searchResId = resolveResourceIdFromAttr(rootView.getContext(), R.attr.ic_search);
|
||||
}
|
||||
|
||||
private void updateFrom(SuggestionItem item) {
|
||||
suggestionIcon.setImageResource(item.fromHistory ? historyResId : searchResId);
|
||||
itemSuggestionQuery.setText(item.query);
|
||||
}
|
||||
|
||||
private static int resolveResourceIdFromAttr(Context context, @AttrRes int attr) {
|
||||
TypedArray a = context.getTheme().obtainStyledAttributes(new int[]{attr});
|
||||
int attributeResourceId = a.getResourceId(0, 0);
|
||||
private static int resolveResourceIdFromAttr(final Context context,
|
||||
@AttrRes final int attr) {
|
||||
final TypedArray a = context.getTheme().obtainStyledAttributes(new int[]{attr});
|
||||
final int attributeResourceId = a.getResourceId(0, 0);
|
||||
a.recycle();
|
||||
return attributeResourceId;
|
||||
}
|
||||
|
||||
private void updateFrom(final SuggestionItem item) {
|
||||
suggestionIcon.setImageResource(item.fromHistory ? historyResId : searchResId);
|
||||
itemSuggestionQuery.setText(item.query);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,17 +3,17 @@ package org.schabi.newpipe.fragments.list.videos;
|
||||
import android.content.Context;
|
||||
import android.content.SharedPreferences;
|
||||
import android.os.Bundle;
|
||||
import android.preference.PreferenceManager;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.CompoundButton;
|
||||
import android.widget.Switch;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.preference.PreferenceManager;
|
||||
|
||||
import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.extractor.ListExtractor;
|
||||
import org.schabi.newpipe.extractor.NewPipe;
|
||||
@@ -25,23 +25,24 @@ import org.schabi.newpipe.util.RelatedStreamInfo;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
import io.reactivex.Single;
|
||||
import io.reactivex.disposables.CompositeDisposable;
|
||||
import io.reactivex.rxjava3.core.Single;
|
||||
import io.reactivex.rxjava3.disposables.CompositeDisposable;
|
||||
|
||||
public class RelatedVideosFragment extends BaseListInfoFragment<RelatedStreamInfo> implements SharedPreferences.OnSharedPreferenceChangeListener{
|
||||
|
||||
private CompositeDisposable disposables = new CompositeDisposable();
|
||||
public class RelatedVideosFragment extends BaseListInfoFragment<RelatedStreamInfo>
|
||||
implements SharedPreferences.OnSharedPreferenceChangeListener {
|
||||
private static final String INFO_KEY = "related_info_key";
|
||||
private final CompositeDisposable disposables = new CompositeDisposable();
|
||||
private RelatedStreamInfo relatedStreamInfo;
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Views
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
private View headerRootLayout;
|
||||
private Switch aSwitch;
|
||||
private Switch autoplaySwitch;
|
||||
|
||||
private boolean mIsVisibleToUser = false;
|
||||
|
||||
public static RelatedVideosFragment getInstance(StreamInfo info) {
|
||||
RelatedVideosFragment instance = new RelatedVideosFragment();
|
||||
public static RelatedVideosFragment getInstance(final StreamInfo info) {
|
||||
final RelatedVideosFragment instance = new RelatedVideosFragment();
|
||||
instance.setInitialData(info);
|
||||
return instance;
|
||||
}
|
||||
@@ -51,86 +52,87 @@ public class RelatedVideosFragment extends BaseListInfoFragment<RelatedStreamInf
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
@Override
|
||||
public void setUserVisibleHint(boolean isVisibleToUser) {
|
||||
super.setUserVisibleHint(isVisibleToUser);
|
||||
mIsVisibleToUser = isVisibleToUser;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAttach(Context context) {
|
||||
public void onAttach(final Context context) {
|
||||
super.onAttach(context);
|
||||
}
|
||||
|
||||
@Override
|
||||
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
|
||||
public View onCreateView(@NonNull final LayoutInflater inflater,
|
||||
@Nullable final ViewGroup container,
|
||||
@Nullable final Bundle savedInstanceState) {
|
||||
return inflater.inflate(R.layout.fragment_related_streams, container, false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroy() {
|
||||
super.onDestroy();
|
||||
if (disposables != null) disposables.clear();
|
||||
if (disposables != null) {
|
||||
disposables.clear();
|
||||
}
|
||||
}
|
||||
|
||||
protected View getListHeader(){
|
||||
if(relatedStreamInfo != null && relatedStreamInfo.getNextStream() != null){
|
||||
headerRootLayout = activity.getLayoutInflater().inflate(R.layout.related_streams_header, itemsList, false);
|
||||
aSwitch = headerRootLayout.findViewById(R.id.autoplay_switch);
|
||||
protected View getListHeader() {
|
||||
if (relatedStreamInfo != null && relatedStreamInfo.getRelatedItems() != null) {
|
||||
headerRootLayout = activity.getLayoutInflater()
|
||||
.inflate(R.layout.related_streams_header, itemsList, false);
|
||||
autoplaySwitch = headerRootLayout.findViewById(R.id.autoplay_switch);
|
||||
|
||||
SharedPreferences pref = PreferenceManager.getDefaultSharedPreferences(getContext());
|
||||
Boolean autoplay = pref.getBoolean(getString(R.string.auto_queue_key), false);
|
||||
aSwitch.setChecked(autoplay);
|
||||
aSwitch.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
|
||||
@Override
|
||||
public void onCheckedChanged(CompoundButton compoundButton, boolean b) {
|
||||
SharedPreferences.Editor prefEdit = PreferenceManager.getDefaultSharedPreferences(getContext()).edit();
|
||||
prefEdit.putBoolean(getString(R.string.auto_queue_key), b);
|
||||
prefEdit.apply();
|
||||
}
|
||||
});
|
||||
final SharedPreferences pref = PreferenceManager
|
||||
.getDefaultSharedPreferences(requireContext());
|
||||
final boolean autoplay = pref.getBoolean(getString(R.string.auto_queue_key), false);
|
||||
autoplaySwitch.setChecked(autoplay);
|
||||
autoplaySwitch.setOnCheckedChangeListener((compoundButton, b) ->
|
||||
PreferenceManager.getDefaultSharedPreferences(requireContext()).edit()
|
||||
.putBoolean(getString(R.string.auto_queue_key), b).apply());
|
||||
return headerRootLayout;
|
||||
}else{
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Single<ListExtractor.InfoItemsPage> loadMoreItemsLogic() {
|
||||
return Single.fromCallable(() -> ListExtractor.InfoItemsPage.emptyPage());
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Single<RelatedStreamInfo> loadResult(boolean forceLoad) {
|
||||
return Single.fromCallable(() -> relatedStreamInfo);
|
||||
return Single.fromCallable(ListExtractor.InfoItemsPage::emptyPage);
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Contract
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
@Override
|
||||
protected Single<RelatedStreamInfo> loadResult(final boolean forceLoad) {
|
||||
return Single.fromCallable(() -> relatedStreamInfo);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void showLoading() {
|
||||
super.showLoading();
|
||||
if(null != headerRootLayout) headerRootLayout.setVisibility(View.INVISIBLE);
|
||||
if (headerRootLayout != null) {
|
||||
headerRootLayout.setVisibility(View.INVISIBLE);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleResult(@NonNull RelatedStreamInfo result) {
|
||||
|
||||
public void handleResult(@NonNull final RelatedStreamInfo result) {
|
||||
super.handleResult(result);
|
||||
|
||||
if(null != headerRootLayout) headerRootLayout.setVisibility(View.VISIBLE);
|
||||
AnimationUtils.slideUp(getView(),120, 96, 0.06f);
|
||||
if (headerRootLayout != null) {
|
||||
headerRootLayout.setVisibility(View.VISIBLE);
|
||||
}
|
||||
AnimationUtils.slideUp(getView(), 120, 96, 0.06f);
|
||||
|
||||
if (!result.getErrors().isEmpty()) {
|
||||
showSnackBarError(result.getErrors(), UserAction.REQUESTED_STREAM, NewPipe.getNameOfService(result.getServiceId()), result.getUrl(), 0);
|
||||
showSnackBarError(result.getErrors(), UserAction.REQUESTED_STREAM,
|
||||
NewPipe.getNameOfService(result.getServiceId()), result.getUrl(), 0);
|
||||
}
|
||||
|
||||
if (disposables != null) disposables.clear();
|
||||
if (disposables != null) {
|
||||
disposables.clear();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleNextItems(ListExtractor.InfoItemsPage result) {
|
||||
public void handleNextItems(final ListExtractor.InfoItemsPage result) {
|
||||
super.handleNextItems(result);
|
||||
|
||||
if (!result.getErrors().isEmpty()) {
|
||||
@@ -147,11 +149,14 @@ public class RelatedVideosFragment extends BaseListInfoFragment<RelatedStreamInf
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
@Override
|
||||
protected boolean onError(Throwable exception) {
|
||||
if (super.onError(exception)) return true;
|
||||
protected boolean onError(final Throwable exception) {
|
||||
if (super.onError(exception)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
hideLoading();
|
||||
showSnackBarError(exception, UserAction.REQUESTED_STREAM, NewPipe.getNameOfService(serviceId), url, R.string.general_error);
|
||||
showSnackBarError(exception, UserAction.REQUESTED_STREAM,
|
||||
NewPipe.getNameOfService(serviceId), url, R.string.general_error);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -160,45 +165,46 @@ public class RelatedVideosFragment extends BaseListInfoFragment<RelatedStreamInf
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
@Override
|
||||
public void setTitle(String title) {
|
||||
return;
|
||||
public void setTitle(final String title) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
|
||||
return;
|
||||
public void onCreateOptionsMenu(final Menu menu, final MenuInflater inflater) {
|
||||
}
|
||||
|
||||
private void setInitialData(StreamInfo info) {
|
||||
private void setInitialData(final StreamInfo info) {
|
||||
super.setInitialData(info.getServiceId(), info.getUrl(), info.getName());
|
||||
if(this.relatedStreamInfo == null) this.relatedStreamInfo = RelatedStreamInfo.getInfo(info);
|
||||
if (this.relatedStreamInfo == null) {
|
||||
this.relatedStreamInfo = RelatedStreamInfo.getInfo(info);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private static final String INFO_KEY = "related_info_key";
|
||||
|
||||
@Override
|
||||
public void onSaveInstanceState(Bundle outState) {
|
||||
public void onSaveInstanceState(final Bundle outState) {
|
||||
super.onSaveInstanceState(outState);
|
||||
outState.putSerializable(INFO_KEY, relatedStreamInfo);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onRestoreInstanceState(@NonNull Bundle savedState) {
|
||||
protected void onRestoreInstanceState(@NonNull final Bundle savedState) {
|
||||
super.onRestoreInstanceState(savedState);
|
||||
if (savedState != null) {
|
||||
Serializable serializable = savedState.getSerializable(INFO_KEY);
|
||||
if(serializable instanceof RelatedStreamInfo){
|
||||
final Serializable serializable = savedState.getSerializable(INFO_KEY);
|
||||
if (serializable instanceof RelatedStreamInfo) {
|
||||
this.relatedStreamInfo = (RelatedStreamInfo) serializable;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String s) {
|
||||
SharedPreferences pref = PreferenceManager.getDefaultSharedPreferences(getContext());
|
||||
Boolean autoplay = pref.getBoolean(getString(R.string.auto_queue_key), false);
|
||||
if(null != aSwitch) aSwitch.setChecked(autoplay);
|
||||
public void onSharedPreferenceChanged(final SharedPreferences sharedPreferences,
|
||||
final String s) {
|
||||
final SharedPreferences pref =
|
||||
PreferenceManager.getDefaultSharedPreferences(requireContext());
|
||||
final boolean autoplay = pref.getBoolean(getString(R.string.auto_queue_key), false);
|
||||
if (autoplaySwitch != null) {
|
||||
autoplaySwitch.setChecked(autoplay);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
package org.schabi.newpipe.info_list;
|
||||
|
||||
import android.content.Context;
|
||||
import androidx.annotation.NonNull;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import com.nostra13.universalimageloader.core.ImageLoader;
|
||||
|
||||
import org.schabi.newpipe.extractor.InfoItem;
|
||||
@@ -29,24 +30,26 @@ import org.schabi.newpipe.util.OnClickGesture;
|
||||
* <p>
|
||||
* Copyright (C) Christian Schabesberger 2016 <chris.schabesberger@mailbox.org>
|
||||
* InfoItemBuilder.java is part of NewPipe.
|
||||
* </p>
|
||||
* <p>
|
||||
* 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.
|
||||
* </p>
|
||||
* <p>
|
||||
* 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.
|
||||
* </p>
|
||||
* <p>
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with NewPipe. If not, see <http://www.gnu.org/licenses/>.
|
||||
* </p>
|
||||
*/
|
||||
|
||||
public class InfoItemBuilder {
|
||||
private static final String TAG = InfoItemBuilder.class.toString();
|
||||
|
||||
private final Context context;
|
||||
private final ImageLoader imageLoader = ImageLoader.getInstance();
|
||||
|
||||
@@ -55,31 +58,40 @@ public class InfoItemBuilder {
|
||||
private OnClickGesture<PlaylistInfoItem> onPlaylistSelectedListener;
|
||||
private OnClickGesture<CommentsInfoItem> onCommentsSelectedListener;
|
||||
|
||||
public InfoItemBuilder(Context context) {
|
||||
public InfoItemBuilder(final Context context) {
|
||||
this.context = context;
|
||||
}
|
||||
|
||||
public View buildView(@NonNull ViewGroup parent, @NonNull final InfoItem infoItem, final HistoryRecordManager historyRecordManager) {
|
||||
public View buildView(@NonNull final ViewGroup parent, @NonNull final InfoItem infoItem,
|
||||
final HistoryRecordManager historyRecordManager) {
|
||||
return buildView(parent, infoItem, historyRecordManager, false);
|
||||
}
|
||||
|
||||
public View buildView(@NonNull ViewGroup parent, @NonNull final InfoItem infoItem,
|
||||
final HistoryRecordManager historyRecordManager, boolean useMiniVariant) {
|
||||
InfoItemHolder holder = holderFromInfoType(parent, infoItem.getInfoType(), useMiniVariant);
|
||||
public View buildView(@NonNull final ViewGroup parent, @NonNull final InfoItem infoItem,
|
||||
final HistoryRecordManager historyRecordManager,
|
||||
final boolean useMiniVariant) {
|
||||
final InfoItemHolder holder
|
||||
= holderFromInfoType(parent, infoItem.getInfoType(), useMiniVariant);
|
||||
holder.updateFromItem(infoItem, historyRecordManager);
|
||||
return holder.itemView;
|
||||
}
|
||||
|
||||
private InfoItemHolder holderFromInfoType(@NonNull ViewGroup parent, @NonNull InfoItem.InfoType infoType, boolean useMiniVariant) {
|
||||
private InfoItemHolder holderFromInfoType(@NonNull final ViewGroup parent,
|
||||
@NonNull final InfoItem.InfoType infoType,
|
||||
final boolean useMiniVariant) {
|
||||
switch (infoType) {
|
||||
case STREAM:
|
||||
return useMiniVariant ? new StreamMiniInfoItemHolder(this, parent) : new StreamInfoItemHolder(this, parent);
|
||||
return useMiniVariant ? new StreamMiniInfoItemHolder(this, parent)
|
||||
: new StreamInfoItemHolder(this, parent);
|
||||
case CHANNEL:
|
||||
return useMiniVariant ? new ChannelMiniInfoItemHolder(this, parent) : new ChannelInfoItemHolder(this, parent);
|
||||
return useMiniVariant ? new ChannelMiniInfoItemHolder(this, parent)
|
||||
: new ChannelInfoItemHolder(this, parent);
|
||||
case PLAYLIST:
|
||||
return useMiniVariant ? new PlaylistMiniInfoItemHolder(this, parent) : new PlaylistInfoItemHolder(this, parent);
|
||||
return useMiniVariant ? new PlaylistMiniInfoItemHolder(this, parent)
|
||||
: new PlaylistInfoItemHolder(this, parent);
|
||||
case COMMENT:
|
||||
return useMiniVariant ? new CommentsMiniInfoItemHolder(this, parent) : new CommentsInfoItemHolder(this, parent);
|
||||
return useMiniVariant ? new CommentsMiniInfoItemHolder(this, parent)
|
||||
: new CommentsInfoItemHolder(this, parent);
|
||||
default:
|
||||
throw new RuntimeException("InfoType not expected = " + infoType.name());
|
||||
}
|
||||
@@ -97,7 +109,7 @@ public class InfoItemBuilder {
|
||||
return onStreamSelectedListener;
|
||||
}
|
||||
|
||||
public void setOnStreamSelectedListener(OnClickGesture<StreamInfoItem> listener) {
|
||||
public void setOnStreamSelectedListener(final OnClickGesture<StreamInfoItem> listener) {
|
||||
this.onStreamSelectedListener = listener;
|
||||
}
|
||||
|
||||
@@ -105,7 +117,7 @@ public class InfoItemBuilder {
|
||||
return onChannelSelectedListener;
|
||||
}
|
||||
|
||||
public void setOnChannelSelectedListener(OnClickGesture<ChannelInfoItem> listener) {
|
||||
public void setOnChannelSelectedListener(final OnClickGesture<ChannelInfoItem> listener) {
|
||||
this.onChannelSelectedListener = listener;
|
||||
}
|
||||
|
||||
@@ -113,7 +125,7 @@ public class InfoItemBuilder {
|
||||
return onPlaylistSelectedListener;
|
||||
}
|
||||
|
||||
public void setOnPlaylistSelectedListener(OnClickGesture<PlaylistInfoItem> listener) {
|
||||
public void setOnPlaylistSelectedListener(final OnClickGesture<PlaylistInfoItem> listener) {
|
||||
this.onPlaylistSelectedListener = listener;
|
||||
}
|
||||
|
||||
@@ -121,8 +133,8 @@ public class InfoItemBuilder {
|
||||
return onCommentsSelectedListener;
|
||||
}
|
||||
|
||||
public void setOnCommentsSelectedListener(OnClickGesture<CommentsInfoItem> onCommentsSelectedListener) {
|
||||
public void setOnCommentsSelectedListener(
|
||||
final OnClickGesture<CommentsInfoItem> onCommentsSelectedListener) {
|
||||
this.onCommentsSelectedListener = onCommentsSelectedListener;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -3,11 +3,12 @@ package org.schabi.newpipe.info_list;
|
||||
import android.app.Activity;
|
||||
import android.app.AlertDialog;
|
||||
import android.content.DialogInterface;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import android.view.View;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.extractor.stream.StreamInfoItem;
|
||||
|
||||
@@ -30,10 +31,10 @@ public class InfoItemDialog {
|
||||
final View bannerView = View.inflate(activity, R.layout.dialog_title, null);
|
||||
bannerView.setSelected(true);
|
||||
|
||||
TextView titleView = bannerView.findViewById(R.id.itemTitleView);
|
||||
final TextView titleView = bannerView.findViewById(R.id.itemTitleView);
|
||||
titleView.setText(title);
|
||||
|
||||
TextView detailsView = bannerView.findViewById(R.id.itemAdditionalDetails);
|
||||
final TextView detailsView = bannerView.findViewById(R.id.itemAdditionalDetails);
|
||||
if (additionalDetail != null) {
|
||||
detailsView.setText(additionalDetail);
|
||||
detailsView.setVisibility(View.VISIBLE);
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
package org.schabi.newpipe.info_list;
|
||||
|
||||
import android.content.Context;
|
||||
import android.util.Log;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.recyclerview.widget.GridLayoutManager;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
import android.util.Log;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
|
||||
import org.schabi.newpipe.database.stream.model.StreamStateEntity;
|
||||
import org.schabi.newpipe.extractor.InfoItem;
|
||||
@@ -83,42 +84,33 @@ public class InfoListAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolde
|
||||
private View header = null;
|
||||
private View footer = null;
|
||||
|
||||
public class HFHolder extends RecyclerView.ViewHolder {
|
||||
public View view;
|
||||
|
||||
public HFHolder(View v) {
|
||||
super(v);
|
||||
view = v;
|
||||
}
|
||||
}
|
||||
|
||||
public InfoListAdapter(Context context) {
|
||||
public InfoListAdapter(final Context context) {
|
||||
this.recordManager = new HistoryRecordManager(context);
|
||||
infoItemBuilder = new InfoItemBuilder(context);
|
||||
infoItemList = new ArrayList<>();
|
||||
}
|
||||
|
||||
public void setOnStreamSelectedListener(OnClickGesture<StreamInfoItem> listener) {
|
||||
public void setOnStreamSelectedListener(final OnClickGesture<StreamInfoItem> listener) {
|
||||
infoItemBuilder.setOnStreamSelectedListener(listener);
|
||||
}
|
||||
|
||||
public void setOnChannelSelectedListener(OnClickGesture<ChannelInfoItem> listener) {
|
||||
public void setOnChannelSelectedListener(final OnClickGesture<ChannelInfoItem> listener) {
|
||||
infoItemBuilder.setOnChannelSelectedListener(listener);
|
||||
}
|
||||
|
||||
public void setOnPlaylistSelectedListener(OnClickGesture<PlaylistInfoItem> listener) {
|
||||
public void setOnPlaylistSelectedListener(final OnClickGesture<PlaylistInfoItem> listener) {
|
||||
infoItemBuilder.setOnPlaylistSelectedListener(listener);
|
||||
}
|
||||
|
||||
public void setOnCommentsSelectedListener(OnClickGesture<CommentsInfoItem> listener) {
|
||||
public void setOnCommentsSelectedListener(final OnClickGesture<CommentsInfoItem> listener) {
|
||||
infoItemBuilder.setOnCommentsSelectedListener(listener);
|
||||
}
|
||||
|
||||
public void useMiniItemVariants(boolean useMiniVariant) {
|
||||
public void setUseMiniVariant(final boolean useMiniVariant) {
|
||||
this.useMiniVariant = useMiniVariant;
|
||||
}
|
||||
|
||||
public void setGridItemVariants(boolean useGridVariant) {
|
||||
public void setUseGridVariant(final boolean useGridVariant) {
|
||||
this.useGridVariant = useGridVariant;
|
||||
}
|
||||
|
||||
@@ -126,55 +118,67 @@ public class InfoListAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolde
|
||||
if (data == null) {
|
||||
return;
|
||||
}
|
||||
if (DEBUG) Log.d(TAG, "addInfoItemList() before > infoItemList.size() = " +
|
||||
infoItemList.size() + ", data.size() = " + data.size());
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "addInfoItemList() before > infoItemList.size() = "
|
||||
+ infoItemList.size() + ", data.size() = " + data.size());
|
||||
}
|
||||
|
||||
int offsetStart = sizeConsideringHeaderOffset();
|
||||
final int offsetStart = sizeConsideringHeaderOffset();
|
||||
infoItemList.addAll(data);
|
||||
|
||||
if (DEBUG) Log.d(TAG, "addInfoItemList() after > offsetStart = " + offsetStart +
|
||||
", infoItemList.size() = " + infoItemList.size() +
|
||||
", header = " + header + ", footer = " + footer +
|
||||
", showFooter = " + showFooter);
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "addInfoItemList() after > offsetStart = " + offsetStart + ", "
|
||||
+ "infoItemList.size() = " + infoItemList.size() + ", "
|
||||
+ "header = " + header + ", footer = " + footer + ", "
|
||||
+ "showFooter = " + showFooter);
|
||||
}
|
||||
notifyItemRangeInserted(offsetStart, data.size());
|
||||
|
||||
if (footer != null && showFooter) {
|
||||
int footerNow = sizeConsideringHeaderOffset();
|
||||
final int footerNow = sizeConsideringHeaderOffset();
|
||||
notifyItemMoved(offsetStart, footerNow);
|
||||
|
||||
if (DEBUG) Log.d(TAG, "addInfoItemList() footer from " + offsetStart +
|
||||
" to " + footerNow);
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "addInfoItemList() footer from " + offsetStart
|
||||
+ " to " + footerNow);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void setInfoItemList(List<? extends InfoItem> data) {
|
||||
public void setInfoItemList(final List<? extends InfoItem> data) {
|
||||
infoItemList.clear();
|
||||
infoItemList.addAll(data);
|
||||
notifyDataSetChanged();
|
||||
}
|
||||
|
||||
public void addInfoItem(@Nullable InfoItem data) {
|
||||
public void addInfoItem(@Nullable final InfoItem data) {
|
||||
if (data == null) {
|
||||
return;
|
||||
}
|
||||
if (DEBUG) Log.d(TAG, "addInfoItem() before > infoItemList.size() = " +
|
||||
infoItemList.size() + ", thread = " + Thread.currentThread());
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "addInfoItem() before > infoItemList.size() = "
|
||||
+ infoItemList.size() + ", thread = " + Thread.currentThread());
|
||||
}
|
||||
|
||||
int positionInserted = sizeConsideringHeaderOffset();
|
||||
final int positionInserted = sizeConsideringHeaderOffset();
|
||||
infoItemList.add(data);
|
||||
|
||||
if (DEBUG) Log.d(TAG, "addInfoItem() after > position = " + positionInserted +
|
||||
", infoItemList.size() = " + infoItemList.size() +
|
||||
", header = " + header + ", footer = " + footer +
|
||||
", showFooter = " + showFooter);
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "addInfoItem() after > position = " + positionInserted + ", "
|
||||
+ "infoItemList.size() = " + infoItemList.size() + ", "
|
||||
+ "header = " + header + ", footer = " + footer + ", "
|
||||
+ "showFooter = " + showFooter);
|
||||
}
|
||||
notifyItemInserted(positionInserted);
|
||||
|
||||
if (footer != null && showFooter) {
|
||||
int footerNow = sizeConsideringHeaderOffset();
|
||||
final int footerNow = sizeConsideringHeaderOffset();
|
||||
notifyItemMoved(positionInserted, footerNow);
|
||||
|
||||
if (DEBUG) Log.d(TAG, "addInfoItem() footer from " + positionInserted +
|
||||
" to " + footerNow);
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "addInfoItem() footer from " + positionInserted
|
||||
+ " to " + footerNow);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -186,29 +190,39 @@ public class InfoListAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolde
|
||||
notifyDataSetChanged();
|
||||
}
|
||||
|
||||
public void setHeader(View header) {
|
||||
boolean changed = header != this.header;
|
||||
public void setHeader(final View header) {
|
||||
final boolean changed = header != this.header;
|
||||
this.header = header;
|
||||
if (changed) notifyDataSetChanged();
|
||||
if (changed) {
|
||||
notifyDataSetChanged();
|
||||
}
|
||||
}
|
||||
|
||||
public void setFooter(View view) {
|
||||
public void setFooter(final View view) {
|
||||
this.footer = view;
|
||||
}
|
||||
|
||||
public void showFooter(boolean show) {
|
||||
if (DEBUG) Log.d(TAG, "showFooter() called with: show = [" + show + "]");
|
||||
if (show == showFooter) return;
|
||||
public void showFooter(final boolean show) {
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "showFooter() called with: show = [" + show + "]");
|
||||
}
|
||||
if (show == showFooter) {
|
||||
return;
|
||||
}
|
||||
|
||||
showFooter = show;
|
||||
if (show) notifyItemInserted(sizeConsideringHeaderOffset());
|
||||
else notifyItemRemoved(sizeConsideringHeaderOffset());
|
||||
if (show) {
|
||||
notifyItemInserted(sizeConsideringHeaderOffset());
|
||||
} else {
|
||||
notifyItemRemoved(sizeConsideringHeaderOffset());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private int sizeConsideringHeaderOffset() {
|
||||
int i = infoItemList.size() + (header != null ? 1 : 0);
|
||||
if (DEBUG) Log.d(TAG, "sizeConsideringHeaderOffset() called → " + i);
|
||||
final int i = infoItemList.size() + (header != null ? 1 : 0);
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "sizeConsideringHeaderOffset() called → " + i);
|
||||
}
|
||||
return i;
|
||||
}
|
||||
|
||||
@@ -219,18 +233,27 @@ public class InfoListAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolde
|
||||
@Override
|
||||
public int getItemCount() {
|
||||
int count = infoItemList.size();
|
||||
if (header != null) count++;
|
||||
if (footer != null && showFooter) count++;
|
||||
if (header != null) {
|
||||
count++;
|
||||
}
|
||||
if (footer != null && showFooter) {
|
||||
count++;
|
||||
}
|
||||
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "getItemCount() called, count = " + count + ", infoItemList.size() = " + infoItemList.size() + ", header = " + header + ", footer = " + footer + ", showFooter = " + showFooter);
|
||||
Log.d(TAG, "getItemCount() called with: "
|
||||
+ "count = " + count + ", infoItemList.size() = " + infoItemList.size() + ", "
|
||||
+ "header = " + header + ", footer = " + footer + ", "
|
||||
+ "showFooter = " + showFooter);
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getItemViewType(int position) {
|
||||
if (DEBUG) Log.d(TAG, "getItemViewType() called with: position = [" + position + "]");
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "getItemViewType() called with: position = [" + position + "]");
|
||||
}
|
||||
|
||||
if (header != null && position == 0) {
|
||||
return HEADER_TYPE;
|
||||
@@ -243,11 +266,14 @@ public class InfoListAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolde
|
||||
final InfoItem item = infoItemList.get(position);
|
||||
switch (item.getInfoType()) {
|
||||
case STREAM:
|
||||
return useGridVariant ? GRID_STREAM_HOLDER_TYPE : useMiniVariant ? MINI_STREAM_HOLDER_TYPE : STREAM_HOLDER_TYPE;
|
||||
return useGridVariant ? GRID_STREAM_HOLDER_TYPE : useMiniVariant
|
||||
? MINI_STREAM_HOLDER_TYPE : STREAM_HOLDER_TYPE;
|
||||
case CHANNEL:
|
||||
return useGridVariant ? GRID_CHANNEL_HOLDER_TYPE : useMiniVariant ? MINI_CHANNEL_HOLDER_TYPE : CHANNEL_HOLDER_TYPE;
|
||||
return useGridVariant ? GRID_CHANNEL_HOLDER_TYPE : useMiniVariant
|
||||
? MINI_CHANNEL_HOLDER_TYPE : CHANNEL_HOLDER_TYPE;
|
||||
case PLAYLIST:
|
||||
return useGridVariant ? GRID_PLAYLIST_HOLDER_TYPE : useMiniVariant ? MINI_PLAYLIST_HOLDER_TYPE : PLAYLIST_HOLDER_TYPE;
|
||||
return useGridVariant ? GRID_PLAYLIST_HOLDER_TYPE : useMiniVariant
|
||||
? MINI_PLAYLIST_HOLDER_TYPE : PLAYLIST_HOLDER_TYPE;
|
||||
case COMMENT:
|
||||
return useMiniVariant ? MINI_COMMENT_HOLDER_TYPE : COMMENT_HOLDER_TYPE;
|
||||
default:
|
||||
@@ -257,9 +283,12 @@ public class InfoListAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolde
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int type) {
|
||||
if (DEBUG)
|
||||
Log.d(TAG, "onCreateViewHolder() called with: parent = [" + parent + "], type = [" + type + "]");
|
||||
public RecyclerView.ViewHolder onCreateViewHolder(@NonNull final ViewGroup parent,
|
||||
final int type) {
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "onCreateViewHolder() called with: "
|
||||
+ "parent = [" + parent + "], type = [" + type + "]");
|
||||
}
|
||||
switch (type) {
|
||||
case HEADER_TYPE:
|
||||
return new HFHolder(header);
|
||||
@@ -293,28 +322,38 @@ public class InfoListAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolde
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position) {
|
||||
if (DEBUG) Log.d(TAG, "onBindViewHolder() called with: holder = [" + holder.getClass().getSimpleName() + "], position = [" + position + "]");
|
||||
public void onBindViewHolder(@NonNull final RecyclerView.ViewHolder holder, int position) {
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "onBindViewHolder() called with: "
|
||||
+ "holder = [" + holder.getClass().getSimpleName() + "], "
|
||||
+ "position = [" + position + "]");
|
||||
}
|
||||
if (holder instanceof InfoItemHolder) {
|
||||
// If header isn't null, offset the items by -1
|
||||
if (header != null) position--;
|
||||
if (header != null) {
|
||||
position--;
|
||||
}
|
||||
|
||||
((InfoItemHolder) holder).updateFromItem(infoItemList.get(position), recordManager);
|
||||
} else if (holder instanceof HFHolder && position == 0 && header != null) {
|
||||
((HFHolder) holder).view = header;
|
||||
} else if (holder instanceof HFHolder && position == sizeConsideringHeaderOffset() && footer != null && showFooter) {
|
||||
} else if (holder instanceof HFHolder && position == sizeConsideringHeaderOffset()
|
||||
&& footer != null && showFooter) {
|
||||
((HFHolder) holder).view = footer;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position, @NonNull List<Object> payloads) {
|
||||
public void onBindViewHolder(@NonNull final RecyclerView.ViewHolder holder, final int position,
|
||||
@NonNull final List<Object> payloads) {
|
||||
if (!payloads.isEmpty() && holder instanceof InfoItemHolder) {
|
||||
for (Object payload : payloads) {
|
||||
for (final Object payload : payloads) {
|
||||
if (payload instanceof StreamStateEntity) {
|
||||
((InfoItemHolder) holder).updateState(infoItemList.get(header == null ? position : position - 1), recordManager);
|
||||
((InfoItemHolder) holder).updateState(infoItemList
|
||||
.get(header == null ? position : position - 1), recordManager);
|
||||
} else if (payload instanceof Boolean) {
|
||||
((InfoItemHolder) holder).updateState(infoItemList.get(header == null ? position : position - 1), recordManager);
|
||||
((InfoItemHolder) holder).updateState(infoItemList
|
||||
.get(header == null ? position : position - 1), recordManager);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
@@ -325,10 +364,19 @@ public class InfoListAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolde
|
||||
public GridLayoutManager.SpanSizeLookup getSpanSizeLookup(final int spanCount) {
|
||||
return new GridLayoutManager.SpanSizeLookup() {
|
||||
@Override
|
||||
public int getSpanSize(int position) {
|
||||
public int getSpanSize(final int position) {
|
||||
final int type = getItemViewType(position);
|
||||
return type == HEADER_TYPE || type == FOOTER_TYPE ? spanCount : 1;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
public static class HFHolder extends RecyclerView.ViewHolder {
|
||||
public View view;
|
||||
|
||||
HFHolder(final View v) {
|
||||
super(v);
|
||||
view = v;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,8 +6,8 @@ import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.info_list.InfoItemBuilder;
|
||||
|
||||
public class ChannelGridInfoItemHolder extends ChannelMiniInfoItemHolder {
|
||||
|
||||
public ChannelGridInfoItemHolder(InfoItemBuilder infoItemBuilder, ViewGroup parent) {
|
||||
super(infoItemBuilder, R.layout.list_channel_grid_item, parent);
|
||||
}
|
||||
public ChannelGridInfoItemHolder(final InfoItemBuilder infoItemBuilder,
|
||||
final ViewGroup parent) {
|
||||
super(infoItemBuilder, R.layout.list_channel_grid_item, parent);
|
||||
}
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user