mirror of
https://github.com/TeamNewPipe/NewPipe
synced 2026-01-15 05:58:02 +00:00
Compare commits
678 Commits
v0.12.0-be
...
v0.13.5
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2a45a13f73 | ||
|
|
067b15c300 | ||
|
|
8a1c283542 | ||
|
|
93d1e8b2ff | ||
|
|
c60d5b54fa | ||
|
|
ef180f082e | ||
|
|
f52741cc37 | ||
|
|
2a2661f066 | ||
|
|
b521903138 | ||
|
|
acbd699d95 | ||
|
|
6b8928becb | ||
|
|
e393bdb1e5 | ||
|
|
bba6b96765 | ||
|
|
740116356c | ||
|
|
2f6e4fa4a3 | ||
|
|
fb3f6721b2 | ||
|
|
4e7bd21e5c | ||
|
|
219c2030b9 | ||
|
|
b75fdb4566 | ||
|
|
4584b14a31 | ||
|
|
814ddb5932 | ||
|
|
6ea0f6290a | ||
|
|
c796fe1fe6 | ||
|
|
a452a164e6 | ||
|
|
0d9dd69b19 | ||
|
|
d7b73c18f1 | ||
|
|
07f66c0e45 | ||
|
|
d449acbf86 | ||
|
|
fce416ba76 | ||
|
|
cb6bfe8556 | ||
|
|
d7b31e1d25 | ||
|
|
85057376d6 | ||
|
|
c43ac7c869 | ||
|
|
b2657315f1 | ||
|
|
de5ed9717c | ||
|
|
e17a6cbb9f | ||
|
|
8e783b774b | ||
|
|
733663f40d | ||
|
|
4b2a792a62 | ||
|
|
f7aa171d01 | ||
|
|
5eafefb683 | ||
|
|
5d1b02a856 | ||
|
|
4acda3d9ae | ||
|
|
643e10ace2 | ||
|
|
7b64a232de | ||
|
|
d7472d837d | ||
|
|
94b473ab4b | ||
|
|
4a05bbb6c8 | ||
|
|
0343659b35 | ||
|
|
f38eadbe30 | ||
|
|
eb29a53ac5 | ||
|
|
bc71e260e2 | ||
|
|
ddf23a3443 | ||
|
|
d41b248d1c | ||
|
|
a025b25933 | ||
|
|
c9a52a6088 | ||
|
|
0276dca406 | ||
|
|
3bb95ad44c | ||
|
|
0a6572c282 | ||
|
|
3937067be1 | ||
|
|
48e4eb44f2 | ||
|
|
c78cc6f2fd | ||
|
|
73a71e0f5c | ||
|
|
93605774f0 | ||
|
|
2834e5d78f | ||
|
|
dd467b4d63 | ||
|
|
1fa541776b | ||
|
|
f6f67c7b0a | ||
|
|
9b3f19c19b | ||
|
|
f4a9ec15e8 | ||
|
|
006c4ecb02 | ||
|
|
1c752b0e18 | ||
|
|
f84aff63e3 | ||
|
|
ae8121b680 | ||
|
|
882fbf9275 | ||
|
|
0a1743251e | ||
|
|
3403a127c1 | ||
|
|
9c5ca9f09d | ||
|
|
73eea5608a | ||
|
|
f48aeb91f4 | ||
|
|
0bda964ece | ||
|
|
14f5d54b50 | ||
|
|
105ac2f6ff | ||
|
|
160560f1fd | ||
|
|
deeb667d6f | ||
|
|
78123ff6f5 | ||
| 9b1fdff22f | |||
|
|
0275502796 | ||
|
|
8af475e319 | ||
|
|
2202c8f09e | ||
|
|
adf309d3a8 | ||
|
|
3071314586 | ||
|
|
88e1df840d | ||
|
|
23c1b66f6c | ||
|
|
b0318a1cce | ||
|
|
c130a66e4d | ||
|
|
3386ba6d1b | ||
|
|
9275569fa6 | ||
|
|
2b23dfd0a6 | ||
|
|
2a13d9990e | ||
|
|
c9669b51c6 | ||
|
|
486c180b3c | ||
|
|
9eb5bf9b87 | ||
|
|
953a89f3a1 | ||
|
|
d638fa1434 | ||
|
|
e6d700288c | ||
|
|
371f14cdc9 | ||
|
|
0733ae2404 | ||
|
|
b1731ebd49 | ||
|
|
342b3191ac | ||
|
|
cd39445245 | ||
|
|
92eac67367 | ||
|
|
0e31a0c704 | ||
|
|
d60c117a70 | ||
|
|
69ccad5998 | ||
|
|
b6d22320e6 | ||
|
|
d3bf948dba | ||
|
|
5de3d96b31 | ||
|
|
d30dd64322 | ||
|
|
386df10a5a | ||
|
|
af147de547 | ||
| 23cd0e5a5e | |||
| c5a566657c | |||
|
|
ce2018c864 | ||
| 472ab46af2 | |||
|
|
7f87d45bb5 | ||
| e9f7ab18bb | |||
|
|
fba83a8afe | ||
|
|
05089abddc | ||
|
|
ccd70aac51 | ||
|
|
5df8445d04 | ||
|
|
8c43674fa4 | ||
|
|
f162316a6b | ||
|
|
670596ed88 | ||
|
|
6b3eb716c4 | ||
|
|
6a5180d94c | ||
|
|
83faaedfcc | ||
|
|
d98d790a7a | ||
|
|
36b5833a3a | ||
| 5e86781a79 | |||
|
|
6a780504b4 | ||
|
|
a0d8212136 | ||
|
|
0e1e6a9d62 | ||
|
|
812282a332 | ||
|
|
a8a4c9e97f | ||
|
|
24c293e335 | ||
|
|
0a596df497 | ||
|
|
3d66c6572b | ||
|
|
f45769cbb2 | ||
|
|
ef51f93c6f | ||
|
|
35af68f148 | ||
|
|
ff21430b43 | ||
|
|
fcb67f5119 | ||
|
|
0a41fbd185 | ||
|
|
d32aaf488f | ||
|
|
1254798013 | ||
|
|
c72d2a2308 | ||
|
|
703181655b | ||
|
|
da4a1c5bf0 | ||
|
|
1130bd502e | ||
|
|
27ea4ee679 | ||
|
|
56e3b66d06 | ||
|
|
7d4768e151 | ||
|
|
7ec1011610 | ||
|
|
e1bbd2055c | ||
|
|
8a19547d9f | ||
|
|
6e6922dab8 | ||
|
|
e009ade922 | ||
|
|
90e15bcab9 | ||
|
|
2a040cea4b | ||
|
|
72f2a7f8db | ||
|
|
8a8022afe6 | ||
|
|
b692bec310 | ||
|
|
536c01c70d | ||
|
|
ec8e14e977 | ||
|
|
85b34f8809 | ||
|
|
22951a56a5 | ||
|
|
46ad84b101 | ||
|
|
5efb77e520 | ||
|
|
76903102b8 | ||
|
|
c8e26b429c | ||
|
|
55c1310f74 | ||
|
|
b8278d91e0 | ||
|
|
b032502148 | ||
|
|
bec1a4dd1a | ||
|
|
4dfb9e7977 | ||
|
|
2bfa165cdc | ||
|
|
658666276d | ||
|
|
62f91b9084 | ||
|
|
719140ab78 | ||
|
|
0471fd8145 | ||
|
|
a079a0c901 | ||
|
|
ac2fa74c8f | ||
|
|
4c10ef65f5 | ||
|
|
cfa697fab2 | ||
|
|
a09b9d3e4d | ||
|
|
c470909f19 | ||
|
|
5e59cfcf9d | ||
|
|
a099fe35d2 | ||
|
|
bcfd8a2450 | ||
|
|
8ed9d71e14 | ||
|
|
004c2fa55a | ||
|
|
3dd63d03cb | ||
|
|
cceedab864 | ||
|
|
b494b2ea39 | ||
|
|
0b29cf086b | ||
|
|
11d33097f7 | ||
|
|
3ae61645de | ||
|
|
4711befffa | ||
|
|
5673d53a20 | ||
|
|
90ca4a5e92 | ||
|
|
ad252956ab | ||
|
|
1b2c091c39 | ||
| 9031bc0c7b | |||
| 1d85e0ea63 | |||
|
|
458774aadb | ||
|
|
ae89f7bea3 | ||
|
|
fd77b8552b | ||
|
|
bae9f5e844 | ||
|
|
e3f3d90b68 | ||
|
|
646fa877ba | ||
|
|
7f3bd8aec2 | ||
|
|
4501203a7a | ||
|
|
06292bceb2 | ||
|
|
f94f14ab65 | ||
|
|
7145c68e03 | ||
|
|
d1b0cd74be | ||
|
|
bac3825c87 | ||
|
|
fc1d283414 | ||
| c0652daa97 | |||
|
|
553903bd9d | ||
|
|
a43ec25b7e | ||
|
|
bb2a66fd02 | ||
|
|
71ac830bfa | ||
|
|
82bce80c62 | ||
|
|
67ddf78e18 | ||
|
|
8285df0f3f | ||
|
|
bdb45295b9 | ||
|
|
f330ee8f8d | ||
|
|
7331e4a7f2 | ||
|
|
51252d3b61 | ||
|
|
dcdb2c323e | ||
|
|
c4ac901c67 | ||
|
|
a7ce072ca2 | ||
|
|
a9b427b877 | ||
|
|
67a9f3a4ad | ||
|
|
1a4905f36a | ||
|
|
cbb1fde7b0 | ||
|
|
b86bd019a7 | ||
| 6a0bada9d2 | |||
|
|
a708278cf0 | ||
|
|
119462cbc9 | ||
|
|
0324a4928c | ||
|
|
895a2a56b5 | ||
|
|
d9e616beee | ||
|
|
aa5d5d2b6d | ||
|
|
85dc555358 | ||
|
|
15b4a7d055 | ||
|
|
696c94050d | ||
|
|
b222614c4a | ||
|
|
edff694bf3 | ||
|
|
be430a6ac0 | ||
|
|
937d40c5f7 | ||
|
|
d3979676ab | ||
|
|
6716262a28 | ||
|
|
06d8bafce6 | ||
|
|
f814755908 | ||
|
|
f355fd2551 | ||
|
|
ea84c62d76 | ||
|
|
acda71cebb | ||
|
|
67dcd2e5c6 | ||
|
|
c996644613 | ||
|
|
171c3e492d | ||
|
|
8834195cc6 | ||
|
|
ed57e72fa1 | ||
|
|
7e84c3e167 | ||
|
|
4adc33471b | ||
|
|
31d07cc1e2 | ||
|
|
a349a66d5a | ||
|
|
7d427b4cc4 | ||
|
|
6c7e54868d | ||
|
|
9484a5e2ee | ||
|
|
3cbd2057e3 | ||
|
|
71ff1cf713 | ||
|
|
c737d891bc | ||
|
|
82a53343fc | ||
|
|
03fdc60018 | ||
|
|
42d19d98ad | ||
|
|
c9915bba18 | ||
|
|
a275d7ff50 | ||
|
|
74199c8624 | ||
|
|
238bff1fee | ||
|
|
de534b58c5 | ||
|
|
111a0f9f17 | ||
|
|
50392ed67d | ||
|
|
3de9da0528 | ||
|
|
ece93cadd5 | ||
|
|
7219c8d33c | ||
|
|
b0a09c7876 | ||
|
|
1d017d3cbc | ||
|
|
5fa56f2aca | ||
|
|
5ded1b139f | ||
|
|
2abb0db272 | ||
|
|
674c041c13 | ||
|
|
03c89a69a2 | ||
|
|
dbed5e2a62 | ||
|
|
d7519903a1 | ||
|
|
ab14f0d7ac | ||
|
|
3b6e43b7ca | ||
|
|
7d3b7af874 | ||
|
|
2acc348062 | ||
|
|
33ba8acd05 | ||
|
|
0c56af7090 | ||
|
|
4375ecaad7 | ||
|
|
90c538d4b6 | ||
|
|
1ffa8329d0 | ||
|
|
b404b7974a | ||
|
|
de44b580f9 | ||
|
|
2a630cd745 | ||
|
|
1ccccc5e39 | ||
|
|
aec851acfa | ||
|
|
ad5ac479fd | ||
|
|
67b82a404c | ||
|
|
f7f86a2f62 | ||
|
|
6e42b4af27 | ||
|
|
fe9ed8af76 | ||
|
|
c4c3f369c0 | ||
|
|
9b9bf40e47 | ||
|
|
14a888a610 | ||
|
|
f3d777c65c | ||
|
|
d494b6c934 | ||
|
|
7294220727 | ||
|
|
665b9087b1 | ||
|
|
f1691050cd | ||
|
|
169b6acd24 | ||
|
|
35b3bf2edb | ||
|
|
8f73c8c98b | ||
|
|
94ea8c21eb | ||
| b6a19c3e2d | |||
|
|
21455d58a6 | ||
|
|
239e9bd3db | ||
|
|
0832171a2b | ||
|
|
1edfcc629c | ||
|
|
9a2a8698ef | ||
|
|
54c31422d9 | ||
|
|
a33464284c | ||
|
|
4842caf426 | ||
|
|
02b6b4d8eb | ||
|
|
4b0d7c7d88 | ||
|
|
559b53acc3 | ||
|
|
4233c18dbb | ||
|
|
adec2c9fcc | ||
|
|
6ad2406262 | ||
|
|
bfeb8ca104 | ||
|
|
5614edfa2f | ||
|
|
946f7b2305 | ||
|
|
dfcf435f88 | ||
|
|
0e734c267c | ||
|
|
faadcec4d1 | ||
|
|
eb81784818 | ||
|
|
327010f76b | ||
|
|
371669dcb6 | ||
|
|
fb702b93ca | ||
|
|
50c8453785 | ||
|
|
14fb5ee6c4 | ||
|
|
46c2343ec6 | ||
|
|
f5c226362f | ||
|
|
9114c1157d | ||
|
|
a4f4230275 | ||
|
|
46a6147c08 | ||
|
|
0d26923d79 | ||
|
|
b57c93cf03 | ||
|
|
ec19e4bc8f | ||
|
|
cb68a9c2ce | ||
|
|
b51ded1580 | ||
|
|
4b6fb5bfeb | ||
|
|
9b8175b5be | ||
|
|
3ecfc622e2 | ||
|
|
d8d2ee4e09 | ||
|
|
be478cb088 | ||
|
|
c187b4487f | ||
|
|
f6d21e74cb | ||
|
|
bde4c4bd25 | ||
|
|
d9aaceea95 | ||
|
|
676d64a24a | ||
|
|
8ae1768f71 | ||
| 212705e7e3 | |||
|
|
b3f6524e5c | ||
|
|
5ca0a0adf2 | ||
|
|
669d2c44c9 | ||
|
|
604bc5b4c1 | ||
|
|
88268ae569 | ||
|
|
140fb86401 | ||
|
|
2b281c4357 | ||
|
|
7b6dbfb456 | ||
|
|
b3c49ac86b | ||
|
|
d309fd9c97 | ||
|
|
f3a280dcb6 | ||
|
|
43894687a2 | ||
|
|
8dd8928dab | ||
|
|
282a4d15c2 | ||
|
|
83a2ffc67e | ||
|
|
e9a6e3fae0 | ||
|
|
97944b9793 | ||
|
|
0b5d995b6d | ||
|
|
1781a9fe9a | ||
|
|
a0479bf7ca | ||
|
|
2affa31edf | ||
|
|
0bd040a851 | ||
|
|
616c6a1607 | ||
| a5176fbf80 | |||
|
|
f2b76b7582 | ||
|
|
f4847f31ae | ||
|
|
05adcf2c12 | ||
|
|
ea301be3c1 | ||
|
|
88e9785b01 | ||
|
|
78efabfc1c | ||
|
|
9d3e22200c | ||
|
|
183f9701fd | ||
|
|
ebbba134fe | ||
|
|
c257be8176 | ||
|
|
afb0aea660 | ||
|
|
960bf46f44 | ||
|
|
16db799e88 | ||
|
|
f8f2cdcfcc | ||
|
|
a1f7862f96 | ||
|
|
3ff9284f2a | ||
|
|
edf8f27c0f | ||
|
|
a720953ff3 | ||
|
|
f79dd26a82 | ||
|
|
36b5fce4dd | ||
|
|
89537322fd | ||
|
|
2229ce1fe9 | ||
|
|
f257b2177a | ||
|
|
056c1fd43e | ||
|
|
acacd3134f | ||
|
|
1fe0ba1a6e | ||
|
|
e639f4b5de | ||
|
|
5a234bd989 | ||
|
|
661227266e | ||
|
|
e01556fd9a | ||
|
|
5cb53ffc18 | ||
|
|
e7178626ab | ||
|
|
7f0948b0ed | ||
|
|
4baee67781 | ||
|
|
7061f38abe | ||
|
|
2aa259c0b2 | ||
|
|
a0f74e715a | ||
|
|
c13e761c3f | ||
|
|
2837e44bab | ||
|
|
d41e3bb41e | ||
|
|
4160bbb8c4 | ||
|
|
404a20f280 | ||
|
|
2241146b9f | ||
|
|
bcb26c5721 | ||
|
|
a681d8c1ba | ||
|
|
0515b74f75 | ||
|
|
7eeb96d88e | ||
|
|
5b6c73ef06 | ||
|
|
c70b866d16 | ||
|
|
b84f2874dc | ||
|
|
c372b5529b | ||
|
|
a43156c38d | ||
|
|
a15112febc | ||
|
|
1c6a677a39 | ||
|
|
9bf749a2c8 | ||
|
|
f6c9d9df20 | ||
|
|
5386e0ded9 | ||
|
|
8fab405a3a | ||
|
|
ee409f3ca9 | ||
|
|
bda9beacaa | ||
|
|
5d8c7e5733 | ||
|
|
a68c763125 | ||
|
|
31ac89d9d6 | ||
|
|
8b258cbbe4 | ||
|
|
53676fc0fd | ||
|
|
42c561af9e | ||
|
|
fae777c14c | ||
|
|
1af6dc614a | ||
|
|
2986004638 | ||
|
|
d6f7b4706b | ||
|
|
b40dd3e5c0 | ||
|
|
2f4097ca9d | ||
|
|
c8a91fbb25 | ||
|
|
0ec259a5fc | ||
|
|
2fda99bdb7 | ||
|
|
1bb4ac4e9c | ||
|
|
f376b98c09 | ||
|
|
7382fdb2d2 | ||
|
|
aead9592cf | ||
|
|
bedacf29d9 | ||
| 570e9a8307 | |||
|
|
fcb94ec603 | ||
| 69886ed58b | |||
|
|
798235cd21 | ||
|
|
b38b6b6f35 | ||
|
|
2dba13c52e | ||
|
|
ff366cb2c5 | ||
|
|
a87861a993 | ||
|
|
65476356c9 | ||
|
|
8af6667f3e | ||
|
|
b546df7b95 | ||
|
|
20c1f12da8 | ||
|
|
e65ff201af | ||
|
|
4f367a3dcd | ||
|
|
d7093bce4d | ||
|
|
79e3e59cae | ||
| f1b292df93 | |||
|
|
0cd383b8dd | ||
|
|
22c404a667 | ||
|
|
ba11a59d89 | ||
|
|
06020851a9 | ||
|
|
fd5cbde18c | ||
|
|
384398a1e2 | ||
|
|
2bdba4ba8b | ||
|
|
db4179a530 | ||
|
|
b905b74dc2 | ||
|
|
ff05c36856 | ||
|
|
40ea5eb53d | ||
|
|
02f48ccc7f | ||
|
|
72eaff148c | ||
|
|
8b60397f06 | ||
|
|
18d019c62a | ||
|
|
27527b18e1 | ||
|
|
ae9aa2662a | ||
|
|
3bfb593b21 | ||
|
|
8556e99241 | ||
|
|
e885822a34 | ||
|
|
8ebb564a79 | ||
|
|
7dc176edcb | ||
|
|
5167fe078b | ||
|
|
bc7188c8a8 | ||
|
|
5a05cb96be | ||
|
|
0258726f0a | ||
|
|
2fa9aa04f4 | ||
|
|
a5f9927459 | ||
|
|
61b422502b | ||
|
|
1e57b5ea49 | ||
|
|
12ce915e8e | ||
|
|
a32273af91 | ||
|
|
96a327af17 | ||
|
|
1910e81ad9 | ||
|
|
80593e774c | ||
|
|
c092fc8e18 | ||
|
|
579efa15c7 | ||
|
|
37ff4e9aeb | ||
|
|
4e4a7de5d4 | ||
|
|
2d1bc6436a | ||
|
|
65726d75cc | ||
|
|
d63c0a32eb | ||
|
|
e49c4162e5 | ||
|
|
ffc5ad5ce7 | ||
|
|
2dbfc28d69 | ||
|
|
1ac7b2b8cb | ||
|
|
7d047e612e | ||
|
|
24f2999669 | ||
|
|
b08728b645 | ||
|
|
00dee43a1e | ||
|
|
21e300b9f3 | ||
|
|
158f0aa2d9 | ||
|
|
10fb763d66 | ||
|
|
4aa2c1c2c2 | ||
|
|
bdf044d264 | ||
|
|
6049a1f2f5 | ||
|
|
a9fea9f606 | ||
|
|
11002e9d45 | ||
|
|
e7a0b850df | ||
|
|
736c902f3c | ||
|
|
a67ff564d0 | ||
|
|
2a778383b1 | ||
|
|
36457400e7 | ||
|
|
1d629e7b2e | ||
|
|
5f764ab8f5 | ||
|
|
a8c9edbc3f | ||
|
|
0a5bffe826 | ||
|
|
9c9b6bc0d6 | ||
|
|
562f7e7e41 | ||
|
|
594c55afa6 | ||
|
|
0abf97e999 | ||
|
|
b811aec773 | ||
|
|
ca47f566dc | ||
|
|
65674f7fd4 | ||
|
|
dcc510ff6c | ||
|
|
a4fe43a964 | ||
|
|
3dab4c07cf | ||
|
|
ee3248ea5d | ||
| 7be5ec0521 | |||
|
|
04e90cc279 | ||
| ec8d488249 | |||
|
|
41fdafac45 | ||
| d2e2622279 | |||
|
|
9476bd6527 | ||
| 24a06ea6f6 | |||
|
|
2567f8eefb | ||
|
|
fa5f5ce251 | ||
|
|
728a61756a | ||
|
|
736ccbe376 | ||
|
|
fa2b226b9e | ||
|
|
e8c5ae194d | ||
|
|
cc2feab37e | ||
|
|
83b084a90b | ||
|
|
e2ac0722c8 | ||
|
|
c3efb40b8e | ||
|
|
03d7a416f3 | ||
|
|
e6e812fdb0 | ||
|
|
b34160eeec | ||
|
|
d01aeab242 | ||
| 9904e01252 | |||
|
|
7f068b691b | ||
|
|
59558efed1 | ||
|
|
a88e19a8ed | ||
|
|
0c17f0825b | ||
|
|
9384d2523a | ||
| 0e13172a89 | |||
|
|
92f34452b5 | ||
| db54929584 | |||
|
|
ba23cafb18 | ||
| a2e189767b | |||
| 0d236fd678 | |||
|
|
9979b160c7 | ||
| 65b332c395 | |||
|
|
b867ac8cc7 | ||
|
|
9a0ff24ffe | ||
|
|
9540a12b6f | ||
|
|
fdfb53c05c | ||
|
|
3ec979cc40 | ||
|
|
9ea08c8a4b | ||
|
|
629eada5c3 | ||
|
|
a1220c77da | ||
|
|
b4668367c6 | ||
|
|
77da40e507 | ||
|
|
b3b2748bb7 | ||
|
|
26e8143616 | ||
|
|
1444fe5468 | ||
|
|
ac431e3ece | ||
|
|
563a4137bd | ||
|
|
19cbcd0c1d | ||
|
|
8803b60b28 | ||
|
|
e9f59ae769 | ||
|
|
5cf3bee336 | ||
|
|
060fe835c7 | ||
|
|
2688ea8f59 | ||
|
|
8eb61cf752 | ||
|
|
6628901d46 | ||
|
|
5a31882be3 | ||
|
|
a42da09d6c | ||
|
|
658cf5c873 | ||
|
|
b8c752b740 | ||
|
|
9151ae7081 | ||
| a1a894f722 | |||
|
|
3352ee3151 | ||
|
|
da3533a430 | ||
| 0bb7f9becf | |||
|
|
40c64ee2d8 | ||
|
|
66651f7111 | ||
|
|
c7d0bd5dec | ||
| 9ce0a9d49d | |||
|
|
2070b353c2 | ||
| 1d91f3b91b | |||
|
|
8b0aebfddb | ||
| 467905d7b0 | |||
|
|
cc81921bcb | ||
| 8600c04ff3 | |||
|
|
3f31445f12 | ||
| 8a33371f37 | |||
|
|
1164bd7183 | ||
| 589fcd09c0 | |||
|
|
c5d49016d4 | ||
|
|
8886b12151 | ||
|
|
336ffd7cf0 | ||
|
|
3088778a9f | ||
|
|
4c1de83b24 | ||
|
|
ee2fbfc2d1 | ||
|
|
dff7fe722b | ||
|
|
eb6dac2e9f | ||
|
|
255760de16 | ||
|
|
a2373b817a | ||
|
|
7d4c45c4c0 |
3
.github/CONTRIBUTING.md
vendored
3
.github/CONTRIBUTING.md
vendored
@@ -11,6 +11,7 @@ Do not report crashes in the GitHub issue tracker. NewPipe has an automated cras
|
||||
|
||||
* 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.
|
||||
@@ -27,7 +28,7 @@ Do not report crashes in the GitHub issue tracker. NewPipe has an automated cras
|
||||
* 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 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.
|
||||
* 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!
|
||||
* Try to figure out yourself why builds on our CI fail.
|
||||
|
||||
@@ -5,7 +5,7 @@ android:
|
||||
components:
|
||||
# The BuildTools version used by NewPipe
|
||||
- tools
|
||||
- build-tools-27.0.1
|
||||
- build-tools-27.0.3
|
||||
|
||||
# The SDK version used to compile NewPipe
|
||||
- android-27
|
||||
|
||||
Binary file not shown.
@@ -1,170 +0,0 @@
|
||||
import java.io.BufferedReader;
|
||||
import java.io.File;
|
||||
import java.io.FileReader;
|
||||
import java.io.IOException;
|
||||
import java.util.regex.*;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.ArrayList;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.charset.Charset;
|
||||
|
||||
public final class CheckTranslations {
|
||||
|
||||
private static boolean debug = false;
|
||||
private static boolean plurals = false;
|
||||
private static boolean empty = false;
|
||||
private static boolean remove = false;
|
||||
private static int checks = 0;
|
||||
private static int matches = 0;
|
||||
private static int changes = 0;
|
||||
private static Pattern p, pb, pe, e, o;
|
||||
|
||||
/**
|
||||
* Search translated strings.xml files for empty item / plural tags
|
||||
* and remove them.
|
||||
* @param args directories which contain string.xml files (in any subdirectory)
|
||||
* -e option to find all empty string tags
|
||||
* -p option to find all empty plurals and item tags
|
||||
* -r option to remove all occurrences from the files
|
||||
* -d option to see more details
|
||||
*/
|
||||
public static void main(String[] args) {
|
||||
if (args.length < 1 || (args[0].equals("-d") && args.length < 2)) {
|
||||
System.out.println("Not enough arguments");
|
||||
return;
|
||||
}
|
||||
for (int i = 0; i < args.length; i++) {
|
||||
switch (args[i]) {
|
||||
case "-d":
|
||||
debug = true;
|
||||
break;
|
||||
case "-p":
|
||||
plurals = true;
|
||||
break;
|
||||
case "-e":
|
||||
empty = true;
|
||||
break;
|
||||
case "-r":
|
||||
remove = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!plurals && !empty) {
|
||||
plurals = true;
|
||||
empty = true;
|
||||
}
|
||||
|
||||
p = Pattern.compile("(<item quantity=\")(zero|one|two|three|few|many|other)(\"></item>|\"/>)");
|
||||
pb = Pattern.compile("(<plurals[\\sa-zA-Z=\"]*>)");
|
||||
pe = Pattern.compile("(</plurals>)");
|
||||
e = Pattern.compile("(<string[\\sa-z_\\\"=]*)((><\\/string>|\\/>){1})");
|
||||
o = Pattern.compile("(<item quantity=\"other\">)[^</>]*(<\\/item>)");
|
||||
|
||||
for (int i = 0; i < args.length; i++) {
|
||||
if (!args[i].equals("-d") && !args[i].equals("-p") && !args[i].equals("-e") && !args[i].equals("-r")) {
|
||||
File f = new File(args[i]);
|
||||
if (f.exists() && !f.isDirectory()) {
|
||||
checkFile(f);
|
||||
} else if (f.isDirectory()) {
|
||||
checkFiles(f.listFiles());
|
||||
} else {
|
||||
System.out.println("'" + args[i] + "' does not exist!");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
System.out.println(checks + " files were checked.");
|
||||
System.out.println(matches + " corrupt lines detected.");
|
||||
if (remove) {
|
||||
System.out.println(matches + " corrupt lines removed and " + changes + " lines fixed.");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private static void checkFiles(File[] f) {
|
||||
for (int i = 0; i < f.length; i++) {
|
||||
if (f[i].exists() && !f[i].isDirectory()) {
|
||||
if (f[i].toString().contains("strings.xml")) {
|
||||
checkFile(f[i]);
|
||||
}
|
||||
} else if (f[i].isDirectory()) {
|
||||
checkFiles(f[i].listFiles());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void checkFile(File f) {
|
||||
// Do not check our original English strings to cause no unwanted changes
|
||||
// Btw. there should not be empty plural/item tags
|
||||
if (f.toString().contains("values/strings.xml")) {
|
||||
return;
|
||||
}
|
||||
if (debug) System.out.println("Checking " + f.toString());
|
||||
checks++;
|
||||
|
||||
|
||||
List<String> lines = new ArrayList<String>();
|
||||
boolean checkFailed = false;
|
||||
boolean otherDetected = false;
|
||||
boolean inPlurals = false;
|
||||
try (BufferedReader br = new BufferedReader(new FileReader(f))) {
|
||||
String line;
|
||||
int ln = 0;
|
||||
while ((line = br.readLine()) != null) {
|
||||
ln++;
|
||||
if (plurals && p.matcher(line).find()) {
|
||||
matches++;
|
||||
if (debug) System.out.println(" Line " + ln + " was " + ((remove) ? "removed" : "detected") + ": '" + line + "'");
|
||||
checkFailed = true;
|
||||
} else if (empty && e.matcher(line).find()) {
|
||||
matches++;
|
||||
checkFailed = true;
|
||||
if (debug) System.out.println(" Line " + ln + " was " + ((remove) ? "removed" : "detected") + ": '" + line + "'");
|
||||
} else {
|
||||
if (remove) lines.add(line);
|
||||
}
|
||||
}
|
||||
br.close();
|
||||
int pluralsLine = 0;
|
||||
for (int i = 0; i < lines.size(); i++) {
|
||||
if (o.matcher(lines.get(i)).find()) {
|
||||
otherDetected = true;
|
||||
}
|
||||
if (plurals && pb.matcher(lines.get(i)).find()) {
|
||||
inPlurals = true;
|
||||
pluralsLine = i;
|
||||
} else if (plurals && pe.matcher(lines.get(i)).find()) {
|
||||
inPlurals = false;
|
||||
if (!otherDetected) {
|
||||
boolean b = false;
|
||||
check: for(int j = pluralsLine; j < i; j++) {
|
||||
if (lines.get(j).contains("many")) {
|
||||
b = true;
|
||||
pluralsLine = j;
|
||||
break check;
|
||||
}
|
||||
}
|
||||
if (remove && b) {
|
||||
if (debug) System.out.println(" Line " + (pluralsLine + 1) + " was " + ((remove) ? "changed" : "detected") + ": '" + lines.get(pluralsLine) + "'");
|
||||
lines.set(pluralsLine, lines.get(pluralsLine).replace("many", "other"));
|
||||
changes++;
|
||||
checkFailed = true;
|
||||
} else if (debug) {
|
||||
if (debug) System.out.println(" WARNING: Line " + (i + 1) + " - No <item quantity=\"other\"> found!");
|
||||
}
|
||||
}
|
||||
otherDetected = false;
|
||||
}
|
||||
|
||||
}
|
||||
if (remove && checkFailed) {
|
||||
Files.write(f.toPath(), lines, Charset.forName("UTF-8"));
|
||||
}
|
||||
} catch (IOException e) {
|
||||
System.out.println(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
40
README.md
40
README.md
@@ -59,18 +59,17 @@ NewPipe does not use any Google framework libraries, or the YouTube API. It only
|
||||
* Search/Watch Playlists
|
||||
* Watch as queues Playlists
|
||||
* Queuing videos
|
||||
* Local playlists
|
||||
* Subtitles
|
||||
* Multi-service support (eg. SoundCloud in NewPipe Beta)
|
||||
|
||||
### Coming Features
|
||||
|
||||
* Multiservice support (eg. SoundCloud)
|
||||
* Bookmarks
|
||||
* Subtitles support
|
||||
* livestream support
|
||||
* Livestream support
|
||||
* Cast to UPnP and Cast
|
||||
* Show comments
|
||||
* ... and many more
|
||||
|
||||
### Multiservice support
|
||||
Although NewPipe only supports YouTube at the moment, it's designed to support many more streaming services. The plan is, that NewPipe will get such support by the version 2.0.
|
||||
|
||||
## Contribution
|
||||
Whether you have ideas, translations, design changes, code cleaning, or real heavy code changes, help is always welcome.
|
||||
The more is done the better it gets!
|
||||
@@ -78,19 +77,24 @@ The more is done the better it gets!
|
||||
If you'd like to get involved, check our [contribution notes](.github/CONTRIBUTING.md).
|
||||
|
||||
## Donate
|
||||
If you like NewPipe we'd be happy about a donation. You can either donate via Bitcoin or BountySource. For further information about donating to NewPipe, please visit our [website](https://newpipe.schabi.org/donate/).
|
||||
If you like NewPipe we'd be happy about a donation. You can either donate via Bitcoin, Bountysource or Liberapay. For further information about donating to NewPipe, please visit our [website](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://www.bountysource.com/teams/newpipe"><img src="https://upload.wikimedia.org/wikipedia/commons/thumb/2/22/Bountysource.png/320px-Bountysource.png" alz="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>
|
||||
<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>
|
||||
|
||||
## License
|
||||
|
||||
@@ -2,14 +2,14 @@ apply plugin: 'com.android.application'
|
||||
|
||||
android {
|
||||
compileSdkVersion 27
|
||||
buildToolsVersion '27.0.1'
|
||||
buildToolsVersion '27.0.3'
|
||||
|
||||
defaultConfig {
|
||||
applicationId "org.schabi.newpipe"
|
||||
minSdkVersion 15
|
||||
targetSdkVersion 27
|
||||
versionCode 48
|
||||
versionName "0.12.0"
|
||||
versionCode 64
|
||||
versionName "0.13.5"
|
||||
|
||||
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
|
||||
vectorDrawables.useSupportLibrary = true
|
||||
@@ -26,13 +26,6 @@ android {
|
||||
debuggable true
|
||||
applicationIdSuffix ".debug"
|
||||
}
|
||||
beta {
|
||||
minifyEnabled true
|
||||
shrinkResources true
|
||||
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
|
||||
|
||||
applicationIdSuffix ".beta"
|
||||
}
|
||||
}
|
||||
|
||||
lintOptions {
|
||||
@@ -48,14 +41,20 @@ android {
|
||||
}
|
||||
|
||||
ext {
|
||||
supportLibVersion = '27.0.2'
|
||||
supportLibVersion = '27.1.1'
|
||||
exoPlayerLibVersion = '2.7.3'
|
||||
roomDbLibVersion = '1.0.0'
|
||||
leakCanaryLibVersion = '1.5.4'
|
||||
okHttpLibVersion = '1.5.0'
|
||||
icepickLibVersion = '3.2.0'
|
||||
stethoLibVersion = '1.5.0'
|
||||
}
|
||||
dependencies {
|
||||
androidTestImplementation('com.android.support.test.espresso:espresso-core:2.2.2') {
|
||||
exclude module: 'support-annotations'
|
||||
}
|
||||
|
||||
implementation 'com.github.TeamNewPipe:NewPipeExtractor:7716b1437815'
|
||||
implementation 'com.github.TeamNewPipe:NewPipeExtractor:bf1c771'
|
||||
|
||||
testImplementation 'junit:junit:4.12'
|
||||
testImplementation 'org.mockito:mockito-core:1.10.19'
|
||||
@@ -66,34 +65,34 @@ dependencies {
|
||||
implementation "com.android.support:recyclerview-v7:$supportLibVersion"
|
||||
implementation "com.android.support:preference-v14:$supportLibVersion"
|
||||
|
||||
implementation 'com.google.code.gson:gson:2.8.2'
|
||||
implementation 'ch.acra:acra:4.9.2'
|
||||
|
||||
implementation 'com.nostra13.universalimageloader:universal-image-loader:1.9.5'
|
||||
implementation 'de.hdodenhof:circleimageview:2.2.0'
|
||||
implementation 'com.github.nirhart:ParallaxScroll:dd53d1f9d1'
|
||||
implementation 'com.nononsenseapps:filepicker:3.0.1'
|
||||
implementation 'com.google.android.exoplayer:exoplayer:2.6.0'
|
||||
implementation 'com.nononsenseapps:filepicker:4.2.1'
|
||||
|
||||
debugImplementation 'com.facebook.stetho:stetho:1.5.0'
|
||||
debugImplementation 'com.facebook.stetho:stetho-urlconnection:1.5.0'
|
||||
debugImplementation 'com.android.support:multidex:1.0.2'
|
||||
implementation "com.google.android.exoplayer:exoplayer:$exoPlayerLibVersion"
|
||||
implementation "com.google.android.exoplayer:extension-mediasession:$exoPlayerLibVersion"
|
||||
|
||||
implementation 'io.reactivex.rxjava2:rxjava:2.1.7'
|
||||
implementation 'io.reactivex.rxjava2:rxandroid:2.0.1'
|
||||
implementation 'com.jakewharton.rxbinding2:rxbinding:2.0.0'
|
||||
debugImplementation "com.facebook.stetho:stetho:$stethoLibVersion"
|
||||
debugImplementation "com.facebook.stetho:stetho-urlconnection:$stethoLibVersion"
|
||||
debugImplementation 'com.android.support:multidex:1.0.3'
|
||||
|
||||
implementation 'android.arch.persistence.room:runtime:1.0.0'
|
||||
implementation 'android.arch.persistence.room:rxjava2:1.0.0'
|
||||
annotationProcessor 'android.arch.persistence.room:compiler:1.0.0'
|
||||
implementation 'io.reactivex.rxjava2:rxjava:2.1.10'
|
||||
implementation 'io.reactivex.rxjava2:rxandroid:2.0.2'
|
||||
implementation 'com.jakewharton.rxbinding2:rxbinding:2.1.1'
|
||||
|
||||
implementation 'frankiesardo:icepick:3.2.0'
|
||||
annotationProcessor 'frankiesardo:icepick-processor:3.2.0'
|
||||
implementation "android.arch.persistence.room:runtime:$roomDbLibVersion"
|
||||
implementation "android.arch.persistence.room:rxjava2:$roomDbLibVersion"
|
||||
annotationProcessor "android.arch.persistence.room:compiler:$roomDbLibVersion"
|
||||
|
||||
debugImplementation 'com.squareup.leakcanary:leakcanary-android:1.5.4'
|
||||
betaImplementation 'com.squareup.leakcanary:leakcanary-android-no-op:1.5.4'
|
||||
releaseImplementation 'com.squareup.leakcanary:leakcanary-android-no-op:1.5.4'
|
||||
implementation "frankiesardo:icepick:$icepickLibVersion"
|
||||
annotationProcessor "frankiesardo:icepick-processor:$icepickLibVersion"
|
||||
|
||||
debugImplementation "com.squareup.leakcanary:leakcanary-android:$leakCanaryLibVersion"
|
||||
releaseImplementation "com.squareup.leakcanary:leakcanary-android-no-op:$leakCanaryLibVersion"
|
||||
|
||||
implementation 'com.squareup.okhttp3:okhttp:3.9.1'
|
||||
debugImplementation 'com.facebook.stetho:stetho-okhttp3:1.5.0'
|
||||
debugImplementation "com.facebook.stetho:stetho-okhttp3:$okHttpLibVersion"
|
||||
}
|
||||
|
||||
6
app/proguard-rules.pro
vendored
6
app/proguard-rules.pro
vendored
@@ -42,3 +42,9 @@
|
||||
-dontwarn javax.annotation.**
|
||||
# A resource is loaded with a relative path so the package of this class must be preserved.
|
||||
-keepnames class okhttp3.internal.publicsuffix.PublicSuffixDatabase
|
||||
-keepclassmembers class * implements java.io.Serializable {
|
||||
static final long serialVersionUID;
|
||||
!static !transient <fields>;
|
||||
private void writeObject(java.io.ObjectOutputStream);
|
||||
private void readObject(java.io.ObjectInputStream);
|
||||
}
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools">
|
||||
|
||||
<application
|
||||
android:label="NewPipe Beta"
|
||||
tools:replace="android:label">
|
||||
</application>
|
||||
|
||||
</manifest>
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 5.8 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 3.0 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 8.1 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 13 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 19 KiB |
@@ -28,6 +28,12 @@
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<receiver android:name="android.support.v4.media.session.MediaButtonReceiver" >
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MEDIA_BUTTON" />
|
||||
</intent-filter>
|
||||
</receiver>
|
||||
|
||||
<activity
|
||||
android:name=".player.old.PlayVideoActivity"
|
||||
android:configChanges="orientation|keyboardHidden|screenSize"
|
||||
@@ -36,7 +42,11 @@
|
||||
|
||||
<service
|
||||
android:name=".player.BackgroundPlayer"
|
||||
android:exported="false"/>
|
||||
android:exported="false">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MEDIA_BUTTON" />
|
||||
</intent-filter>
|
||||
</service>
|
||||
|
||||
<activity
|
||||
android:name=".player.BackgroundPlayerActivity"
|
||||
@@ -70,6 +80,9 @@
|
||||
android:name=".history.HistoryActivity"
|
||||
android:label="@string/title_activity_history"/>
|
||||
|
||||
<service android:name=".local.subscription.services.SubscriptionsImportService"/>
|
||||
<service android:name=".local.subscription.services.SubscriptionsExportService"/>
|
||||
|
||||
<activity
|
||||
android:name=".PanicResponderActivity"
|
||||
android:launchMode="singleInstance"
|
||||
@@ -117,13 +130,13 @@
|
||||
android:grantUriPermissions="true">
|
||||
<meta-data
|
||||
android:name="android.support.FILE_PROVIDER_PATHS"
|
||||
android:resource="@xml/provider_paths"/>
|
||||
android:resource="@xml/nnf_provider_paths"/>
|
||||
</provider>
|
||||
|
||||
<activity
|
||||
android:name=".RouterActivity"
|
||||
android:excludeFromRecents="true"
|
||||
android:label="@string/preferred_player_share_menu_title"
|
||||
android:label="@string/preferred_open_action_share_menu_title"
|
||||
android:taskAffinity=""
|
||||
android:theme="@style/RouterActivityThemeDark">
|
||||
|
||||
|
||||
@@ -89,7 +89,6 @@ public class App extends Application {
|
||||
SettingsActivity.initSettings(this);
|
||||
|
||||
NewPipe.init(getDownloader());
|
||||
NewPipeDatabase.init(this);
|
||||
StateSaver.init(this);
|
||||
initNotificationChannel();
|
||||
|
||||
@@ -168,6 +167,7 @@ public class App extends Application {
|
||||
return new ImageLoaderConfiguration.Builder(this)
|
||||
.memoryCache(new LRULimitedMemoryCache(memoryCacheSizeMb * 1024 * 1024))
|
||||
.diskCacheSize(diskCacheSizeMb * 1024 * 1024)
|
||||
.imageDownloader(new ImageDownloader(getApplicationContext()))
|
||||
.build();
|
||||
}
|
||||
|
||||
|
||||
@@ -1,18 +1,14 @@
|
||||
package org.schabi.newpipe;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.res.TypedArray;
|
||||
import android.os.Bundle;
|
||||
import android.support.annotation.AttrRes;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.v4.app.Fragment;
|
||||
import android.support.v7.app.AppCompatActivity;
|
||||
import android.util.Log;
|
||||
import android.view.View;
|
||||
|
||||
import com.nostra13.universalimageloader.core.DisplayImageOptions;
|
||||
import com.nostra13.universalimageloader.core.ImageLoader;
|
||||
import com.nostra13.universalimageloader.core.display.FadeInBitmapDisplayer;
|
||||
import com.squareup.leakcanary.RefWatcher;
|
||||
|
||||
import icepick.Icepick;
|
||||
@@ -87,33 +83,13 @@ public abstract class BaseFragment extends Fragment {
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// DisplayImageOptions default configurations
|
||||
// Utils
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
public static final DisplayImageOptions BASE_OPTIONS =
|
||||
new DisplayImageOptions.Builder().cacheInMemory(true).build();
|
||||
|
||||
public static final DisplayImageOptions DISPLAY_AVATAR_OPTIONS =
|
||||
new DisplayImageOptions.Builder()
|
||||
.cloneFrom(BASE_OPTIONS)
|
||||
.showImageOnLoading(R.drawable.buddy)
|
||||
.showImageForEmptyUri(R.drawable.buddy)
|
||||
.showImageOnFail(R.drawable.buddy)
|
||||
.build();
|
||||
|
||||
public static final DisplayImageOptions DISPLAY_THUMBNAIL_OPTIONS =
|
||||
new DisplayImageOptions.Builder()
|
||||
.cloneFrom(BASE_OPTIONS)
|
||||
.displayer(new FadeInBitmapDisplayer(250))
|
||||
.showImageForEmptyUri(R.drawable.dummy_thumbnail)
|
||||
.showImageOnFail(R.drawable.dummy_thumbnail)
|
||||
.build();
|
||||
|
||||
public static final DisplayImageOptions DISPLAY_BANNER_OPTIONS =
|
||||
new DisplayImageOptions.Builder()
|
||||
.cloneFrom(BASE_OPTIONS)
|
||||
.showImageOnLoading(R.drawable.channel_banner)
|
||||
.showImageForEmptyUri(R.drawable.channel_banner)
|
||||
.showImageOnFail(R.drawable.channel_banner)
|
||||
.build();
|
||||
public void setTitle(String title) {
|
||||
if (DEBUG) Log.d(TAG, "setTitle() called with: title = [" + title + "]");
|
||||
if (activity != null && activity.getSupportActionBar() != null) {
|
||||
activity.getSupportActionBar().setTitle(title);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import android.text.TextUtils;
|
||||
import org.schabi.newpipe.extractor.exceptions.ReCaptchaException;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
@@ -72,6 +73,31 @@ public class Downloader implements org.schabi.newpipe.extractor.Downloader {
|
||||
mCookies = cookies;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the size of the content that the url is pointing by firing a HEAD request.
|
||||
*
|
||||
* @param url an url pointing to the content
|
||||
* @return the size of the content, in bytes
|
||||
*/
|
||||
public long getContentLength(String url) throws IOException {
|
||||
Response response = null;
|
||||
try {
|
||||
final Request request = new Request.Builder()
|
||||
.head().url(url)
|
||||
.addHeader("User-Agent", USER_AGENT)
|
||||
.build();
|
||||
response = client.newCall(request).execute();
|
||||
|
||||
return Long.parseLong(response.header("Content-Length"));
|
||||
} catch (NumberFormatException e) {
|
||||
throw new IOException("Invalid content length", e);
|
||||
} finally {
|
||||
if (response != null) {
|
||||
response.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Download the text file at the supplied URL as in download(String),
|
||||
* but set the HTTP header field "Accept-Language" to the supplied string.
|
||||
@@ -98,6 +124,18 @@ public class Downloader implements org.schabi.newpipe.extractor.Downloader {
|
||||
*/
|
||||
@Override
|
||||
public String download(String siteUrl, Map<String, String> customProperties) throws IOException, ReCaptchaException {
|
||||
return getBody(siteUrl, customProperties).string();
|
||||
}
|
||||
|
||||
public InputStream stream(String siteUrl) throws IOException {
|
||||
try {
|
||||
return getBody(siteUrl, Collections.emptyMap()).byteStream();
|
||||
} catch (ReCaptchaException e) {
|
||||
throw new IOException(e.getMessage(), e.getCause());
|
||||
}
|
||||
}
|
||||
|
||||
private ResponseBody getBody(String siteUrl, Map<String, String> customProperties) throws IOException, ReCaptchaException {
|
||||
final Request.Builder requestBuilder = new Request.Builder()
|
||||
.method("GET", null).url(siteUrl)
|
||||
.addHeader("User-Agent", USER_AGENT);
|
||||
@@ -123,7 +161,7 @@ public class Downloader implements org.schabi.newpipe.extractor.Downloader {
|
||||
return null;
|
||||
}
|
||||
|
||||
return body.string();
|
||||
return body;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
46
app/src/main/java/org/schabi/newpipe/ImageDownloader.java
Normal file
46
app/src/main/java/org/schabi/newpipe/ImageDownloader.java
Normal file
@@ -0,0 +1,46 @@
|
||||
package org.schabi.newpipe;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.content.Context;
|
||||
import android.content.SharedPreferences;
|
||||
import android.content.res.Resources;
|
||||
import android.preference.PreferenceManager;
|
||||
|
||||
import com.nostra13.universalimageloader.core.download.BaseImageDownloader;
|
||||
|
||||
import org.schabi.newpipe.extractor.NewPipe;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
|
||||
public class ImageDownloader extends BaseImageDownloader {
|
||||
private final Resources resources;
|
||||
private final SharedPreferences preferences;
|
||||
private final String downloadThumbnailKey;
|
||||
|
||||
public ImageDownloader(Context context) {
|
||||
super(context);
|
||||
this.resources = context.getResources();
|
||||
this.preferences = PreferenceManager.getDefaultSharedPreferences(context);
|
||||
this.downloadThumbnailKey = context.getString(R.string.download_thumbnail_key);
|
||||
}
|
||||
|
||||
private boolean isDownloadingThumbnail() {
|
||||
return preferences.getBoolean(downloadThumbnailKey, true);
|
||||
}
|
||||
|
||||
@SuppressLint("ResourceType")
|
||||
@Override
|
||||
public InputStream getStream(String imageUri, Object extra) throws IOException {
|
||||
if (isDownloadingThumbnail()) {
|
||||
return super.getStream(imageUri, extra);
|
||||
} else {
|
||||
return resources.openRawResource(R.drawable.dummy_thumbnail_dark);
|
||||
}
|
||||
}
|
||||
|
||||
protected InputStream getStreamFromNetwork(String imageUri, Object extra) throws IOException {
|
||||
final Downloader downloader = (Downloader) NewPipe.getDownloader();
|
||||
return downloader.stream(imageUri);
|
||||
}
|
||||
}
|
||||
@@ -22,11 +22,13 @@ package org.schabi.newpipe;
|
||||
|
||||
import android.content.Intent;
|
||||
import android.content.SharedPreferences;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
import android.os.Handler;
|
||||
import android.os.Looper;
|
||||
import android.preference.PreferenceManager;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.design.widget.NavigationView;
|
||||
import android.support.v4.app.Fragment;
|
||||
import android.support.v4.view.GravityCompat;
|
||||
@@ -44,7 +46,6 @@ import android.view.View;
|
||||
import android.widget.Button;
|
||||
import android.widget.ImageButton;
|
||||
import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
|
||||
import org.schabi.newpipe.extractor.NewPipe;
|
||||
import org.schabi.newpipe.extractor.StreamingService;
|
||||
@@ -55,6 +56,7 @@ import org.schabi.newpipe.fragments.list.search.SearchFragment;
|
||||
import org.schabi.newpipe.report.ErrorActivity;
|
||||
import org.schabi.newpipe.util.Constants;
|
||||
import org.schabi.newpipe.util.NavigationHelper;
|
||||
import org.schabi.newpipe.util.PermissionHelper;
|
||||
import org.schabi.newpipe.util.ServiceHelper;
|
||||
import org.schabi.newpipe.util.StateSaver;
|
||||
import org.schabi.newpipe.util.ThemeHelper;
|
||||
@@ -94,44 +96,47 @@ public class MainActivity extends AppCompatActivity {
|
||||
drawer = findViewById(R.id.drawer_layout);
|
||||
drawerItems = findViewById(R.id.navigation);
|
||||
|
||||
//drawerItems.setItemIconTintList(null); // Set null to use the original icon
|
||||
for(StreamingService s : NewPipe.getServices()) {
|
||||
final String title = s.getServiceInfo().getName() +
|
||||
(ServiceHelper.isBeta(s) ? " (beta)" : "");
|
||||
final MenuItem item = drawerItems.getMenu()
|
||||
.add(R.id.menu_services_group, s.getServiceId(), 0, title);
|
||||
item.setIcon(ServiceHelper.getIcon(s.getServiceId()));
|
||||
}
|
||||
|
||||
drawerItems.getMenu().getItem(ServiceHelper.getSelectedServiceId(this)).setChecked(true);
|
||||
|
||||
if (!BuildConfig.BUILD_TYPE.equals("release")) {
|
||||
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;
|
||||
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) {
|
||||
lastService = ServiceHelper.getSelectedServiceId(MainActivity.this);
|
||||
@Override
|
||||
public void onDrawerOpened(View drawerView) {
|
||||
lastService = ServiceHelper.getSelectedServiceId(MainActivity.this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDrawerClosed(View drawerView) {
|
||||
if (lastService != ServiceHelper.getSelectedServiceId(MainActivity.this)) {
|
||||
new Handler(Looper.getMainLooper()).post(MainActivity.this::recreate);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@Override
|
||||
public void onDrawerClosed(View drawerView) {
|
||||
if (lastService != ServiceHelper.getSelectedServiceId(MainActivity.this)) {
|
||||
new Handler(Looper.getMainLooper()).post(MainActivity.this::recreate);
|
||||
}
|
||||
}
|
||||
});
|
||||
drawerItems.setNavigationItemSelectedListener(this::changeService);
|
||||
|
||||
drawerItems.setNavigationItemSelectedListener(this::changeService);
|
||||
|
||||
setupDrawerFooter();
|
||||
setupDrawerHeader();
|
||||
} else {
|
||||
drawer.setDrawerLockMode(DrawerLayout.LOCK_MODE_LOCKED_CLOSED);
|
||||
}
|
||||
setupDrawerFooter();
|
||||
setupDrawerHeader();
|
||||
}
|
||||
|
||||
|
||||
private boolean changeService(MenuItem item) {
|
||||
if (item.getGroupId() == R.id.menu_services_group) {
|
||||
drawerItems.getMenu().getItem(ServiceHelper.getSelectedServiceId(this)).setChecked(false);
|
||||
ServiceHelper.setSelectedServiceId(this, item.getTitle().toString());
|
||||
ServiceHelper.setSelectedServiceId(this, item.getItemId());
|
||||
drawerItems.getMenu().getItem(ServiceHelper.getSelectedServiceId(this)).setChecked(true);
|
||||
headerServiceView.setText("gurken");
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
@@ -146,7 +151,8 @@ public class MainActivity extends AppCompatActivity {
|
||||
|
||||
settings.setOnClickListener(view -> NavigationHelper.openSettings(this));
|
||||
downloads.setOnClickListener(view ->NavigationHelper.openDownloads(this));
|
||||
history.setOnClickListener(view -> NavigationHelper.openHistory(this));
|
||||
history.setOnClickListener(view ->
|
||||
NavigationHelper.openStatisticFragment(getSupportFragmentManager()));
|
||||
}
|
||||
|
||||
private void setupDrawerHeader() {
|
||||
@@ -230,6 +236,26 @@ public class MainActivity extends AppCompatActivity {
|
||||
} else super.onBackPressed();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
|
||||
for (int i: grantResults){
|
||||
if (i == PackageManager.PERMISSION_DENIED){
|
||||
return;
|
||||
}
|
||||
}
|
||||
switch (requestCode) {
|
||||
case PermissionHelper.DOWNLOADS_REQUEST_CODE:
|
||||
NavigationHelper.openDownloads(this);
|
||||
break;
|
||||
case PermissionHelper.DOWNLOAD_DIALOG_REQUEST_CODE:
|
||||
Fragment fragment = getSupportFragmentManager().findFragmentById(R.id.fragment_holder);
|
||||
if (fragment instanceof VideoDetailFragment) {
|
||||
((VideoDetailFragment) fragment).openDownloadDialog();
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Implement the following diagram behavior for the up button:
|
||||
* <pre>
|
||||
@@ -302,16 +328,16 @@ public class MainActivity extends AppCompatActivity {
|
||||
case android.R.id.home:
|
||||
onHomeButtonPressed();
|
||||
return true;
|
||||
case R.id.action_settings:
|
||||
NavigationHelper.openSettings(this);
|
||||
return true;
|
||||
case R.id.action_show_downloads:
|
||||
return NavigationHelper.openDownloads(this);
|
||||
case R.id.action_history:
|
||||
NavigationHelper.openStatisticFragment(getSupportFragmentManager());
|
||||
return true;
|
||||
case R.id.action_about:
|
||||
NavigationHelper.openAbout(this);
|
||||
return true;
|
||||
case R.id.action_history:
|
||||
NavigationHelper.openHistory(this);
|
||||
case R.id.action_settings:
|
||||
NavigationHelper.openSettings(this);
|
||||
return true;
|
||||
default:
|
||||
return super.onOptionsItemSelected(item);
|
||||
|
||||
@@ -11,31 +11,32 @@ import static org.schabi.newpipe.database.Migrations.MIGRATION_11_12;
|
||||
|
||||
public final class NewPipeDatabase {
|
||||
|
||||
private static AppDatabase databaseInstance;
|
||||
private static volatile AppDatabase databaseInstance;
|
||||
|
||||
private NewPipeDatabase() {
|
||||
//no instance
|
||||
}
|
||||
|
||||
public static void init(Context context) {
|
||||
databaseInstance = Room
|
||||
.databaseBuilder(context, AppDatabase.class, DATABASE_NAME)
|
||||
private static AppDatabase getDatabase(Context context) {
|
||||
return Room
|
||||
.databaseBuilder(context.getApplicationContext(), AppDatabase.class, DATABASE_NAME)
|
||||
.addMigrations(MIGRATION_11_12)
|
||||
.fallbackToDestructiveMigration()
|
||||
.build();
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Deprecated
|
||||
public static AppDatabase getInstance() {
|
||||
if (databaseInstance == null) throw new RuntimeException("Database not initialized");
|
||||
public static AppDatabase getInstance(@NonNull Context context) {
|
||||
AppDatabase result = databaseInstance;
|
||||
if (result == null) {
|
||||
synchronized (NewPipeDatabase.class) {
|
||||
result = databaseInstance;
|
||||
if (result == null) {
|
||||
databaseInstance = (result = getDatabase(context));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return databaseInstance;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public static AppDatabase getInstance(Context context) {
|
||||
if (databaseInstance == null) init(context);
|
||||
return databaseInstance;
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package org.schabi.newpipe;
|
||||
|
||||
import android.app.IntentService;
|
||||
import android.content.Context;
|
||||
import android.content.DialogInterface;
|
||||
import android.content.Intent;
|
||||
import android.content.SharedPreferences;
|
||||
@@ -24,7 +25,6 @@ import android.widget.Toast;
|
||||
|
||||
import org.schabi.newpipe.extractor.Info;
|
||||
import org.schabi.newpipe.extractor.NewPipe;
|
||||
import org.schabi.newpipe.extractor.ServiceList;
|
||||
import org.schabi.newpipe.extractor.StreamingService;
|
||||
import org.schabi.newpipe.extractor.StreamingService.LinkType;
|
||||
import org.schabi.newpipe.extractor.channel.ChannelInfo;
|
||||
@@ -32,10 +32,10 @@ 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.player.helper.PlayerHelper;
|
||||
import org.schabi.newpipe.playlist.ChannelPlayQueue;
|
||||
import org.schabi.newpipe.playlist.PlayQueue;
|
||||
import org.schabi.newpipe.playlist.PlaylistPlayQueue;
|
||||
import org.schabi.newpipe.playlist.SinglePlayQueue;
|
||||
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.ExtractorHelper;
|
||||
import org.schabi.newpipe.util.NavigationHelper;
|
||||
@@ -43,9 +43,11 @@ import org.schabi.newpipe.util.PermissionHelper;
|
||||
import org.schabi.newpipe.util.ThemeHelper;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
|
||||
import icepick.Icepick;
|
||||
import icepick.State;
|
||||
@@ -57,6 +59,8 @@ import io.reactivex.disposables.Disposable;
|
||||
import io.reactivex.functions.Consumer;
|
||||
import io.reactivex.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;
|
||||
|
||||
/**
|
||||
@@ -64,13 +68,10 @@ import static org.schabi.newpipe.util.ThemeHelper.resolveResourceIdFromAttr;
|
||||
*/
|
||||
public class RouterActivity extends AppCompatActivity {
|
||||
|
||||
@State
|
||||
protected int currentServiceId = -1;
|
||||
@State protected int currentServiceId = -1;
|
||||
private StreamingService currentService;
|
||||
@State
|
||||
protected LinkType currentLinkType;
|
||||
@State
|
||||
protected int selectedRadioPosition = -1;
|
||||
@State protected LinkType currentLinkType;
|
||||
@State protected int selectedRadioPosition = -1;
|
||||
protected int selectedPreviously = -1;
|
||||
|
||||
protected String currentUrl;
|
||||
@@ -91,8 +92,7 @@ public class RouterActivity extends AppCompatActivity {
|
||||
}
|
||||
|
||||
setTheme(ThemeHelper.isLightThemeSelected(this)
|
||||
? R.style.RouterActivityThemeLight
|
||||
: R.style.RouterActivityThemeDark);
|
||||
? R.style.RouterActivityThemeLight : R.style.RouterActivityThemeDark);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -122,7 +122,7 @@ public class RouterActivity extends AppCompatActivity {
|
||||
currentService = NewPipe.getServiceByUrl(url);
|
||||
currentServiceId = currentService.getServiceId();
|
||||
currentLinkType = currentService.getLinkTypeByUrl(url);
|
||||
currentUrl = NavigationHelper.getCleanUrl(currentService, url, currentLinkType);
|
||||
currentUrl = url;
|
||||
} else {
|
||||
currentService = NewPipe.getService(currentServiceId);
|
||||
}
|
||||
@@ -159,57 +159,69 @@ public class RouterActivity extends AppCompatActivity {
|
||||
|
||||
protected void onSuccess() {
|
||||
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 String selectedChoiceKey = preferences.getString(getString(R.string.preferred_open_action_key), getString(R.string.preferred_open_action_default));
|
||||
|
||||
if ((isExtAudioEnabled || isExtVideoEnabled) && currentLinkType != LinkType.STREAM) {
|
||||
Toast.makeText(this, R.string.external_player_unsupported_link_type, Toast.LENGTH_LONG).show();
|
||||
finish();
|
||||
return;
|
||||
}
|
||||
|
||||
// TODO: Add some sort of "capabilities" field to services (audio only, video and audio, etc.)
|
||||
if (currentService == ServiceList.SoundCloud) {
|
||||
handleChoice(getString(R.string.background_player_key));
|
||||
return;
|
||||
}
|
||||
|
||||
final String playerChoiceKey = 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);
|
||||
final String backgroundPlayerKey = getString(R.string.background_player_key);
|
||||
final String popupPlayerKey = getString(R.string.popup_player_key);
|
||||
final String alwaysAskKey = getString(R.string.always_ask_open_action_key);
|
||||
|
||||
if (playerChoiceKey.equals(alwaysAskKey)) {
|
||||
showDialog();
|
||||
if (selectedChoiceKey.equals(alwaysAskKey)) {
|
||||
final List<AdapterChoiceItem> choices = getChoicesForService(currentService, currentLinkType);
|
||||
|
||||
if (choices.size() == 1) {
|
||||
handleChoice(choices.get(0).key);
|
||||
} else if (choices.size() == 0) {
|
||||
handleChoice(showInfoKey);
|
||||
} else {
|
||||
showDialog(choices);
|
||||
}
|
||||
} else if (selectedChoiceKey.equals(showInfoKey)) {
|
||||
handleChoice(showInfoKey);
|
||||
} else {
|
||||
handleChoice(playerChoiceKey);
|
||||
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();
|
||||
handleChoice(showInfoKey);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
final List<StreamingService.ServiceInfo.MediaCapability> capabilities = currentService.getServiceInfo().getMediaCapabilities();
|
||||
|
||||
boolean serviceSupportsChoice = false;
|
||||
if (isVideoPlayerSelected) {
|
||||
serviceSupportsChoice = capabilities.contains(VIDEO);
|
||||
} else if (selectedChoiceKey.equals(backgroundPlayerKey)) {
|
||||
serviceSupportsChoice = capabilities.contains(AUDIO);
|
||||
}
|
||||
|
||||
if (serviceSupportsChoice) {
|
||||
handleChoice(selectedChoiceKey);
|
||||
} else {
|
||||
handleChoice(showInfoKey);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void showDialog() {
|
||||
SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(this);
|
||||
final ContextThemeWrapper themeWrapper = new ContextThemeWrapper(this,
|
||||
ThemeHelper.isLightThemeSelected(this) ? R.style.LightTheme : R.style.DarkTheme);
|
||||
private void showDialog(final List<AdapterChoiceItem> choices) {
|
||||
final SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(this);
|
||||
final Context themeWrapperContext = getThemeWrapperContext();
|
||||
|
||||
LayoutInflater inflater = LayoutInflater.from(themeWrapper);
|
||||
final LayoutInflater inflater = LayoutInflater.from(themeWrapperContext);
|
||||
final LinearLayout rootLayout = (LinearLayout) inflater.inflate(R.layout.preferred_player_dialog_view, null, false);
|
||||
final RadioGroup radioGroup = rootLayout.findViewById(android.R.id.list);
|
||||
|
||||
final AdapterChoiceItem[] choices = {
|
||||
new AdapterChoiceItem(getString(R.string.show_info_key), getString(R.string.show_info),
|
||||
resolveResourceIdFromAttr(themeWrapper, R.attr.info)),
|
||||
new AdapterChoiceItem(getString(R.string.video_player_key), getString(R.string.video_player),
|
||||
resolveResourceIdFromAttr(themeWrapper, R.attr.play)),
|
||||
new AdapterChoiceItem(getString(R.string.background_player_key), getString(R.string.background_player),
|
||||
resolveResourceIdFromAttr(themeWrapper, R.attr.audio)),
|
||||
new AdapterChoiceItem(getString(R.string.popup_player_key), getString(R.string.popup_player),
|
||||
resolveResourceIdFromAttr(themeWrapper, R.attr.popup))
|
||||
};
|
||||
|
||||
final DialogInterface.OnClickListener dialogButtonsClickListener = (dialog, which) -> {
|
||||
final int indexOfChild = radioGroup.indexOfChild(
|
||||
radioGroup.findViewById(radioGroup.getCheckedRadioButtonId()));
|
||||
final AdapterChoiceItem choice = choices[indexOfChild];
|
||||
final AdapterChoiceItem choice = choices.get(indexOfChild);
|
||||
|
||||
handleChoice(choice.key);
|
||||
|
||||
@@ -218,8 +230,8 @@ public class RouterActivity extends AppCompatActivity {
|
||||
}
|
||||
};
|
||||
|
||||
final AlertDialog alertDialog = new AlertDialog.Builder(themeWrapper)
|
||||
.setTitle(R.string.preferred_player_share_menu_title)
|
||||
final AlertDialog alertDialog = new AlertDialog.Builder(themeWrapperContext)
|
||||
.setTitle(R.string.preferred_open_action_share_menu_title)
|
||||
.setView(radioGroup)
|
||||
.setCancelable(true)
|
||||
.setNegativeButton(R.string.just_once, dialogButtonsClickListener)
|
||||
@@ -227,6 +239,7 @@ public class RouterActivity extends AppCompatActivity {
|
||||
.setOnDismissListener((dialog) -> finish())
|
||||
.create();
|
||||
|
||||
//noinspection CodeBlock2Expr
|
||||
alertDialog.setOnShowListener(dialog -> {
|
||||
setDialogButtonsState(alertDialog, radioGroup.getCheckedRadioButtonId() != -1);
|
||||
});
|
||||
@@ -240,7 +253,7 @@ public class RouterActivity extends AppCompatActivity {
|
||||
selectedRadioPosition = indexOfChild;
|
||||
|
||||
if (selectedPreviously == selectedRadioPosition) {
|
||||
handleChoice(choices[selectedRadioPosition].key);
|
||||
handleChoice(choices.get(selectedRadioPosition).key);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -259,8 +272,8 @@ public class RouterActivity extends AppCompatActivity {
|
||||
if (selectedRadioPosition == -1) {
|
||||
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.length; i++) {
|
||||
AdapterChoiceItem c = choices[i];
|
||||
for (int i = 0; i < choices.size(); i++) {
|
||||
AdapterChoiceItem c = choices.get(i);
|
||||
if (lastSelectedPlayer.equals(c.key)) {
|
||||
selectedRadioPosition = i;
|
||||
break;
|
||||
@@ -269,7 +282,7 @@ public class RouterActivity extends AppCompatActivity {
|
||||
}
|
||||
}
|
||||
|
||||
selectedRadioPosition = Math.min(Math.max(-1, selectedRadioPosition), choices.length - 1);
|
||||
selectedRadioPosition = Math.min(Math.max(-1, selectedRadioPosition), choices.size() - 1);
|
||||
if (selectedRadioPosition != -1) {
|
||||
((RadioButton) radioGroup.getChildAt(selectedRadioPosition)).setChecked(true);
|
||||
}
|
||||
@@ -278,6 +291,39 @@ public class RouterActivity extends AppCompatActivity {
|
||||
alertDialog.show();
|
||||
}
|
||||
|
||||
private List<AdapterChoiceItem> getChoicesForService(StreamingService service, LinkType linkType) {
|
||||
final Context context = getThemeWrapperContext();
|
||||
|
||||
final List<AdapterChoiceItem> returnList = new ArrayList<>();
|
||||
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);
|
||||
|
||||
returnList.add(new AdapterChoiceItem(getString(R.string.show_info_key), getString(R.string.show_info),
|
||||
resolveResourceIdFromAttr(context, R.attr.info)));
|
||||
|
||||
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 (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)));
|
||||
}
|
||||
|
||||
return returnList;
|
||||
}
|
||||
|
||||
private Context getThemeWrapperContext() {
|
||||
return new ContextThemeWrapper(this,
|
||||
ThemeHelper.isLightThemeSelected(this) ? R.style.LightTheme : R.style.DarkTheme);
|
||||
}
|
||||
|
||||
private void setDialogButtonsState(AlertDialog dialog, boolean state) {
|
||||
final Button negativeButton = dialog.getButton(DialogInterface.BUTTON_NEGATIVE);
|
||||
final Button positiveButton = dialog.getButton(DialogInterface.BUTTON_POSITIVE);
|
||||
@@ -287,17 +333,15 @@ public class RouterActivity extends AppCompatActivity {
|
||||
positiveButton.setEnabled(state);
|
||||
}
|
||||
|
||||
private void handleChoice(final String playerChoiceKey) {
|
||||
if (Arrays.asList(getResources()
|
||||
.getStringArray(R.array.preferred_open_action_values_list))
|
||||
.contains(playerChoiceKey)) {
|
||||
private void handleChoice(final String selectedChoiceKey) {
|
||||
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),
|
||||
playerChoiceKey).apply();
|
||||
.putString(getString(R.string.preferred_open_action_last_selected_key), selectedChoiceKey)
|
||||
.apply();
|
||||
}
|
||||
|
||||
if (playerChoiceKey.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;
|
||||
@@ -305,7 +349,7 @@ public class RouterActivity extends AppCompatActivity {
|
||||
|
||||
// stop and bypass FetcherService if InfoScreen was selected since
|
||||
// StreamDetailFragment can fetch data itself
|
||||
if(playerChoiceKey.equals(getString(R.string.show_info_key))) {
|
||||
if (selectedChoiceKey.equals(getString(R.string.show_info_key))) {
|
||||
disposables.add(Observable
|
||||
.fromCallable(() -> NavigationHelper.getIntentByLink(this, currentUrl))
|
||||
.subscribeOn(Schedulers.io())
|
||||
@@ -322,11 +366,8 @@ public class RouterActivity extends AppCompatActivity {
|
||||
}
|
||||
|
||||
final Intent intent = new Intent(this, FetcherService.class);
|
||||
intent.putExtra(FetcherService.KEY_CHOICE,
|
||||
new Choice(currentService.getServiceId(),
|
||||
currentLinkType,
|
||||
currentUrl,
|
||||
playerChoiceKey));
|
||||
final Choice choice = new Choice(currentService.getServiceId(), currentLinkType, currentUrl, selectedChoiceKey);
|
||||
intent.putExtra(FetcherService.KEY_CHOICE, choice);
|
||||
startService(intent);
|
||||
|
||||
finish();
|
||||
@@ -334,8 +375,7 @@ public class RouterActivity extends AppCompatActivity {
|
||||
|
||||
private static class AdapterChoiceItem {
|
||||
final String description, key;
|
||||
@DrawableRes
|
||||
final int icon;
|
||||
@DrawableRes final int icon;
|
||||
|
||||
AdapterChoiceItem(String key, String description, int icon) {
|
||||
this.description = description;
|
||||
@@ -554,7 +594,7 @@ public class RouterActivity extends AppCompatActivity {
|
||||
* @param sharedText text to scan for URLs.
|
||||
* @return potential URLs
|
||||
*/
|
||||
protected String[] getUris(final String sharedText) {
|
||||
protected String[] getUris(final String sharedText) {
|
||||
final Collection<String> result = new HashSet<>();
|
||||
if (sharedText != null) {
|
||||
final String[] array = sharedText.split("\\p{Space}");
|
||||
|
||||
@@ -32,7 +32,6 @@ public class AboutActivity extends AppCompatActivity {
|
||||
new SoftwareComponent("Giga Get", "2014", "Peter Cai", "https://github.com/PaperAirplane-Dev-Team/GigaGet", StandardLicenses.GPL2),
|
||||
new SoftwareComponent("NewPipe Extractor", "2017", "Christian Schabesberger", "https://github.com/TeamNewPipe/NewPipeExtractor", StandardLicenses.GPL3),
|
||||
new SoftwareComponent("Jsoup", "2017", "Jonathan Hedley", "https://github.com/jhy/jsoup", StandardLicenses.MIT),
|
||||
new SoftwareComponent("Google Gson", "2008", "Google Inc", "https://github.com/google/gson", StandardLicenses.APACHE2),
|
||||
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),
|
||||
@@ -129,47 +128,31 @@ public class AboutActivity extends AppCompatActivity {
|
||||
public View onCreateView(LayoutInflater inflater, ViewGroup container,
|
||||
Bundle savedInstanceState) {
|
||||
View rootView = inflater.inflate(R.layout.fragment_about, container, false);
|
||||
Context context = this.getContext();
|
||||
|
||||
TextView version = rootView.findViewById(R.id.app_version);
|
||||
version.setText(BuildConfig.VERSION_NAME);
|
||||
|
||||
View githubLink = rootView.findViewById(R.id.github_link);
|
||||
githubLink.setOnClickListener(new OnGithubLinkClickListener());
|
||||
githubLink.setOnClickListener(nv -> openWebsite(context.getString(R.string.github_url), context));
|
||||
|
||||
View donationLink = rootView.findViewById(R.id.donation_link);
|
||||
donationLink.setOnClickListener(new OnDonationLinkClickListener());
|
||||
donationLink.setOnClickListener(v -> openWebsite(context.getString(R.string.donation_url), context));
|
||||
|
||||
View websiteLink = rootView.findViewById(R.id.website_link);
|
||||
websiteLink.setOnClickListener(new OnWebsiteLinkClickListener());
|
||||
websiteLink.setOnClickListener(nv -> openWebsite(context.getString(R.string.website_url), context));
|
||||
|
||||
View privacyPolicyLink = rootView.findViewById(R.id.privacy_policy_link);
|
||||
privacyPolicyLink.setOnClickListener(v -> openWebsite(context.getString(R.string.privacy_policy_url), context));
|
||||
|
||||
return rootView;
|
||||
}
|
||||
|
||||
private static class OnGithubLinkClickListener implements View.OnClickListener {
|
||||
@Override
|
||||
public void onClick(final View view) {
|
||||
final Context context = view.getContext();
|
||||
Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(context.getString(R.string.github_url)));
|
||||
context.startActivity(intent);
|
||||
}
|
||||
private void openWebsite(String url, Context context) {
|
||||
Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(url));
|
||||
context.startActivity(intent);
|
||||
}
|
||||
|
||||
private static class OnDonationLinkClickListener implements View.OnClickListener {
|
||||
@Override
|
||||
public void onClick(final View view) {
|
||||
final Context context = view.getContext();
|
||||
Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(context.getString(R.string.donation_url)));
|
||||
context.startActivity(intent);
|
||||
}
|
||||
}
|
||||
|
||||
private static class OnWebsiteLinkClickListener implements View.OnClickListener {
|
||||
@Override
|
||||
public void onClick(final View view) {
|
||||
final Context context = view.getContext();
|
||||
Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(context.getString(R.string.website_url)));
|
||||
context.startActivity(intent);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package org.schabi.newpipe.about;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.net.Uri;
|
||||
@@ -39,7 +40,7 @@ public class LicenseFragment extends Fragment {
|
||||
* @param license the license to show
|
||||
*/
|
||||
public static void showLicense(Context context, License license) {
|
||||
new LicenseFragmentHelper().execute(context, license);
|
||||
new LicenseFragmentHelper((Activity) context).execute(license);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
package org.schabi.newpipe.about;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.Context;
|
||||
import android.content.DialogInterface;
|
||||
import android.os.AsyncTask;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.support.v7.app.AlertDialog;
|
||||
import android.webkit.WebView;
|
||||
import org.schabi.newpipe.R;
|
||||
@@ -10,26 +13,46 @@ import org.schabi.newpipe.util.ThemeHelper;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.InputStreamReader;
|
||||
import java.lang.ref.WeakReference;
|
||||
|
||||
public class LicenseFragmentHelper extends AsyncTask<Object, Void, Integer> {
|
||||
|
||||
private Context context;
|
||||
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) {
|
||||
context = (Context) objects[0];
|
||||
license = (License) objects[1];
|
||||
license = (License) objects[0];
|
||||
return 1;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPostExecute(Integer result){
|
||||
String webViewData = getFormattedLicense(context, license);
|
||||
AlertDialog.Builder alert = new AlertDialog.Builder(context);
|
||||
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(context);
|
||||
WebView wv = new WebView(activity);
|
||||
wv.loadData(webViewData, "text/html; charset=UTF-8", null);
|
||||
|
||||
alert.setView(wv);
|
||||
|
||||
@@ -9,7 +9,7 @@ import android.arch.persistence.room.PrimaryKey;
|
||||
import org.schabi.newpipe.extractor.stream.StreamInfo;
|
||||
import org.schabi.newpipe.extractor.stream.StreamInfoItem;
|
||||
import org.schabi.newpipe.extractor.stream.StreamType;
|
||||
import org.schabi.newpipe.playlist.PlayQueueItem;
|
||||
import org.schabi.newpipe.player.playqueue.PlayQueueItem;
|
||||
import org.schabi.newpipe.util.Constants;
|
||||
|
||||
import java.io.Serializable;
|
||||
@@ -71,14 +71,14 @@ public class StreamEntity implements Serializable {
|
||||
|
||||
@Ignore
|
||||
public StreamEntity(final StreamInfoItem item) {
|
||||
this(item.service_id, item.name, item.url, item.stream_type, item.thumbnail_url,
|
||||
item.uploader_name, item.duration);
|
||||
this(item.getServiceId(), item.getName(), item.getUrl(), item.getStreamType(), item.getThumbnailUrl(),
|
||||
item.getUploaderName(), item.getDuration());
|
||||
}
|
||||
|
||||
@Ignore
|
||||
public StreamEntity(final StreamInfo info) {
|
||||
this(info.service_id, info.name, info.url, info.stream_type, info.thumbnail_url,
|
||||
info.uploader_name, info.duration);
|
||||
this(info.getServiceId(), info.getName(), info.getUrl(), info.getStreamType(), info.getThumbnailUrl(),
|
||||
info.getUploaderName(), info.getDuration());
|
||||
}
|
||||
|
||||
@Ignore
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
package org.schabi.newpipe.database.subscription;
|
||||
|
||||
import android.arch.persistence.room.Dao;
|
||||
import android.arch.persistence.room.Insert;
|
||||
import android.arch.persistence.room.OnConflictStrategy;
|
||||
import android.arch.persistence.room.Query;
|
||||
import android.arch.persistence.room.Transaction;
|
||||
|
||||
import org.schabi.newpipe.database.BasicDAO;
|
||||
|
||||
@@ -11,24 +14,56 @@ import io.reactivex.Flowable;
|
||||
|
||||
import static org.schabi.newpipe.database.subscription.SubscriptionEntity.SUBSCRIPTION_SERVICE_ID;
|
||||
import static org.schabi.newpipe.database.subscription.SubscriptionEntity.SUBSCRIPTION_TABLE;
|
||||
import static org.schabi.newpipe.database.subscription.SubscriptionEntity.SUBSCRIPTION_UID;
|
||||
import static org.schabi.newpipe.database.subscription.SubscriptionEntity.SUBSCRIPTION_URL;
|
||||
|
||||
@Dao
|
||||
public interface SubscriptionDAO extends BasicDAO<SubscriptionEntity> {
|
||||
public abstract class SubscriptionDAO implements BasicDAO<SubscriptionEntity> {
|
||||
@Override
|
||||
@Query("SELECT * FROM " + SUBSCRIPTION_TABLE)
|
||||
Flowable<List<SubscriptionEntity>> getAll();
|
||||
public abstract Flowable<List<SubscriptionEntity>> getAll();
|
||||
|
||||
@Override
|
||||
@Query("DELETE FROM " + SUBSCRIPTION_TABLE)
|
||||
int deleteAll();
|
||||
public abstract int deleteAll();
|
||||
|
||||
@Override
|
||||
@Query("SELECT * FROM " + SUBSCRIPTION_TABLE + " WHERE " + SUBSCRIPTION_SERVICE_ID + " = :serviceId")
|
||||
Flowable<List<SubscriptionEntity>> listByService(int serviceId);
|
||||
public abstract Flowable<List<SubscriptionEntity>> listByService(int serviceId);
|
||||
|
||||
@Query("SELECT * FROM " + SUBSCRIPTION_TABLE + " WHERE " +
|
||||
SUBSCRIPTION_URL + " LIKE :url AND " +
|
||||
SUBSCRIPTION_SERVICE_ID + " = :serviceId")
|
||||
Flowable<List<SubscriptionEntity>> getSubscription(int serviceId, String url);
|
||||
public abstract Flowable<List<SubscriptionEntity>> getSubscription(int serviceId, String url);
|
||||
|
||||
@Query("SELECT " + SUBSCRIPTION_UID + " FROM " + SUBSCRIPTION_TABLE + " WHERE " +
|
||||
SUBSCRIPTION_URL + " LIKE :url AND " +
|
||||
SUBSCRIPTION_SERVICE_ID + " = :serviceId")
|
||||
abstract Long getSubscriptionIdInternal(int serviceId, String url);
|
||||
|
||||
@Insert(onConflict = OnConflictStrategy.IGNORE)
|
||||
abstract Long insertInternal(final SubscriptionEntity entities);
|
||||
|
||||
@Transaction
|
||||
public List<SubscriptionEntity> upsertAll(List<SubscriptionEntity> entities) {
|
||||
for (SubscriptionEntity entity : entities) {
|
||||
Long uid = insertInternal(entity);
|
||||
|
||||
if (uid != -1) {
|
||||
entity.setUid(uid);
|
||||
continue;
|
||||
}
|
||||
|
||||
uid = getSubscriptionIdInternal(entity.getServiceId(), entity.getUrl());
|
||||
entity.setUid(uid);
|
||||
|
||||
if (uid == -1) {
|
||||
throw new IllegalStateException("Invalid subscription id (-1)");
|
||||
}
|
||||
|
||||
update(entity);
|
||||
}
|
||||
|
||||
return entities;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,7 +5,9 @@ import android.arch.persistence.room.Entity;
|
||||
import android.arch.persistence.room.Ignore;
|
||||
import android.arch.persistence.room.Index;
|
||||
import android.arch.persistence.room.PrimaryKey;
|
||||
import android.support.annotation.NonNull;
|
||||
|
||||
import org.schabi.newpipe.extractor.channel.ChannelInfo;
|
||||
import org.schabi.newpipe.extractor.channel.ChannelInfoItem;
|
||||
import org.schabi.newpipe.util.Constants;
|
||||
|
||||
@@ -17,6 +19,7 @@ import static org.schabi.newpipe.database.subscription.SubscriptionEntity.SUBSCR
|
||||
indices = {@Index(value = {SUBSCRIPTION_SERVICE_ID, SUBSCRIPTION_URL}, unique = true)})
|
||||
public class SubscriptionEntity {
|
||||
|
||||
final static String SUBSCRIPTION_UID = "uid";
|
||||
final static String SUBSCRIPTION_TABLE = "subscriptions";
|
||||
final static String SUBSCRIPTION_SERVICE_ID = "service_id";
|
||||
final static String SUBSCRIPTION_URL = "url";
|
||||
@@ -116,9 +119,18 @@ public class SubscriptionEntity {
|
||||
@Ignore
|
||||
public ChannelInfoItem toChannelInfoItem() {
|
||||
ChannelInfoItem item = new ChannelInfoItem(getServiceId(), getUrl(), getName());
|
||||
item.thumbnail_url = getAvatarUrl();
|
||||
item.subscriber_count = getSubscriberCount();
|
||||
item.description = getDescription();
|
||||
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());
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,56 +1,61 @@
|
||||
package org.schabi.newpipe.download;
|
||||
|
||||
import android.content.Context;
|
||||
import android.os.Bundle;
|
||||
import android.support.annotation.IdRes;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.support.v4.app.DialogFragment;
|
||||
import android.support.v7.widget.Toolbar;
|
||||
import android.util.Log;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.AdapterView;
|
||||
import android.widget.ArrayAdapter;
|
||||
import android.widget.EditText;
|
||||
import android.widget.RadioButton;
|
||||
import android.widget.RadioGroup;
|
||||
import android.widget.SeekBar;
|
||||
import android.widget.Spinner;
|
||||
import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
|
||||
import org.schabi.newpipe.MainActivity;
|
||||
import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.extractor.MediaFormat;
|
||||
import org.schabi.newpipe.extractor.stream.AudioStream;
|
||||
import org.schabi.newpipe.extractor.stream.Stream;
|
||||
import org.schabi.newpipe.extractor.stream.StreamInfo;
|
||||
import org.schabi.newpipe.extractor.stream.VideoStream;
|
||||
import org.schabi.newpipe.fragments.detail.SpinnerToolbarAdapter;
|
||||
import org.schabi.newpipe.settings.NewPipeSettings;
|
||||
import org.schabi.newpipe.util.FilenameUtils;
|
||||
import org.schabi.newpipe.util.ListHelper;
|
||||
import org.schabi.newpipe.util.PermissionHelper;
|
||||
import org.schabi.newpipe.util.StreamItemAdapter;
|
||||
import org.schabi.newpipe.util.StreamItemAdapter.StreamSizeWrapper;
|
||||
import org.schabi.newpipe.util.ThemeHelper;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import icepick.Icepick;
|
||||
import icepick.State;
|
||||
import io.reactivex.disposables.CompositeDisposable;
|
||||
import us.shandian.giga.service.DownloadManagerService;
|
||||
|
||||
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 String INFO_KEY = "info_key";
|
||||
private static final String SORTED_VIDEOS_LIST_KEY = "sorted_videos_list_key";
|
||||
private static final String SELECTED_VIDEO_KEY = "selected_video_key";
|
||||
private static final String SELECTED_AUDIO_KEY = "selected_audio_key";
|
||||
@State protected StreamInfo currentInfo;
|
||||
@State protected StreamSizeWrapper<AudioStream> wrappedAudioStreams = StreamSizeWrapper.empty();
|
||||
@State protected StreamSizeWrapper<VideoStream> wrappedVideoStreams = StreamSizeWrapper.empty();
|
||||
@State protected int selectedVideoIndex = 0;
|
||||
@State protected int selectedAudioIndex = 0;
|
||||
|
||||
private StreamInfo currentInfo;
|
||||
private ArrayList<VideoStream> sortedStreamVideosList;
|
||||
private int selectedVideoIndex;
|
||||
private int selectedAudioIndex;
|
||||
private StreamItemAdapter<AudioStream> audioStreamsAdapter;
|
||||
private StreamItemAdapter<VideoStream> videoStreamsAdapter;
|
||||
|
||||
private CompositeDisposable disposables = new CompositeDisposable();
|
||||
|
||||
private EditText nameEditText;
|
||||
private Spinner streamsSpinner;
|
||||
@@ -58,17 +63,50 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck
|
||||
private TextView threadsCountTextView;
|
||||
private SeekBar threadsSeekBar;
|
||||
|
||||
public static DownloadDialog newInstance(StreamInfo info, ArrayList<VideoStream> sortedStreamVideosList, int selectedVideoIndex) {
|
||||
public static DownloadDialog newInstance(StreamInfo info) {
|
||||
DownloadDialog dialog = new DownloadDialog();
|
||||
dialog.setInfo(info, sortedStreamVideosList, selectedVideoIndex);
|
||||
dialog.setStyle(DialogFragment.STYLE_NO_TITLE, 0);
|
||||
dialog.setInfo(info);
|
||||
return dialog;
|
||||
}
|
||||
|
||||
private void setInfo(StreamInfo info, ArrayList<VideoStream> sortedStreamVideosList, int selectedVideoIndex) {
|
||||
public static DownloadDialog newInstance(Context context, 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);
|
||||
instance.setVideoStreams(streamsList);
|
||||
instance.setSelectedVideoStream(selectedStreamIndex);
|
||||
instance.setAudioStreams(info.getAudioStreams());
|
||||
return instance;
|
||||
}
|
||||
|
||||
private void setInfo(StreamInfo info) {
|
||||
this.currentInfo = info;
|
||||
}
|
||||
|
||||
public void setAudioStreams(List<AudioStream> audioStreams) {
|
||||
setAudioStreams(new StreamSizeWrapper<>(audioStreams));
|
||||
}
|
||||
|
||||
public void setAudioStreams(StreamSizeWrapper<AudioStream> wrappedAudioStreams) {
|
||||
this.wrappedAudioStreams = wrappedAudioStreams;
|
||||
}
|
||||
|
||||
public void setVideoStreams(List<VideoStream> videoStreams) {
|
||||
setVideoStreams(new StreamSizeWrapper<>(videoStreams));
|
||||
}
|
||||
|
||||
public void setVideoStreams(StreamSizeWrapper<VideoStream> wrappedVideoStreams) {
|
||||
this.wrappedVideoStreams = wrappedVideoStreams;
|
||||
}
|
||||
|
||||
public void setSelectedVideoStream(int selectedVideoIndex) {
|
||||
this.selectedVideoIndex = selectedVideoIndex;
|
||||
this.sortedStreamVideosList = sortedStreamVideosList;
|
||||
}
|
||||
|
||||
public void setSelectedAudioStream(int selectedAudioIndex) {
|
||||
this.selectedAudioIndex = selectedAudioIndex;
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
@@ -79,33 +117,26 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck
|
||||
public void onCreate(@Nullable Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
if (DEBUG) Log.d(TAG, "onCreate() called with: savedInstanceState = [" + savedInstanceState + "]");
|
||||
if (!PermissionHelper.checkStoragePermissions(getActivity())) {
|
||||
if (!PermissionHelper.checkStoragePermissions(getActivity(), PermissionHelper.DOWNLOAD_DIALOG_REQUEST_CODE)) {
|
||||
getDialog().dismiss();
|
||||
return;
|
||||
}
|
||||
|
||||
if (savedInstanceState != null) {
|
||||
Serializable serial = savedInstanceState.getSerializable(INFO_KEY);
|
||||
if (serial instanceof StreamInfo) currentInfo = (StreamInfo) serial;
|
||||
setStyle(STYLE_NO_TITLE, ThemeHelper.getDialogTheme(getContext()));
|
||||
Icepick.restoreInstanceState(this, savedInstanceState);
|
||||
|
||||
serial = savedInstanceState.getSerializable(SORTED_VIDEOS_LIST_KEY);
|
||||
if (serial instanceof ArrayList) { //noinspection unchecked
|
||||
sortedStreamVideosList = (ArrayList<VideoStream>) serial;
|
||||
}
|
||||
|
||||
selectedVideoIndex = savedInstanceState.getInt(SELECTED_VIDEO_KEY, 0);
|
||||
selectedAudioIndex = savedInstanceState.getInt(SELECTED_AUDIO_KEY, 0);
|
||||
}
|
||||
this.videoStreamsAdapter = new StreamItemAdapter<>(getContext(), wrappedVideoStreams, true);
|
||||
this.audioStreamsAdapter = new StreamItemAdapter<>(getContext(), wrappedAudioStreams);
|
||||
}
|
||||
|
||||
@Override
|
||||
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
|
||||
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
|
||||
if (DEBUG) Log.d(TAG, "onCreateView() called with: inflater = [" + inflater + "], container = [" + container + "], savedInstanceState = [" + savedInstanceState + "]");
|
||||
return inflater.inflate(R.layout.dialog_url, container);
|
||||
return inflater.inflate(R.layout.download_dialog, container);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onViewCreated(View view, @Nullable Bundle savedInstanceState) {
|
||||
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
|
||||
super.onViewCreated(view, savedInstanceState);
|
||||
nameEditText = view.findViewById(R.id.file_name);
|
||||
nameEditText.setText(FilenameUtils.createFilename(getContext(), currentInfo.getName()));
|
||||
@@ -116,12 +147,12 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck
|
||||
|
||||
threadsCountTextView = view.findViewById(R.id.threads_count);
|
||||
threadsSeekBar = view.findViewById(R.id.threads);
|
||||
|
||||
radioVideoAudioGroup = view.findViewById(R.id.video_audio_group);
|
||||
radioVideoAudioGroup.setOnCheckedChangeListener(this);
|
||||
|
||||
initToolbar(view.<Toolbar>findViewById(R.id.toolbar));
|
||||
checkDownloadOptions(view);
|
||||
setupVideoSpinner(sortedStreamVideosList, streamsSpinner);
|
||||
initToolbar(view.findViewById(R.id.toolbar));
|
||||
setupDownloadOptions();
|
||||
|
||||
int def = 3;
|
||||
threadsCountTextView.setText(String.valueOf(def));
|
||||
@@ -141,15 +172,35 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck
|
||||
public void onStopTrackingTouch(SeekBar p1) {
|
||||
}
|
||||
});
|
||||
|
||||
fetchStreamsSize();
|
||||
}
|
||||
|
||||
private void fetchStreamsSize() {
|
||||
disposables.clear();
|
||||
|
||||
disposables.add(StreamSizeWrapper.fetchSizeForWrapper(wrappedVideoStreams).subscribe(result -> {
|
||||
if (radioVideoAudioGroup.getCheckedRadioButtonId() == R.id.video_button) {
|
||||
setupVideoSpinner();
|
||||
}
|
||||
}));
|
||||
disposables.add(StreamSizeWrapper.fetchSizeForWrapper(wrappedAudioStreams).subscribe(result -> {
|
||||
if (radioVideoAudioGroup.getCheckedRadioButtonId() == R.id.audio_button) {
|
||||
setupAudioSpinner();
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroy() {
|
||||
super.onDestroy();
|
||||
disposables.clear();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSaveInstanceState(Bundle outState) {
|
||||
super.onSaveInstanceState(outState);
|
||||
outState.putSerializable(INFO_KEY, currentInfo);
|
||||
outState.putSerializable(SORTED_VIDEOS_LIST_KEY, sortedStreamVideosList);
|
||||
outState.putInt(SELECTED_VIDEO_KEY, selectedVideoIndex);
|
||||
outState.putInt(SELECTED_AUDIO_KEY, selectedAudioIndex);
|
||||
Icepick.saveInstanceState(this, outState);
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
@@ -161,39 +212,31 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck
|
||||
toolbar.setTitle(R.string.download_dialog_title);
|
||||
toolbar.setNavigationIcon(ThemeHelper.isLightThemeSelected(getActivity()) ? R.drawable.ic_arrow_back_black_24dp : R.drawable.ic_arrow_back_white_24dp);
|
||||
toolbar.inflateMenu(R.menu.dialog_url);
|
||||
toolbar.setNavigationOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
getDialog().dismiss();
|
||||
}
|
||||
});
|
||||
toolbar.setNavigationOnClickListener(v -> getDialog().dismiss());
|
||||
|
||||
toolbar.setOnMenuItemClickListener(new Toolbar.OnMenuItemClickListener() {
|
||||
@Override
|
||||
public boolean onMenuItemClick(MenuItem item) {
|
||||
if (item.getItemId() == R.id.okay) {
|
||||
downloadSelected();
|
||||
return true;
|
||||
} else return false;
|
||||
toolbar.setOnMenuItemClickListener(item -> {
|
||||
if (item.getItemId() == R.id.okay) {
|
||||
downloadSelected();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
}
|
||||
|
||||
public void setupAudioSpinner(final List<AudioStream> audioStreams, Spinner spinner) {
|
||||
String[] items = new String[audioStreams.size()];
|
||||
for (int i = 0; i < audioStreams.size(); i++) {
|
||||
AudioStream audioStream = audioStreams.get(i);
|
||||
items[i] = audioStream.getFormat().getName() + " " + audioStream.getAverageBitrate() + "kbps";
|
||||
}
|
||||
private void setupAudioSpinner() {
|
||||
if (getContext() == null) return;
|
||||
|
||||
ArrayAdapter<String> itemAdapter = new ArrayAdapter<>(getContext(), android.R.layout.simple_spinner_dropdown_item, items);
|
||||
spinner.setAdapter(itemAdapter);
|
||||
spinner.setSelection(selectedAudioIndex);
|
||||
streamsSpinner.setAdapter(audioStreamsAdapter);
|
||||
streamsSpinner.setSelection(selectedAudioIndex);
|
||||
setRadioButtonsState(true);
|
||||
}
|
||||
|
||||
public void setupVideoSpinner(final List<VideoStream> videoStreams, Spinner spinner) {
|
||||
spinner.setAdapter(new SpinnerToolbarAdapter(getContext(), videoStreams, true));
|
||||
spinner.setSelection(selectedVideoIndex);
|
||||
private void setupVideoSpinner() {
|
||||
if (getContext() == null) return;
|
||||
|
||||
streamsSpinner.setAdapter(videoStreamsAdapter);
|
||||
streamsSpinner.setSelection(selectedVideoIndex);
|
||||
setRadioButtonsState(true);
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
@@ -205,10 +248,10 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck
|
||||
if (DEBUG) Log.d(TAG, "onCheckedChanged() called with: group = [" + group + "], checkedId = [" + checkedId + "]");
|
||||
switch (checkedId) {
|
||||
case R.id.audio_button:
|
||||
setupAudioSpinner(currentInfo.audio_streams, streamsSpinner);
|
||||
setupAudioSpinner();
|
||||
break;
|
||||
case R.id.video_button:
|
||||
setupVideoSpinner(sortedStreamVideosList, streamsSpinner);
|
||||
setupVideoSpinner();
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -238,37 +281,53 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck
|
||||
// Utils
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
protected void checkDownloadOptions(View view) {
|
||||
RadioButton audioButton = view.findViewById(R.id.audio_button);
|
||||
RadioButton videoButton = view.findViewById(R.id.video_button);
|
||||
protected void setupDownloadOptions() {
|
||||
setRadioButtonsState(false);
|
||||
|
||||
if (currentInfo.getAudioStreams() == null || currentInfo.getAudioStreams().size() == 0) {
|
||||
audioButton.setVisibility(View.GONE);
|
||||
final RadioButton audioButton = radioVideoAudioGroup.findViewById(R.id.audio_button);
|
||||
final RadioButton videoButton = radioVideoAudioGroup.findViewById(R.id.video_button);
|
||||
final boolean isVideoStreamsAvailable = videoStreamsAdapter.getCount() > 0;
|
||||
final boolean isAudioStreamsAvailable = audioStreamsAdapter.getCount() > 0;
|
||||
|
||||
audioButton.setVisibility(isAudioStreamsAvailable ? View.VISIBLE : View.GONE);
|
||||
videoButton.setVisibility(isVideoStreamsAvailable ? View.VISIBLE : View.GONE);
|
||||
|
||||
if (isVideoStreamsAvailable) {
|
||||
videoButton.setChecked(true);
|
||||
} else if (sortedStreamVideosList == null || sortedStreamVideosList.size() == 0) {
|
||||
videoButton.setVisibility(View.GONE);
|
||||
setupVideoSpinner();
|
||||
} else if (isAudioStreamsAvailable) {
|
||||
audioButton.setChecked(true);
|
||||
setupAudioSpinner();
|
||||
} else {
|
||||
Toast.makeText(getContext(), R.string.no_streams_available_download, Toast.LENGTH_SHORT).show();
|
||||
getDialog().dismiss();
|
||||
}
|
||||
}
|
||||
|
||||
private void setRadioButtonsState(boolean enabled) {
|
||||
radioVideoAudioGroup.findViewById(R.id.audio_button).setEnabled(enabled);
|
||||
radioVideoAudioGroup.findViewById(R.id.video_button).setEnabled(enabled);
|
||||
}
|
||||
|
||||
private void downloadSelected() {
|
||||
String url, location;
|
||||
Stream stream;
|
||||
String location;
|
||||
|
||||
String fileName = nameEditText.getText().toString().trim();
|
||||
if (fileName.isEmpty()) fileName = FilenameUtils.createFilename(getContext(), currentInfo.getName());
|
||||
|
||||
boolean isAudio = radioVideoAudioGroup.getCheckedRadioButtonId() == R.id.audio_button;
|
||||
if (isAudio) {
|
||||
url = currentInfo.getAudioStreams().get(selectedAudioIndex).getUrl();
|
||||
stream = audioStreamsAdapter.getItem(selectedAudioIndex);
|
||||
location = NewPipeSettings.getAudioDownloadPath(getContext());
|
||||
fileName += "." + currentInfo.getAudioStreams().get(selectedAudioIndex).getFormat().getSuffix();
|
||||
} else {
|
||||
url = sortedStreamVideosList.get(selectedVideoIndex).getUrl();
|
||||
stream = videoStreamsAdapter.getItem(selectedVideoIndex);
|
||||
location = NewPipeSettings.getVideoDownloadPath(getContext());
|
||||
fileName += "." + sortedStreamVideosList.get(selectedVideoIndex).getFormat().getSuffix();
|
||||
}
|
||||
|
||||
String url = stream.getUrl();
|
||||
fileName += "." + stream.getFormat().getSuffix();
|
||||
|
||||
DownloadManagerService.startMission(getContext(), url, location, fileName, isAudio, threadsSeekBar.getProgress() + 1);
|
||||
getDialog().dismiss();
|
||||
}
|
||||
|
||||
@@ -239,20 +239,14 @@ public abstract class BaseStateFragment<I> extends BaseFragment implements ViewC
|
||||
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));
|
||||
ErrorActivity.reportError(getContext(), exception, MainActivity.class, rootView,
|
||||
ErrorActivity.ErrorInfo.make(userAction, serviceName, request, errorId));
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Utils
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
public void setTitle(String title) {
|
||||
if (DEBUG) Log.d(TAG, "setTitle() called with: title = [" + title + "]");
|
||||
if (activity != null && activity.getSupportActionBar() != null) {
|
||||
activity.getSupportActionBar().setTitle(title);
|
||||
}
|
||||
}
|
||||
|
||||
protected void openUrlInBrowser(String url) {
|
||||
Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(url));
|
||||
startActivity(Intent.createChooser(intent, activity.getString(R.string.share_dialog_title)));
|
||||
|
||||
@@ -27,10 +27,10 @@ import org.schabi.newpipe.extractor.ServiceList;
|
||||
import org.schabi.newpipe.extractor.StreamingService;
|
||||
import org.schabi.newpipe.extractor.kiosk.KioskList;
|
||||
import org.schabi.newpipe.fragments.list.channel.ChannelFragment;
|
||||
import org.schabi.newpipe.fragments.list.feed.FeedFragment;
|
||||
import org.schabi.newpipe.local.feed.FeedFragment;
|
||||
import org.schabi.newpipe.fragments.list.kiosk.KioskFragment;
|
||||
import org.schabi.newpipe.fragments.local.bookmark.BookmarkFragment;
|
||||
import org.schabi.newpipe.fragments.subscription.SubscriptionFragment;
|
||||
import org.schabi.newpipe.local.bookmark.BookmarkFragment;
|
||||
import org.schabi.newpipe.local.subscription.SubscriptionFragment;
|
||||
import org.schabi.newpipe.report.ErrorActivity;
|
||||
import org.schabi.newpipe.report.UserAction;
|
||||
import org.schabi.newpipe.util.KioskTranslator;
|
||||
@@ -106,7 +106,7 @@ public class MainFragment extends BaseFragment implements TabLayout.OnTabSelecte
|
||||
super.onCreateOptionsMenu(menu, inflater);
|
||||
if (DEBUG) Log.d(TAG, "onCreateOptionsMenu() called with: menu = [" + menu + "], inflater = [" + inflater + "]");
|
||||
inflater.inflate(R.menu.main_fragment_menu, menu);
|
||||
SubMenu kioskMenu = menu.addSubMenu(getString(R.string.kiosk));
|
||||
SubMenu kioskMenu = menu.addSubMenu(Menu.NONE, Menu.NONE, 200, getString(R.string.kiosk));
|
||||
try {
|
||||
createKioskMenu(kioskMenu, inflater);
|
||||
} catch (Exception e) {
|
||||
|
||||
@@ -1,74 +0,0 @@
|
||||
package org.schabi.newpipe.fragments.detail;
|
||||
|
||||
import android.content.Context;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.BaseAdapter;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.Spinner;
|
||||
import android.widget.TextView;
|
||||
|
||||
import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.extractor.MediaFormat;
|
||||
import org.schabi.newpipe.extractor.stream.VideoStream;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class SpinnerToolbarAdapter extends BaseAdapter {
|
||||
private final List<VideoStream> videoStreams;
|
||||
private final boolean showIconNoAudio;
|
||||
|
||||
private final Context context;
|
||||
|
||||
public SpinnerToolbarAdapter(Context context, List<VideoStream> videoStreams, boolean showIconNoAudio) {
|
||||
this.context = context;
|
||||
this.videoStreams = videoStreams;
|
||||
this.showIconNoAudio = showIconNoAudio;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getCount() {
|
||||
return videoStreams.size();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object getItem(int position) {
|
||||
return videoStreams.get(position);
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getItemId(int position) {
|
||||
return position;
|
||||
}
|
||||
|
||||
@Override
|
||||
public View getDropDownView(int position, View convertView, ViewGroup parent) {
|
||||
return getCustomView(position, convertView, parent, true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public View getView(int position, View convertView, ViewGroup parent) {
|
||||
return getCustomView(((Spinner) parent).getSelectedItemPosition(), convertView, parent, false);
|
||||
}
|
||||
|
||||
private View getCustomView(int position, View convertView, ViewGroup parent, boolean isDropdownItem) {
|
||||
if (convertView == null) {
|
||||
convertView = LayoutInflater.from(context).inflate(R.layout.resolutions_spinner_item, parent, false);
|
||||
}
|
||||
|
||||
ImageView woSoundIcon = convertView.findViewById(R.id.wo_sound_icon);
|
||||
TextView text = convertView.findViewById(android.R.id.text1);
|
||||
VideoStream item = (VideoStream) getItem(position);
|
||||
text.setText(item.getFormat().getName() + " " + item.getResolution());
|
||||
|
||||
int visibility = !showIconNoAudio ? View.GONE
|
||||
: item.isVideoOnly ? View.VISIBLE
|
||||
: isDropdownItem ? View.INVISIBLE
|
||||
: View.GONE;
|
||||
woSoundIcon.setVisibility(visibility);
|
||||
|
||||
return convertView;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -12,6 +12,7 @@ import android.preference.PreferenceManager;
|
||||
import android.support.annotation.DrawableRes;
|
||||
import android.support.annotation.FloatRange;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.support.v4.content.ContextCompat;
|
||||
import android.support.v4.view.animation.FastOutSlowInInterpolator;
|
||||
import android.support.v7.app.ActionBar;
|
||||
@@ -43,6 +44,7 @@ import android.widget.Toast;
|
||||
|
||||
import com.nirhart.parallaxscroll.views.ParallaxScrollView;
|
||||
import com.nostra13.universalimageloader.core.assist.FailReason;
|
||||
import com.nostra13.universalimageloader.core.listener.ImageLoadingListener;
|
||||
import com.nostra13.universalimageloader.core.listener.SimpleImageLoadingListener;
|
||||
|
||||
import org.schabi.newpipe.R;
|
||||
@@ -56,22 +58,25 @@ import org.schabi.newpipe.extractor.services.youtube.YoutubeStreamExtractor;
|
||||
import org.schabi.newpipe.extractor.stream.AudioStream;
|
||||
import org.schabi.newpipe.extractor.stream.StreamInfo;
|
||||
import org.schabi.newpipe.extractor.stream.StreamInfoItem;
|
||||
import org.schabi.newpipe.extractor.stream.StreamType;
|
||||
import org.schabi.newpipe.extractor.stream.VideoStream;
|
||||
import org.schabi.newpipe.fragments.BackPressable;
|
||||
import org.schabi.newpipe.fragments.BaseStateFragment;
|
||||
import org.schabi.newpipe.fragments.local.dialog.PlaylistAppendDialog;
|
||||
import org.schabi.newpipe.util.StreamItemAdapter;
|
||||
import org.schabi.newpipe.util.StreamItemAdapter.StreamSizeWrapper;
|
||||
import org.schabi.newpipe.local.dialog.PlaylistAppendDialog;
|
||||
import org.schabi.newpipe.info_list.InfoItemBuilder;
|
||||
import org.schabi.newpipe.info_list.InfoItemDialog;
|
||||
import org.schabi.newpipe.player.MainVideoPlayer;
|
||||
import org.schabi.newpipe.player.PopupVideoPlayer;
|
||||
import org.schabi.newpipe.player.helper.PlayerHelper;
|
||||
import org.schabi.newpipe.player.old.PlayVideoActivity;
|
||||
import org.schabi.newpipe.playlist.PlayQueue;
|
||||
import org.schabi.newpipe.playlist.SinglePlayQueue;
|
||||
import org.schabi.newpipe.report.ErrorActivity;
|
||||
import org.schabi.newpipe.player.playqueue.PlayQueue;
|
||||
import org.schabi.newpipe.player.playqueue.SinglePlayQueue;
|
||||
import org.schabi.newpipe.report.UserAction;
|
||||
import org.schabi.newpipe.util.Constants;
|
||||
import org.schabi.newpipe.util.ExtractorHelper;
|
||||
import org.schabi.newpipe.util.ImageDisplayConstants;
|
||||
import org.schabi.newpipe.util.InfoCache;
|
||||
import org.schabi.newpipe.util.ListHelper;
|
||||
import org.schabi.newpipe.util.Localization;
|
||||
@@ -81,17 +86,16 @@ import org.schabi.newpipe.util.PermissionHelper;
|
||||
import org.schabi.newpipe.util.ThemeHelper;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
|
||||
import icepick.State;
|
||||
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.functions.Function;
|
||||
import io.reactivex.schedulers.Schedulers;
|
||||
|
||||
import static org.schabi.newpipe.util.AnimationUtils.animateView;
|
||||
@@ -107,8 +111,6 @@ public class VideoDetailFragment
|
||||
// Amount of videos to show on start
|
||||
private static final int INITIAL_RELATED_VIDEOS = 8;
|
||||
|
||||
private ArrayList<VideoStream> sortedStreamVideosList;
|
||||
|
||||
private InfoItemBuilder infoItemBuilder = null;
|
||||
|
||||
private int updateFlags = 0;
|
||||
@@ -120,22 +122,21 @@ public class VideoDetailFragment
|
||||
private boolean showRelatedStreams;
|
||||
private boolean wasRelatedStreamsExpanded = false;
|
||||
|
||||
@State
|
||||
protected int serviceId = Constants.NO_SERVICE_ID;
|
||||
@State
|
||||
protected String name;
|
||||
@State
|
||||
protected String url;
|
||||
@State protected int serviceId = Constants.NO_SERVICE_ID;
|
||||
@State protected String name;
|
||||
@State protected String url;
|
||||
|
||||
private StreamInfo currentInfo;
|
||||
private Disposable currentWorker;
|
||||
private CompositeDisposable disposables = new CompositeDisposable();
|
||||
|
||||
private int selectedVideoStream = -1;
|
||||
private List<VideoStream> sortedVideoStreams;
|
||||
private int selectedVideoStreamIndex = -1;
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Views
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
private Menu menu;
|
||||
|
||||
private Spinner spinnerToolbar;
|
||||
@@ -157,6 +158,7 @@ public class VideoDetailFragment
|
||||
private TextView detailControlsAddToPlaylist;
|
||||
private TextView detailControlsDownload;
|
||||
private TextView appendControlsDetail;
|
||||
private TextView detailDurationView;
|
||||
|
||||
private LinearLayout videoDescriptionRootLayout;
|
||||
private TextView videoUploadDateView;
|
||||
@@ -202,7 +204,7 @@ public class VideoDetailFragment
|
||||
}
|
||||
|
||||
@Override
|
||||
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
|
||||
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
|
||||
return inflater.inflate(R.layout.fragment_video_detail, container, false);
|
||||
}
|
||||
|
||||
@@ -321,7 +323,7 @@ public class VideoDetailFragment
|
||||
if (serializable instanceof StreamInfo) {
|
||||
//noinspection unchecked
|
||||
currentInfo = (StreamInfo) serializable;
|
||||
InfoCache.getInstance().putInfo(currentInfo);
|
||||
InfoCache.getInstance().putInfo(serviceId, url, currentInfo);
|
||||
}
|
||||
|
||||
serializable = savedState.getSerializable(STACK_KEY);
|
||||
@@ -353,21 +355,8 @@ public class VideoDetailFragment
|
||||
}
|
||||
break;
|
||||
case R.id.detail_controls_download:
|
||||
if (!PermissionHelper.checkStoragePermissions(activity)) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
DownloadDialog downloadDialog =
|
||||
DownloadDialog.newInstance(currentInfo,
|
||||
sortedStreamVideosList,
|
||||
selectedVideoStream);
|
||||
downloadDialog.show(activity.getSupportFragmentManager(), "downloadDialog");
|
||||
} catch (Exception e) {
|
||||
Toast.makeText(activity,
|
||||
R.string.could_not_setup_download_menu,
|
||||
Toast.LENGTH_LONG).show();
|
||||
e.printStackTrace();
|
||||
if (PermissionHelper.checkStoragePermissions(activity, PermissionHelper.DOWNLOAD_DIALOG_REQUEST_CODE)) {
|
||||
this.openDownloadDialog();
|
||||
}
|
||||
break;
|
||||
case R.id.detail_uploader_root_layout:
|
||||
@@ -382,7 +371,8 @@ public class VideoDetailFragment
|
||||
}
|
||||
break;
|
||||
case R.id.detail_thumbnail_root_layout:
|
||||
if (currentInfo.video_streams.isEmpty() && currentInfo.video_only_streams.isEmpty()) {
|
||||
if (currentInfo.getVideoStreams().isEmpty()
|
||||
&& currentInfo.getVideoOnlyStreams().isEmpty()) {
|
||||
openBackgroundPlayer(false);
|
||||
} else {
|
||||
openVideoPlayer();
|
||||
@@ -408,6 +398,9 @@ public class VideoDetailFragment
|
||||
case R.id.detail_controls_popup:
|
||||
openPopupPlayer(true);
|
||||
break;
|
||||
case R.id.detail_controls_download:
|
||||
NavigationHelper.openDownloads(getActivity());
|
||||
break;
|
||||
}
|
||||
|
||||
return true;
|
||||
@@ -458,7 +451,6 @@ public class VideoDetailFragment
|
||||
@Override
|
||||
protected void initViews(View rootView, Bundle savedInstanceState) {
|
||||
super.initViews(rootView, savedInstanceState);
|
||||
|
||||
spinnerToolbar = activity.findViewById(R.id.toolbar).findViewById(R.id.toolbar_spinner);
|
||||
|
||||
parallaxScrollRootView = rootView.findViewById(R.id.detail_main_content);
|
||||
@@ -479,6 +471,7 @@ public class VideoDetailFragment
|
||||
detailControlsAddToPlaylist = rootView.findViewById(R.id.detail_controls_playlist_append);
|
||||
detailControlsDownload = rootView.findViewById(R.id.detail_controls_download);
|
||||
appendControlsDetail = rootView.findViewById(R.id.touch_append_detail);
|
||||
detailDurationView = rootView.findViewById(R.id.detail_duration_view);
|
||||
|
||||
videoDescriptionRootLayout = rootView.findViewById(R.id.detail_description_root_layout);
|
||||
videoUploadDateView = rootView.findViewById(R.id.detail_upload_date_view);
|
||||
@@ -529,6 +522,7 @@ public class VideoDetailFragment
|
||||
detailControlsPopup.setOnClickListener(this);
|
||||
detailControlsAddToPlaylist.setOnClickListener(this);
|
||||
detailControlsDownload.setOnClickListener(this);
|
||||
detailControlsDownload.setOnLongClickListener(this);
|
||||
relatedStreamExpandButton.setOnClickListener(this);
|
||||
|
||||
detailControlsBackground.setLongClickable(true);
|
||||
@@ -545,7 +539,8 @@ public class VideoDetailFragment
|
||||
|
||||
final String[] commands = new String[]{
|
||||
context.getResources().getString(R.string.enqueue_on_background),
|
||||
context.getResources().getString(R.string.enqueue_on_popup)
|
||||
context.getResources().getString(R.string.enqueue_on_popup),
|
||||
context.getResources().getString(R.string.append_playlist)
|
||||
};
|
||||
|
||||
final DialogInterface.OnClickListener actions = (DialogInterface dialogInterface, int i) -> {
|
||||
@@ -556,6 +551,12 @@ public class VideoDetailFragment
|
||||
case 1:
|
||||
NavigationHelper.enqueueOnPopupPlayer(getActivity(), new SinglePlayQueue(item));
|
||||
break;
|
||||
case 2:
|
||||
if (getFragmentManager() != null) {
|
||||
PlaylistAppendDialog.fromStreamInfoItems(Collections.singletonList(item))
|
||||
.show(getFragmentManager(), TAG);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
@@ -579,30 +580,25 @@ public class VideoDetailFragment
|
||||
};
|
||||
}
|
||||
|
||||
private void initThumbnailViews(StreamInfo info) {
|
||||
private void initThumbnailViews(@NonNull StreamInfo info) {
|
||||
thumbnailImageView.setImageResource(R.drawable.dummy_thumbnail_dark);
|
||||
if (!TextUtils.isEmpty(info.getThumbnailUrl())) {
|
||||
imageLoader.displayImage(
|
||||
info.getThumbnailUrl(),
|
||||
thumbnailImageView,
|
||||
DISPLAY_THUMBNAIL_OPTIONS, new SimpleImageLoadingListener() {
|
||||
final String infoServiceName = NewPipe.getNameOfService(info.getServiceId());
|
||||
final ImageLoadingListener onFailListener = new SimpleImageLoadingListener() {
|
||||
@Override
|
||||
public void onLoadingFailed(String imageUri, View view, FailReason failReason) {
|
||||
ErrorActivity.reportError(
|
||||
activity,
|
||||
failReason.getCause(),
|
||||
null,
|
||||
activity.findViewById(android.R.id.content),
|
||||
ErrorActivity.ErrorInfo.make(UserAction.LOAD_IMAGE,
|
||||
NewPipe.getNameOfService(currentInfo.getServiceId()),
|
||||
imageUri,
|
||||
R.string.could_not_load_thumbnails));
|
||||
showSnackBarError(failReason.getCause(), UserAction.LOAD_IMAGE,
|
||||
infoServiceName, imageUri, R.string.could_not_load_thumbnails);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
imageLoader.displayImage(info.getThumbnailUrl(), thumbnailImageView,
|
||||
ImageDisplayConstants.DISPLAY_THUMBNAIL_OPTIONS, onFailListener);
|
||||
}
|
||||
|
||||
if (!TextUtils.isEmpty(info.getUploaderAvatarUrl())) {
|
||||
imageLoader.displayImage(info.getUploaderAvatarUrl(), uploaderThumb, DISPLAY_AVATAR_OPTIONS);
|
||||
imageLoader.displayImage(info.getUploaderAvatarUrl(), uploaderThumb,
|
||||
ImageDisplayConstants.DISPLAY_AVATAR_OPTIONS);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -617,7 +613,8 @@ public class VideoDetailFragment
|
||||
relatedStreamRootLayout.setVisibility(View.VISIBLE);
|
||||
} else nextStreamTitle.setVisibility(View.GONE);
|
||||
|
||||
if (info.related_streams != null && !info.related_streams.isEmpty() && showRelatedStreams) {
|
||||
if (info.getRelatedStreams() != null
|
||||
&& !info.getRelatedStreams().isEmpty() && showRelatedStreams) {
|
||||
//long first = System.nanoTime(), each;
|
||||
int to = info.getRelatedStreams().size() >= INITIAL_RELATED_VIDEOS
|
||||
? INITIAL_RELATED_VIDEOS
|
||||
@@ -681,15 +678,15 @@ public class VideoDetailFragment
|
||||
int id = item.getItemId();
|
||||
switch (id) {
|
||||
case R.id.menu_item_share: {
|
||||
if(currentInfo != null) {
|
||||
shareUrl(currentInfo.name, url);
|
||||
} else {
|
||||
shareUrl(url, url);
|
||||
if (currentInfo != null) {
|
||||
shareUrl(currentInfo.getName(), currentInfo.getUrl());
|
||||
}
|
||||
return true;
|
||||
}
|
||||
case R.id.menu_item_openInBrowser: {
|
||||
openUrlInBrowser(url);
|
||||
if (currentInfo != null) {
|
||||
openUrlInBrowser(currentInfo.getUrl());
|
||||
}
|
||||
return true;
|
||||
}
|
||||
case R.id.action_play_with_kodi:
|
||||
@@ -722,27 +719,25 @@ public class VideoDetailFragment
|
||||
|
||||
private void setupActionBar(final StreamInfo info) {
|
||||
if (DEBUG) Log.d(TAG, "setupActionBarHandler() called with: info = [" + info + "]");
|
||||
sortedStreamVideosList = new ArrayList<>(ListHelper.getSortedStreamVideosList(
|
||||
activity, info.getVideoStreams(), info.getVideoOnlyStreams(), false));
|
||||
|
||||
selectedVideoStream = ListHelper.getDefaultResolutionIndex(activity, sortedStreamVideosList);
|
||||
|
||||
boolean isExternalPlayerEnabled = PreferenceManager.getDefaultSharedPreferences(activity)
|
||||
.getBoolean(activity.getString(R.string.use_external_video_player_key), false);
|
||||
spinnerToolbar.setAdapter(new SpinnerToolbarAdapter(activity, sortedStreamVideosList,
|
||||
isExternalPlayerEnabled));
|
||||
spinnerToolbar.setSelection(selectedVideoStream);
|
||||
|
||||
sortedVideoStreams = ListHelper.getSortedStreamVideosList(activity, info.getVideoStreams(), info.getVideoOnlyStreams(), false);
|
||||
selectedVideoStreamIndex = ListHelper.getDefaultResolutionIndex(activity, sortedVideoStreams);
|
||||
|
||||
final StreamItemAdapter<VideoStream> streamsAdapter = new StreamItemAdapter<>(activity, new StreamSizeWrapper<>(sortedVideoStreams), isExternalPlayerEnabled);
|
||||
spinnerToolbar.setAdapter(streamsAdapter);
|
||||
spinnerToolbar.setSelection(selectedVideoStreamIndex);
|
||||
spinnerToolbar.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
|
||||
@Override
|
||||
public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
|
||||
selectedVideoStream = position;
|
||||
selectedVideoStreamIndex = position;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onNothingSelected(AdapterView<?> parent) {
|
||||
}
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
@@ -818,7 +813,7 @@ public class VideoDetailFragment
|
||||
public void prepareAndHandleInfo(final StreamInfo info, boolean scrollToTop) {
|
||||
if (DEBUG) Log.d(TAG, "prepareAndHandleInfo() called with: info = [" + info + "], scrollToTop = [" + scrollToTop + "]");
|
||||
|
||||
setInitialData(info.getServiceId(), info.getUrl(), info.getName());
|
||||
setInitialData(info.getServiceId(), info.getOriginalUrl(), info.getName());
|
||||
pushToStack(serviceId, url, name);
|
||||
showLoading();
|
||||
|
||||
@@ -954,8 +949,9 @@ public class VideoDetailFragment
|
||||
this.autoPlayEnabled = autoplay;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private VideoStream getSelectedVideoStream() {
|
||||
return sortedStreamVideosList.get(selectedVideoStream);
|
||||
return sortedVideoStreams != null ? sortedVideoStreams.get(selectedVideoStreamIndex) : null;
|
||||
}
|
||||
|
||||
private void prepareDescription(final String descriptionHtml) {
|
||||
@@ -1006,9 +1002,6 @@ public class VideoDetailFragment
|
||||
int height = isPortrait
|
||||
? (int) (metrics.widthPixels / (16.0f / 9.0f))
|
||||
: (int) (metrics.heightPixels / 2f);
|
||||
thumbnailImageView.setScaleType(isPortrait
|
||||
? ImageView.ScaleType.CENTER_CROP
|
||||
: ImageView.ScaleType.FIT_CENTER);
|
||||
thumbnailImageView.setLayoutParams(
|
||||
new FrameLayout.LayoutParams(RelativeLayout.LayoutParams.MATCH_PARENT, height));
|
||||
thumbnailImageView.setMinimumHeight(height);
|
||||
@@ -1094,6 +1087,7 @@ public class VideoDetailFragment
|
||||
animateView(contentRootLayoutHiding, false, 200);
|
||||
animateView(spinnerToolbar, false, 200);
|
||||
animateView(thumbnailPlayButton, false, 50);
|
||||
animateView(detailDurationView, false, 100);
|
||||
|
||||
videoTitleTextView.setText(name != null ? name : "");
|
||||
videoTitleTextView.setMaxLines(1);
|
||||
@@ -1114,7 +1108,7 @@ public class VideoDetailFragment
|
||||
public void handleResult(@NonNull StreamInfo info) {
|
||||
super.handleResult(info);
|
||||
|
||||
setInitialData(info.getServiceId(), info.getUrl(), info.getName());
|
||||
setInitialData(info.getServiceId(), info.getOriginalUrl(), info.getName());
|
||||
pushToStack(serviceId, url, name);
|
||||
|
||||
animateView(thumbnailPlayButton, true, 200);
|
||||
@@ -1164,6 +1158,18 @@ public class VideoDetailFragment
|
||||
thumbsDisabledTextView.setVisibility(View.GONE);
|
||||
}
|
||||
|
||||
if (info.getDuration() > 0) {
|
||||
detailDurationView.setText(Localization.getDurationString(info.getDuration()));
|
||||
detailDurationView.setBackgroundColor(ContextCompat.getColor(activity, R.color.duration_background_color));
|
||||
animateView(detailDurationView, true, 100);
|
||||
} else if (info.getStreamType() == StreamType.LIVE_STREAM) {
|
||||
detailDurationView.setText(R.string.duration_live);
|
||||
detailDurationView.setBackgroundColor(ContextCompat.getColor(activity, R.color.live_duration_background_color));
|
||||
animateView(detailDurationView, true, 100);
|
||||
} else {
|
||||
detailDurationView.setVisibility(View.GONE);
|
||||
}
|
||||
|
||||
videoTitleRoot.setClickable(true);
|
||||
videoTitleToggleArrow.setVisibility(View.VISIBLE);
|
||||
videoTitleToggleArrow.setImageResource(R.drawable.arrow_down);
|
||||
@@ -1182,7 +1188,9 @@ public class VideoDetailFragment
|
||||
toggleExpandRelatedVideos(currentInfo);
|
||||
wasRelatedStreamsExpanded = false;
|
||||
}
|
||||
|
||||
setTitleToUrl(info.getServiceId(), info.getUrl(), info.getName());
|
||||
setTitleToUrl(info.getServiceId(), info.getOriginalUrl(), info.getName());
|
||||
|
||||
if (!info.getErrors().isEmpty()) {
|
||||
showSnackBarError(info.getErrors(),
|
||||
@@ -1192,11 +1200,21 @@ public class VideoDetailFragment
|
||||
0);
|
||||
}
|
||||
|
||||
if (info.video_streams.isEmpty() && info.video_only_streams.isEmpty()) {
|
||||
detailControlsBackground.setVisibility(View.GONE);
|
||||
detailControlsPopup.setVisibility(View.GONE);
|
||||
spinnerToolbar.setVisibility(View.GONE);
|
||||
thumbnailPlayButton.setImageResource(R.drawable.ic_headset_white_24dp);
|
||||
switch (info.getStreamType()) {
|
||||
case LIVE_STREAM:
|
||||
case AUDIO_LIVE_STREAM:
|
||||
detailControlsDownload.setVisibility(View.GONE);
|
||||
spinnerToolbar.setVisibility(View.GONE);
|
||||
break;
|
||||
default:
|
||||
if (!info.getVideoStreams().isEmpty()
|
||||
|| !info.getVideoOnlyStreams().isEmpty()) break;
|
||||
|
||||
detailControlsBackground.setVisibility(View.GONE);
|
||||
detailControlsPopup.setVisibility(View.GONE);
|
||||
spinnerToolbar.setVisibility(View.GONE);
|
||||
thumbnailPlayButton.setImageResource(R.drawable.ic_headset_white_24dp);
|
||||
break;
|
||||
}
|
||||
|
||||
if (autoPlayEnabled) {
|
||||
@@ -1206,6 +1224,23 @@ public class VideoDetailFragment
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public void openDownloadDialog() {
|
||||
try {
|
||||
DownloadDialog downloadDialog = DownloadDialog.newInstance(currentInfo);
|
||||
downloadDialog.setVideoStreams(sortedVideoStreams);
|
||||
downloadDialog.setAudioStreams(currentInfo.getAudioStreams());
|
||||
downloadDialog.setSelectedVideoStream(selectedVideoStreamIndex);
|
||||
|
||||
downloadDialog.show(activity.getSupportFragmentManager(), "downloadDialog");
|
||||
} catch (Exception e) {
|
||||
Toast.makeText(activity,
|
||||
R.string.could_not_setup_download_menu,
|
||||
Toast.LENGTH_LONG).show();
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Stream Results
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
@@ -1216,8 +1251,6 @@ public class VideoDetailFragment
|
||||
|
||||
if (exception instanceof YoutubeStreamExtractor.GemaException) {
|
||||
onBlockedByGemaError();
|
||||
} else if (exception instanceof YoutubeStreamExtractor.LiveStreamException) {
|
||||
showError(getString(R.string.live_streams_not_supported), false);
|
||||
} else if (exception instanceof ContentNotAvailableException) {
|
||||
showError(getString(R.string.content_not_available), false);
|
||||
} else {
|
||||
|
||||
@@ -20,10 +20,10 @@ import org.schabi.newpipe.extractor.playlist.PlaylistInfoItem;
|
||||
import org.schabi.newpipe.extractor.stream.StreamInfoItem;
|
||||
import org.schabi.newpipe.fragments.BaseStateFragment;
|
||||
import org.schabi.newpipe.fragments.OnScrollBelowItemsListener;
|
||||
import org.schabi.newpipe.fragments.local.dialog.PlaylistAppendDialog;
|
||||
import org.schabi.newpipe.local.dialog.PlaylistAppendDialog;
|
||||
import org.schabi.newpipe.info_list.InfoItemDialog;
|
||||
import org.schabi.newpipe.info_list.InfoListAdapter;
|
||||
import org.schabi.newpipe.playlist.SinglePlayQueue;
|
||||
import org.schabi.newpipe.player.playqueue.SinglePlayQueue;
|
||||
import org.schabi.newpipe.util.NavigationHelper;
|
||||
import org.schabi.newpipe.util.OnClickGesture;
|
||||
import org.schabi.newpipe.util.StateSaver;
|
||||
@@ -140,10 +140,7 @@ public abstract class BaseListFragment<I, N> extends BaseStateFragment<I> implem
|
||||
infoListAdapter.setOnStreamSelectedListener(new OnClickGesture<StreamInfoItem>() {
|
||||
@Override
|
||||
public void selected(StreamInfoItem selectedItem) {
|
||||
onItemSelected(selectedItem);
|
||||
NavigationHelper.openVideoDetailFragment(
|
||||
useAsFrontPage?getParentFragment().getFragmentManager():getFragmentManager(),
|
||||
selectedItem.getServiceId(), selectedItem.getUrl(), selectedItem.getName());
|
||||
onStreamSelected(selectedItem);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -156,8 +153,7 @@ public abstract class BaseListFragment<I, N> extends BaseStateFragment<I> implem
|
||||
@Override
|
||||
public void selected(ChannelInfoItem selectedItem) {
|
||||
onItemSelected(selectedItem);
|
||||
NavigationHelper.openChannelFragment(
|
||||
useAsFrontPage?getParentFragment().getFragmentManager():getFragmentManager(),
|
||||
NavigationHelper.openChannelFragment(useAsFrontPage ? getParentFragment().getFragmentManager() : getFragmentManager(),
|
||||
selectedItem.getServiceId(), selectedItem.getUrl(), selectedItem.getName());
|
||||
}
|
||||
});
|
||||
@@ -166,8 +162,7 @@ public abstract class BaseListFragment<I, N> extends BaseStateFragment<I> implem
|
||||
@Override
|
||||
public void selected(PlaylistInfoItem selectedItem) {
|
||||
onItemSelected(selectedItem);
|
||||
NavigationHelper.openPlaylistFragment(
|
||||
useAsFrontPage?getParentFragment().getFragmentManager():getFragmentManager(),
|
||||
NavigationHelper.openPlaylistFragment(useAsFrontPage ? getParentFragment().getFragmentManager() : getFragmentManager(),
|
||||
selectedItem.getServiceId(), selectedItem.getUrl(), selectedItem.getName());
|
||||
}
|
||||
});
|
||||
@@ -181,6 +176,12 @@ public abstract class BaseListFragment<I, N> extends BaseStateFragment<I> implem
|
||||
});
|
||||
}
|
||||
|
||||
private void onStreamSelected(StreamInfoItem selectedItem) {
|
||||
onItemSelected(selectedItem);
|
||||
NavigationHelper.openVideoDetailFragment(useAsFrontPage ? getParentFragment().getFragmentManager() : getFragmentManager(),
|
||||
selectedItem.getServiceId(), selectedItem.getUrl(), selectedItem.getName());
|
||||
}
|
||||
|
||||
protected void onScrollToBottom() {
|
||||
if (hasMoreItems() && !isLoading.get()) {
|
||||
loadMoreItems();
|
||||
@@ -219,6 +220,7 @@ public abstract class BaseListFragment<I, N> extends BaseStateFragment<I> implem
|
||||
|
||||
new InfoItemDialog(getActivity(), item, commands, actions).show();
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Menu
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
@@ -230,7 +232,7 @@ public abstract class BaseListFragment<I, N> extends BaseStateFragment<I> implem
|
||||
ActionBar supportActionBar = activity.getSupportActionBar();
|
||||
if (supportActionBar != null) {
|
||||
supportActionBar.setDisplayShowTitleEnabled(true);
|
||||
if(useAsFrontPage) {
|
||||
if (useAsFrontPage) {
|
||||
supportActionBar.setDisplayHomeAsUpEnabled(false);
|
||||
} else {
|
||||
supportActionBar.setDisplayHomeAsUpEnabled(true);
|
||||
@@ -277,9 +279,8 @@ public abstract class BaseListFragment<I, N> extends BaseStateFragment<I> implem
|
||||
|
||||
@Override
|
||||
public void showListFooter(final boolean show) {
|
||||
itemsList.post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
itemsList.post(() -> {
|
||||
if (infoListAdapter != null && itemsList != null) {
|
||||
infoListAdapter.showFooter(show);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -19,7 +19,8 @@ import io.reactivex.disposables.Disposable;
|
||||
import io.reactivex.functions.Consumer;
|
||||
import io.reactivex.schedulers.Schedulers;
|
||||
|
||||
public abstract class BaseListInfoFragment<I extends ListInfo> extends BaseListFragment<I, ListExtractor.NextItemsResult> {
|
||||
public abstract class BaseListInfoFragment<I extends ListInfo>
|
||||
extends BaseListFragment<I, ListExtractor.InfoItemsPage> {
|
||||
|
||||
@State
|
||||
protected int serviceId = Constants.NO_SERVICE_ID;
|
||||
@@ -29,7 +30,7 @@ public abstract class BaseListInfoFragment<I extends ListInfo> extends BaseListF
|
||||
protected String url;
|
||||
|
||||
protected I currentInfo;
|
||||
protected String currentNextItemsUrl;
|
||||
protected String currentNextPageUrl;
|
||||
protected Disposable currentWorker;
|
||||
|
||||
@Override
|
||||
@@ -73,7 +74,7 @@ public abstract class BaseListInfoFragment<I extends ListInfo> extends BaseListF
|
||||
public void writeTo(Queue<Object> objectsToSave) {
|
||||
super.writeTo(objectsToSave);
|
||||
objectsToSave.add(currentInfo);
|
||||
objectsToSave.add(currentNextItemsUrl);
|
||||
objectsToSave.add(currentNextPageUrl);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -81,7 +82,7 @@ public abstract class BaseListInfoFragment<I extends ListInfo> extends BaseListF
|
||||
public void readFrom(@NonNull Queue<Object> savedObjects) throws Exception {
|
||||
super.readFrom(savedObjects);
|
||||
currentInfo = (I) savedObjects.poll();
|
||||
currentNextItemsUrl = (String) savedObjects.poll();
|
||||
currentNextPageUrl = (String) savedObjects.poll();
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
@@ -116,7 +117,7 @@ public abstract class BaseListInfoFragment<I extends ListInfo> extends BaseListF
|
||||
.subscribe((@NonNull I result) -> {
|
||||
isLoading.set(false);
|
||||
currentInfo = result;
|
||||
currentNextItemsUrl = result.next_streams_url;
|
||||
currentNextPageUrl = result.getNextPageUrl();
|
||||
handleResult(result);
|
||||
}, (@NonNull Throwable throwable) -> onError(throwable));
|
||||
}
|
||||
@@ -125,7 +126,7 @@ public abstract class BaseListInfoFragment<I extends ListInfo> extends BaseListF
|
||||
* Implement the logic to load more items<br/>
|
||||
* You can use the default implementations from {@link org.schabi.newpipe.util.ExtractorHelper}
|
||||
*/
|
||||
protected abstract Single<ListExtractor.NextItemsResult> loadMoreItemsLogic();
|
||||
protected abstract Single<ListExtractor.InfoItemsPage> loadMoreItemsLogic();
|
||||
|
||||
protected void loadMoreItems() {
|
||||
isLoading.set(true);
|
||||
@@ -134,9 +135,9 @@ public abstract class BaseListInfoFragment<I extends ListInfo> extends BaseListF
|
||||
currentWorker = loadMoreItemsLogic()
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe((@io.reactivex.annotations.NonNull ListExtractor.NextItemsResult nextItemsResult) -> {
|
||||
.subscribe((@io.reactivex.annotations.NonNull ListExtractor.InfoItemsPage InfoItemsPage) -> {
|
||||
isLoading.set(false);
|
||||
handleNextItems(nextItemsResult);
|
||||
handleNextItems(InfoItemsPage);
|
||||
}, (@io.reactivex.annotations.NonNull Throwable throwable) -> {
|
||||
isLoading.set(false);
|
||||
onError(throwable);
|
||||
@@ -144,17 +145,17 @@ public abstract class BaseListInfoFragment<I extends ListInfo> extends BaseListF
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleNextItems(ListExtractor.NextItemsResult result) {
|
||||
public void handleNextItems(ListExtractor.InfoItemsPage result) {
|
||||
super.handleNextItems(result);
|
||||
currentNextItemsUrl = result.nextItemsUrl;
|
||||
infoListAdapter.addInfoItemList(result.nextItemsList);
|
||||
currentNextPageUrl = result.getNextPageUrl();
|
||||
infoListAdapter.addInfoItemList(result.getItems());
|
||||
|
||||
showListFooter(hasMoreItems());
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean hasMoreItems() {
|
||||
return !TextUtils.isEmpty(currentNextItemsUrl);
|
||||
return !TextUtils.isEmpty(currentNextPageUrl);
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
@@ -170,8 +171,8 @@ public abstract class BaseListInfoFragment<I extends ListInfo> extends BaseListF
|
||||
setTitle(name);
|
||||
|
||||
if (infoListAdapter.getItemsList().size() == 0) {
|
||||
if (result.related_streams.size() > 0) {
|
||||
infoListAdapter.addInfoItemList(result.related_streams);
|
||||
if (result.getRelatedItems().size() > 0) {
|
||||
infoListAdapter.addInfoItemList(result.getRelatedItems());
|
||||
showListFooter(hasMoreItems());
|
||||
} else {
|
||||
infoListAdapter.clearStreamItemList();
|
||||
|
||||
@@ -27,23 +27,28 @@ import com.jakewharton.rxbinding2.view.RxView;
|
||||
|
||||
import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.database.subscription.SubscriptionEntity;
|
||||
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.ExtractionException;
|
||||
import org.schabi.newpipe.extractor.stream.StreamInfoItem;
|
||||
import org.schabi.newpipe.fragments.list.BaseListInfoFragment;
|
||||
import org.schabi.newpipe.fragments.subscription.SubscriptionService;
|
||||
import org.schabi.newpipe.info_list.InfoItemDialog;
|
||||
import org.schabi.newpipe.playlist.ChannelPlayQueue;
|
||||
import org.schabi.newpipe.playlist.PlayQueue;
|
||||
import org.schabi.newpipe.playlist.SinglePlayQueue;
|
||||
import org.schabi.newpipe.local.dialog.PlaylistAppendDialog;
|
||||
import org.schabi.newpipe.player.playqueue.ChannelPlayQueue;
|
||||
import org.schabi.newpipe.player.playqueue.PlayQueue;
|
||||
import org.schabi.newpipe.player.playqueue.SinglePlayQueue;
|
||||
import org.schabi.newpipe.report.UserAction;
|
||||
import org.schabi.newpipe.local.subscription.SubscriptionService;
|
||||
import org.schabi.newpipe.util.AnimationUtils;
|
||||
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 java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
@@ -108,11 +113,11 @@ public class ChannelFragment extends BaseListInfoFragment<ChannelInfo> {
|
||||
@Override
|
||||
public void onAttach(Context context) {
|
||||
super.onAttach(context);
|
||||
subscriptionService = SubscriptionService.getInstance();
|
||||
subscriptionService = SubscriptionService.getInstance(activity);
|
||||
}
|
||||
|
||||
@Override
|
||||
public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
|
||||
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
|
||||
return inflater.inflate(R.layout.fragment_channel, container, false);
|
||||
}
|
||||
|
||||
@@ -156,6 +161,7 @@ public class ChannelFragment extends BaseListInfoFragment<ChannelInfo> {
|
||||
context.getResources().getString(R.string.start_here_on_main),
|
||||
context.getResources().getString(R.string.start_here_on_background),
|
||||
context.getResources().getString(R.string.start_here_on_popup),
|
||||
context.getResources().getString(R.string.append_playlist)
|
||||
};
|
||||
|
||||
final DialogInterface.OnClickListener actions = new DialogInterface.OnClickListener() {
|
||||
@@ -178,6 +184,12 @@ public class ChannelFragment extends BaseListInfoFragment<ChannelInfo> {
|
||||
case 4:
|
||||
NavigationHelper.playOnPopupPlayer(activity, getPlayQueue(index));
|
||||
break;
|
||||
case 5:
|
||||
if (getFragmentManager() != null) {
|
||||
PlaylistAppendDialog.fromStreamInfoItems(Collections.singletonList(item))
|
||||
.show(getFragmentManager(), TAG);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
@@ -388,8 +400,8 @@ public class ChannelFragment extends BaseListInfoFragment<ChannelInfo> {
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
@Override
|
||||
protected Single<ListExtractor.NextItemsResult> loadMoreItemsLogic() {
|
||||
return ExtractorHelper.getMoreChannelItems(serviceId, url, currentNextItemsUrl);
|
||||
protected Single<ListExtractor.InfoItemsPage> loadMoreItemsLogic() {
|
||||
return ExtractorHelper.getMoreChannelItems(serviceId, url, currentNextPageUrl);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -415,8 +427,10 @@ public class ChannelFragment extends BaseListInfoFragment<ChannelInfo> {
|
||||
super.handleResult(result);
|
||||
|
||||
headerRootLayout.setVisibility(View.VISIBLE);
|
||||
imageLoader.displayImage(result.banner_url, headerChannelBanner, DISPLAY_BANNER_OPTIONS);
|
||||
imageLoader.displayImage(result.avatar_url, headerAvatarView, DISPLAY_AVATAR_OPTIONS);
|
||||
imageLoader.displayImage(result.getBannerUrl(), headerChannelBanner,
|
||||
ImageDisplayConstants.DISPLAY_BANNER_OPTIONS);
|
||||
imageLoader.displayImage(result.getAvatarUrl(), headerAvatarView,
|
||||
ImageDisplayConstants.DISPLAY_AVATAR_OPTIONS);
|
||||
|
||||
if (result.getSubscriberCount() != -1) {
|
||||
headerSubscribersTextView.setText(Localization.localizeSubscribersCount(activity, result.getSubscriberCount()));
|
||||
@@ -427,8 +441,8 @@ public class ChannelFragment extends BaseListInfoFragment<ChannelInfo> {
|
||||
|
||||
playlistCtrl.setVisibility(View.VISIBLE);
|
||||
|
||||
if (!result.errors.isEmpty()) {
|
||||
showSnackBarError(result.errors, UserAction.REQUESTED_CHANNEL, NewPipe.getNameOfService(result.getServiceId()), result.getUrl(), 0);
|
||||
if (!result.getErrors().isEmpty()) {
|
||||
showSnackBarError(result.getErrors(), UserAction.REQUESTED_CHANNEL, NewPipe.getNameOfService(result.getServiceId()), result.getUrl(), 0);
|
||||
}
|
||||
|
||||
if (disposables != null) disposables.clear();
|
||||
@@ -436,24 +450,12 @@ public class ChannelFragment extends BaseListInfoFragment<ChannelInfo> {
|
||||
updateSubscription(result);
|
||||
monitorSubscription(result);
|
||||
|
||||
headerPlayAllButton.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View view) {
|
||||
NavigationHelper.playOnMainPlayer(activity, getPlayQueue());
|
||||
}
|
||||
});
|
||||
headerPopupButton.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View view) {
|
||||
NavigationHelper.playOnPopupPlayer(activity, getPlayQueue());
|
||||
}
|
||||
});
|
||||
headerBackgroundButton.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View view) {
|
||||
NavigationHelper.playOnBackgroundPlayer(activity, getPlayQueue());
|
||||
}
|
||||
});
|
||||
headerPlayAllButton.setOnClickListener(
|
||||
view -> NavigationHelper.playOnMainPlayer(activity, getPlayQueue()));
|
||||
headerPopupButton.setOnClickListener(
|
||||
view -> NavigationHelper.playOnPopupPlayer(activity, getPlayQueue()));
|
||||
headerBackgroundButton.setOnClickListener(
|
||||
view -> NavigationHelper.playOnBackgroundPlayer(activity, getPlayQueue()));
|
||||
}
|
||||
|
||||
private PlayQueue getPlayQueue() {
|
||||
@@ -461,17 +463,23 @@ public class ChannelFragment extends BaseListInfoFragment<ChannelInfo> {
|
||||
}
|
||||
|
||||
private PlayQueue getPlayQueue(final int index) {
|
||||
final List<StreamInfoItem> streamItems = new ArrayList<>();
|
||||
for(InfoItem i : infoListAdapter.getItemsList()) {
|
||||
if(i instanceof StreamInfoItem) {
|
||||
streamItems.add((StreamInfoItem) i);
|
||||
}
|
||||
}
|
||||
return new ChannelPlayQueue(
|
||||
currentInfo.getServiceId(),
|
||||
currentInfo.getUrl(),
|
||||
currentInfo.getNextStreamsUrl(),
|
||||
infoListAdapter.getItemsList(),
|
||||
currentInfo.getNextPageUrl(),
|
||||
streamItems,
|
||||
index
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleNextItems(ListExtractor.NextItemsResult result) {
|
||||
public void handleNextItems(ListExtractor.InfoItemsPage result) {
|
||||
super.handleNextItems(result);
|
||||
|
||||
if (!result.getErrors().isEmpty()) {
|
||||
|
||||
@@ -77,8 +77,7 @@ public class KioskFragment extends BaseListInfoFragment<KioskInfo> {
|
||||
UrlIdHandler kioskTypeUrlIdHandler = service.getKioskList()
|
||||
.getUrlIdHandlerByType(kioskId);
|
||||
instance.setInitialData(serviceId,
|
||||
kioskTypeUrlIdHandler.getUrl(kioskId),
|
||||
kioskId);
|
||||
kioskTypeUrlIdHandler.getUrl(kioskId), kioskId);
|
||||
instance.kioskId = kioskId;
|
||||
return instance;
|
||||
}
|
||||
@@ -141,12 +140,12 @@ public class KioskFragment extends BaseListInfoFragment<KioskInfo> {
|
||||
}
|
||||
|
||||
@Override
|
||||
public Single<ListExtractor.NextItemsResult> loadMoreItemsLogic() {
|
||||
public Single<ListExtractor.InfoItemsPage> loadMoreItemsLogic() {
|
||||
String contentCountry = PreferenceManager
|
||||
.getDefaultSharedPreferences(activity)
|
||||
.getString(getString(R.string.content_country_key),
|
||||
getString(R.string.default_country_value));
|
||||
return ExtractorHelper.getMoreKioskItems(serviceId, url, currentNextItemsUrl, contentCountry);
|
||||
return ExtractorHelper.getMoreKioskItems(serviceId, url, currentNextPageUrl, contentCountry);
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
@@ -168,13 +167,13 @@ public class KioskFragment extends BaseListInfoFragment<KioskInfo> {
|
||||
|
||||
if (!result.getErrors().isEmpty()) {
|
||||
showSnackBarError(result.getErrors(),
|
||||
UserAction.REQUESTED_PLAYLIST,
|
||||
UserAction.REQUESTED_KIOSK,
|
||||
NewPipe.getNameOfService(result.getServiceId()), result.getUrl(), 0);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleNextItems(ListExtractor.NextItemsResult result) {
|
||||
public void handleNextItems(ListExtractor.InfoItemsPage result) {
|
||||
super.handleNextItems(result);
|
||||
|
||||
if (!result.getErrors().isEmpty()) {
|
||||
|
||||
@@ -22,22 +22,25 @@ import org.reactivestreams.Subscription;
|
||||
import org.schabi.newpipe.NewPipeDatabase;
|
||||
import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.database.playlist.model.PlaylistRemoteEntity;
|
||||
import org.schabi.newpipe.extractor.InfoItem;
|
||||
import org.schabi.newpipe.extractor.ListExtractor;
|
||||
import org.schabi.newpipe.extractor.NewPipe;
|
||||
import org.schabi.newpipe.extractor.exceptions.ExtractionException;
|
||||
import org.schabi.newpipe.extractor.playlist.PlaylistInfo;
|
||||
import org.schabi.newpipe.extractor.stream.StreamInfoItem;
|
||||
import org.schabi.newpipe.fragments.list.BaseListInfoFragment;
|
||||
import org.schabi.newpipe.fragments.local.RemotePlaylistManager;
|
||||
import org.schabi.newpipe.local.playlist.RemotePlaylistManager;
|
||||
import org.schabi.newpipe.info_list.InfoItemDialog;
|
||||
import org.schabi.newpipe.playlist.PlayQueue;
|
||||
import org.schabi.newpipe.playlist.PlaylistPlayQueue;
|
||||
import org.schabi.newpipe.playlist.SinglePlayQueue;
|
||||
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.ExtractorHelper;
|
||||
import org.schabi.newpipe.util.ImageDisplayConstants;
|
||||
import org.schabi.newpipe.util.NavigationHelper;
|
||||
import org.schabi.newpipe.util.ThemeHelper;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
|
||||
@@ -206,8 +209,8 @@ public class PlaylistFragment extends BaseListInfoFragment<PlaylistInfo> {
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
@Override
|
||||
protected Single<ListExtractor.NextItemsResult> loadMoreItemsLogic() {
|
||||
return ExtractorHelper.getMorePlaylistItems(serviceId, url, currentNextItemsUrl);
|
||||
protected Single<ListExtractor.InfoItemsPage> loadMoreItemsLogic() {
|
||||
return ExtractorHelper.getMorePlaylistItems(serviceId, url, currentNextPageUrl);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -268,8 +271,10 @@ public class PlaylistFragment extends BaseListInfoFragment<PlaylistInfo> {
|
||||
|
||||
playlistCtrl.setVisibility(View.VISIBLE);
|
||||
|
||||
imageLoader.displayImage(result.getUploaderAvatarUrl(), headerUploaderAvatar, DISPLAY_AVATAR_OPTIONS);
|
||||
headerStreamCount.setText(getResources().getQuantityString(R.plurals.videos, (int) result.stream_count, (int) result.stream_count));
|
||||
imageLoader.displayImage(result.getUploaderAvatarUrl(), headerUploaderAvatar,
|
||||
ImageDisplayConstants.DISPLAY_AVATAR_OPTIONS);
|
||||
headerStreamCount.setText(getResources().getQuantityString(R.plurals.videos,
|
||||
(int) result.getStreamCount(), (int) result.getStreamCount()));
|
||||
|
||||
if (!result.getErrors().isEmpty()) {
|
||||
showSnackBarError(result.getErrors(), UserAction.REQUESTED_PLAYLIST, NewPipe.getNameOfService(result.getServiceId()), result.getUrl(), 0);
|
||||
@@ -297,17 +302,23 @@ 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) {
|
||||
infoItems.add((StreamInfoItem) i);
|
||||
}
|
||||
}
|
||||
return new PlaylistPlayQueue(
|
||||
currentInfo.getServiceId(),
|
||||
currentInfo.getUrl(),
|
||||
currentInfo.getNextStreamsUrl(),
|
||||
infoListAdapter.getItemsList(),
|
||||
currentInfo.getNextPageUrl(),
|
||||
infoItems,
|
||||
index
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleNextItems(ListExtractor.NextItemsResult result) {
|
||||
public void handleNextItems(ListExtractor.InfoItemsPage result) {
|
||||
super.handleNextItems(result);
|
||||
|
||||
if (!result.getErrors().isEmpty()) {
|
||||
|
||||
@@ -41,7 +41,7 @@ import org.schabi.newpipe.extractor.search.SearchEngine;
|
||||
import org.schabi.newpipe.extractor.search.SearchResult;
|
||||
import org.schabi.newpipe.fragments.BackPressable;
|
||||
import org.schabi.newpipe.fragments.list.BaseListFragment;
|
||||
import org.schabi.newpipe.history.HistoryRecordManager;
|
||||
import org.schabi.newpipe.local.history.HistoryRecordManager;
|
||||
import org.schabi.newpipe.report.UserAction;
|
||||
import org.schabi.newpipe.util.Constants;
|
||||
import org.schabi.newpipe.util.AnimationUtils;
|
||||
@@ -71,7 +71,9 @@ import io.reactivex.subjects.PublishSubject;
|
||||
|
||||
import static org.schabi.newpipe.util.AnimationUtils.animateView;
|
||||
|
||||
public class SearchFragment extends BaseListFragment<SearchResult, ListExtractor.NextItemsResult> implements BackPressable {
|
||||
public class SearchFragment
|
||||
extends BaseListFragment<SearchResult, ListExtractor.InfoItemsPage>
|
||||
implements BackPressable {
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Search
|
||||
@@ -527,23 +529,26 @@ public class SearchFragment extends BaseListFragment<SearchResult, ListExtractor
|
||||
}
|
||||
|
||||
private void showDeleteSuggestionDialog(final SuggestionItem item) {
|
||||
final Disposable onDelete = historyRecordManager.deleteSearchHistory(item.query)
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(
|
||||
howManyDeleted -> suggestionPublisher
|
||||
.onNext(searchEditText.getText().toString()),
|
||||
|
||||
throwable -> showSnackBarError(throwable,
|
||||
UserAction.SOMETHING_ELSE, "none",
|
||||
"Deleting item failed", R.string.general_error)
|
||||
);
|
||||
|
||||
if (activity == null || historyRecordManager == null || suggestionPublisher == null ||
|
||||
searchEditText == null || disposables == null) return;
|
||||
final String query = item.query;
|
||||
new AlertDialog.Builder(activity)
|
||||
.setTitle(item.query)
|
||||
.setTitle(query)
|
||||
.setMessage(R.string.delete_item_search_history)
|
||||
.setCancelable(true)
|
||||
.setNegativeButton(R.string.cancel, null)
|
||||
.setPositiveButton(R.string.delete, (dialog, which) -> disposables.add(onDelete))
|
||||
.setPositiveButton(R.string.delete, (dialog, which) -> {
|
||||
final Disposable onDelete = historyRecordManager.deleteSearchHistory(query)
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(
|
||||
howManyDeleted -> suggestionPublisher
|
||||
.onNext(searchEditText.getText().toString()),
|
||||
throwable -> showSnackBarError(throwable,
|
||||
UserAction.DELETE_FROM_HISTORY, "none",
|
||||
"Deleting item failed", R.string.general_error)
|
||||
);
|
||||
disposables.add(onDelete);
|
||||
})
|
||||
.show();
|
||||
}
|
||||
|
||||
@@ -701,19 +706,8 @@ public class SearchFragment extends BaseListFragment<SearchResult, ListExtractor
|
||||
searchDisposable = ExtractorHelper.searchFor(serviceId, searchQuery, currentPage, contentCountry, filter)
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(new Consumer<SearchResult>() {
|
||||
@Override
|
||||
public void accept(@NonNull SearchResult result) throws Exception {
|
||||
isLoading.set(false);
|
||||
handleResult(result);
|
||||
}
|
||||
}, new Consumer<Throwable>() {
|
||||
@Override
|
||||
public void accept(@NonNull Throwable throwable) throws Exception {
|
||||
isLoading.set(false);
|
||||
onError(throwable);
|
||||
}
|
||||
});
|
||||
.doOnEvent((searchResult, throwable) -> isLoading.set(false))
|
||||
.subscribe(this::handleResult, this::onError);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -725,19 +719,8 @@ public class SearchFragment extends BaseListFragment<SearchResult, ListExtractor
|
||||
searchDisposable = ExtractorHelper.getMoreSearchItems(serviceId, searchQuery, currentNextPage, contentCountry, filter)
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(new Consumer<ListExtractor.NextItemsResult>() {
|
||||
@Override
|
||||
public void accept(@NonNull ListExtractor.NextItemsResult result) throws Exception {
|
||||
isLoading.set(false);
|
||||
handleNextItems(result);
|
||||
}
|
||||
}, new Consumer<Throwable>() {
|
||||
@Override
|
||||
public void accept(@NonNull Throwable throwable) throws Exception {
|
||||
isLoading.set(false);
|
||||
onError(throwable);
|
||||
}
|
||||
});
|
||||
.doOnEvent((nextItemsResult, throwable) -> isLoading.set(false))
|
||||
.subscribe(this::handleNextItems, this::onError);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -778,12 +761,7 @@ public class SearchFragment extends BaseListFragment<SearchResult, ListExtractor
|
||||
public void handleSuggestions(@NonNull final List<SuggestionItem> suggestions) {
|
||||
if (DEBUG) Log.d(TAG, "handleSuggestions() called with: suggestions = [" + suggestions + "]");
|
||||
suggestionsRecyclerView.smoothScrollToPosition(0);
|
||||
suggestionsRecyclerView.post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
suggestionListAdapter.setItems(suggestions);
|
||||
}
|
||||
});
|
||||
suggestionsRecyclerView.post(() -> suggestionListAdapter.setItems(suggestions));
|
||||
|
||||
if (errorPanelRoot.getVisibility() == View.VISIBLE) {
|
||||
hideLoading();
|
||||
@@ -841,10 +819,10 @@ public class SearchFragment extends BaseListFragment<SearchResult, ListExtractor
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleNextItems(ListExtractor.NextItemsResult result) {
|
||||
public void handleNextItems(ListExtractor.InfoItemsPage result) {
|
||||
showListFooter(false);
|
||||
currentPage = Integer.parseInt(result.getNextItemsUrl());
|
||||
infoListAdapter.addInfoItemList(result.getNextItemsList());
|
||||
currentPage = Integer.parseInt(result.getNextPageUrl());
|
||||
infoListAdapter.addInfoItemList(result.getItems());
|
||||
|
||||
if (!result.getErrors().isEmpty()) {
|
||||
showSnackBarError(result.getErrors(), UserAction.SEARCHED, NewPipe.getNameOfService(serviceId)
|
||||
|
||||
@@ -63,24 +63,15 @@ public class SuggestionListAdapter extends RecyclerView.Adapter<SuggestionListAd
|
||||
public void onBindViewHolder(SuggestionItemHolder holder, int position) {
|
||||
final SuggestionItem currentItem = getItem(position);
|
||||
holder.updateFrom(currentItem);
|
||||
holder.queryView.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
if (listener != null) listener.onSuggestionItemSelected(currentItem);
|
||||
}
|
||||
holder.queryView.setOnClickListener(v -> {
|
||||
if (listener != null) listener.onSuggestionItemSelected(currentItem);
|
||||
});
|
||||
holder.queryView.setOnLongClickListener(new View.OnLongClickListener() {
|
||||
@Override
|
||||
public boolean onLongClick(View v) {
|
||||
holder.queryView.setOnLongClickListener(v -> {
|
||||
if (listener != null) listener.onSuggestionItemLongClick(currentItem);
|
||||
return true;
|
||||
}
|
||||
});
|
||||
holder.insertView.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
if (listener != null) listener.onSuggestionItemInserted(currentItem);
|
||||
}
|
||||
holder.insertView.setOnClickListener(v -> {
|
||||
if (listener != null) listener.onSuggestionItemInserted(currentItem);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -1,21 +0,0 @@
|
||||
package org.schabi.newpipe.fragments.local.bookmark;
|
||||
|
||||
import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.database.stream.StreamStatisticsEntry;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
public final class LastPlayedFragment extends StatisticsPlaylistFragment {
|
||||
@Override
|
||||
protected String getName() {
|
||||
return getString(R.string.title_last_played);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected List<StreamStatisticsEntry> processResult(List<StreamStatisticsEntry> results) {
|
||||
Collections.sort(results, (left, right) ->
|
||||
right.latestAccessDate.compareTo(left.latestAccessDate));
|
||||
return results;
|
||||
}
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
package org.schabi.newpipe.fragments.local.bookmark;
|
||||
|
||||
import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.database.stream.StreamStatisticsEntry;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
public final class MostPlayedFragment extends StatisticsPlaylistFragment {
|
||||
@Override
|
||||
protected String getName() {
|
||||
return getString(R.string.title_most_played);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected List<StreamStatisticsEntry> processResult(List<StreamStatisticsEntry> results) {
|
||||
Collections.sort(results, (left, right) ->
|
||||
((Long) right.watchCount).compareTo(left.watchCount));
|
||||
return results;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,252 +0,0 @@
|
||||
package org.schabi.newpipe.fragments.subscription;
|
||||
|
||||
import android.content.Context;
|
||||
import android.os.Bundle;
|
||||
import android.os.Parcelable;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.support.v7.widget.LinearLayoutManager;
|
||||
import android.support.v7.widget.RecyclerView;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
|
||||
import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.database.subscription.SubscriptionEntity;
|
||||
import org.schabi.newpipe.extractor.InfoItem;
|
||||
import org.schabi.newpipe.extractor.channel.ChannelInfoItem;
|
||||
import org.schabi.newpipe.fragments.BaseStateFragment;
|
||||
import org.schabi.newpipe.info_list.InfoListAdapter;
|
||||
import org.schabi.newpipe.report.UserAction;
|
||||
import org.schabi.newpipe.util.NavigationHelper;
|
||||
import org.schabi.newpipe.util.OnClickGesture;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
|
||||
import icepick.State;
|
||||
import io.reactivex.Observer;
|
||||
import io.reactivex.android.schedulers.AndroidSchedulers;
|
||||
import io.reactivex.disposables.CompositeDisposable;
|
||||
import io.reactivex.disposables.Disposable;
|
||||
import io.reactivex.schedulers.Schedulers;
|
||||
|
||||
import static org.schabi.newpipe.util.AnimationUtils.animateView;
|
||||
|
||||
public class SubscriptionFragment extends BaseStateFragment<List<SubscriptionEntity>> {
|
||||
private View headerRootLayout;
|
||||
|
||||
private InfoListAdapter infoListAdapter;
|
||||
private RecyclerView itemsList;
|
||||
|
||||
@State
|
||||
protected Parcelable itemsListState;
|
||||
|
||||
/* Used for independent events */
|
||||
private CompositeDisposable disposables = new CompositeDisposable();
|
||||
private SubscriptionService subscriptionService;
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////
|
||||
// Fragment LifeCycle
|
||||
///////////////////////////////////////////////////////////////////////////
|
||||
|
||||
|
||||
@Override
|
||||
public void setUserVisibleHint(boolean isVisibleToUser) {
|
||||
super.setUserVisibleHint(isVisibleToUser);
|
||||
if(isVisibleToUser && activity != null) {
|
||||
activity.getSupportActionBar()
|
||||
.setTitle(R.string.tab_subscriptions);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public void onAttach(Context context) {
|
||||
super.onAttach(context);
|
||||
infoListAdapter = new InfoListAdapter(activity);
|
||||
subscriptionService = SubscriptionService.getInstance();
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, Bundle savedInstanceState) {
|
||||
activity.getSupportActionBar().setDisplayShowTitleEnabled(true);
|
||||
activity.setTitle(R.string.tab_subscriptions);
|
||||
if(useAsFrontPage) {
|
||||
activity.getSupportActionBar().setDisplayHomeAsUpEnabled(false);
|
||||
}
|
||||
return inflater.inflate(R.layout.fragment_subscription, container, false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPause() {
|
||||
super.onPause();
|
||||
itemsListState = itemsList.getLayoutManager().onSaveInstanceState();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroyView() {
|
||||
if (disposables != null) disposables.clear();
|
||||
|
||||
super.onDestroyView();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroy() {
|
||||
if (disposables != null) disposables.dispose();
|
||||
disposables = null;
|
||||
subscriptionService = null;
|
||||
|
||||
super.onDestroy();
|
||||
}
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////
|
||||
// Fragment Views
|
||||
///////////////////////////////////////////////////////////////////////////
|
||||
|
||||
@Override
|
||||
protected void initViews(View rootView, Bundle savedInstanceState) {
|
||||
super.initViews(rootView, savedInstanceState);
|
||||
|
||||
infoListAdapter = new InfoListAdapter(getActivity());
|
||||
itemsList = rootView.findViewById(R.id.items_list);
|
||||
itemsList.setLayoutManager(new LinearLayoutManager(activity));
|
||||
|
||||
infoListAdapter.setHeader(headerRootLayout = activity.getLayoutInflater().inflate(R.layout.subscription_header, itemsList, false));
|
||||
infoListAdapter.useMiniItemVariants(true);
|
||||
|
||||
itemsList.setAdapter(infoListAdapter);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void initListeners() {
|
||||
super.initListeners();
|
||||
|
||||
infoListAdapter.setOnChannelSelectedListener(new OnClickGesture<ChannelInfoItem>() {
|
||||
@Override
|
||||
public void selected(ChannelInfoItem selectedItem) {
|
||||
// Requires the parent fragment to find holder for fragment replacement
|
||||
NavigationHelper.openChannelFragment(getParentFragment().getFragmentManager(),
|
||||
selectedItem.getServiceId(), selectedItem.url, selectedItem.getName());
|
||||
}
|
||||
});
|
||||
|
||||
headerRootLayout.setOnClickListener(view ->
|
||||
NavigationHelper.openWhatsNewFragment(getParentFragment().getFragmentManager()));
|
||||
}
|
||||
|
||||
private void resetFragment() {
|
||||
if (disposables != null) disposables.clear();
|
||||
if (infoListAdapter != null) infoListAdapter.clearStreamItemList();
|
||||
}
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////
|
||||
// Subscriptions Loader
|
||||
///////////////////////////////////////////////////////////////////////////
|
||||
|
||||
@Override
|
||||
public void startLoading(boolean forceLoad) {
|
||||
super.startLoading(forceLoad);
|
||||
resetFragment();
|
||||
|
||||
subscriptionService.getSubscription().toObservable()
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(getSubscriptionObserver());
|
||||
}
|
||||
|
||||
private Observer<List<SubscriptionEntity>> getSubscriptionObserver() {
|
||||
return new Observer<List<SubscriptionEntity>>() {
|
||||
@Override
|
||||
public void onSubscribe(Disposable d) {
|
||||
showLoading();
|
||||
disposables.add(d);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onNext(List<SubscriptionEntity> subscriptions) {
|
||||
handleResult(subscriptions);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(Throwable exception) {
|
||||
SubscriptionFragment.this.onError(exception);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onComplete() {
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleResult(@NonNull List<SubscriptionEntity> result) {
|
||||
super.handleResult(result);
|
||||
|
||||
infoListAdapter.clearStreamItemList();
|
||||
|
||||
if (result.isEmpty()) {
|
||||
showEmptyState();
|
||||
} else {
|
||||
infoListAdapter.addInfoItemList(getSubscriptionItems(result));
|
||||
if (itemsListState != null) {
|
||||
itemsList.getLayoutManager().onRestoreInstanceState(itemsListState);
|
||||
itemsListState = null;
|
||||
}
|
||||
|
||||
hideLoading();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private List<InfoItem> getSubscriptionItems(List<SubscriptionEntity> subscriptions) {
|
||||
List<InfoItem> items = new ArrayList<>();
|
||||
for (final SubscriptionEntity subscription : subscriptions) items.add(subscription.toChannelInfoItem());
|
||||
|
||||
Collections.sort(items, new Comparator<InfoItem>() {
|
||||
@Override
|
||||
public int compare(InfoItem o1, InfoItem o2) {
|
||||
return o1.name.compareToIgnoreCase(o2.name);
|
||||
}
|
||||
});
|
||||
return items;
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Contract
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
@Override
|
||||
public void showLoading() {
|
||||
super.showLoading();
|
||||
animateView(itemsList, false, 100);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void hideLoading() {
|
||||
super.hideLoading();
|
||||
animateView(itemsList, true, 200);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void showEmptyState() {
|
||||
super.showEmptyState();
|
||||
animateView(itemsList, false, 200);
|
||||
}
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////
|
||||
// Fragment Error Handling
|
||||
///////////////////////////////////////////////////////////////////////////
|
||||
|
||||
@Override
|
||||
protected boolean onError(Throwable exception) {
|
||||
resetFragment();
|
||||
if (super.onError(exception)) return true;
|
||||
|
||||
onUnrecoverableError(exception, UserAction.SOMETHING_ELSE, "none", "Subscriptions", R.string.general_error);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -1,141 +0,0 @@
|
||||
package org.schabi.newpipe.history;
|
||||
|
||||
import android.content.Intent;
|
||||
import android.os.Bundle;
|
||||
import android.support.design.widget.FloatingActionButton;
|
||||
import android.support.design.widget.TabLayout;
|
||||
import android.support.v4.app.Fragment;
|
||||
import android.support.v4.app.FragmentManager;
|
||||
import android.support.v4.app.FragmentPagerAdapter;
|
||||
import android.support.v4.app.FragmentStatePagerAdapter;
|
||||
import android.support.v4.view.ViewPager;
|
||||
import android.support.v7.app.AppCompatActivity;
|
||||
import android.support.v7.widget.Toolbar;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuItem;
|
||||
|
||||
import com.jakewharton.rxbinding2.view.RxView;
|
||||
|
||||
import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.settings.SettingsActivity;
|
||||
import org.schabi.newpipe.util.ThemeHelper;
|
||||
|
||||
import io.reactivex.android.schedulers.AndroidSchedulers;
|
||||
|
||||
public class HistoryActivity extends AppCompatActivity {
|
||||
|
||||
private static final String TAG = "HistoryActivity";
|
||||
/**
|
||||
* The {@link android.support.v4.view.PagerAdapter} 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 android.support.v4.app.FragmentStatePagerAdapter}.
|
||||
*/
|
||||
private SectionsPagerAdapter mSectionsPagerAdapter;
|
||||
|
||||
/**
|
||||
* The {@link ViewPager} that will host the section contents.
|
||||
*/
|
||||
private ViewPager mViewPager;
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
ThemeHelper.setTheme(this);
|
||||
setContentView(R.layout.activity_history);
|
||||
|
||||
Toolbar toolbar = findViewById(R.id.toolbar);
|
||||
setSupportActionBar(toolbar);
|
||||
if (getSupportActionBar() != null) {
|
||||
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
|
||||
getSupportActionBar().setTitle(R.string.title_activity_history);
|
||||
}
|
||||
// Create the adapter that will return a fragment for each of the three
|
||||
// primary sections of the activity.
|
||||
mSectionsPagerAdapter = new SectionsPagerAdapter(getSupportFragmentManager());
|
||||
|
||||
// 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 FloatingActionButton fab = findViewById(R.id.fab);
|
||||
RxView.clicks(fab)
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(ignored -> {
|
||||
int currentItem = mViewPager.getCurrentItem();
|
||||
HistoryFragment fragment = (HistoryFragment) mSectionsPagerAdapter
|
||||
.instantiateItem(mViewPager, currentItem);
|
||||
fragment.onHistoryCleared();
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onCreateOptionsMenu(Menu menu) {
|
||||
// Inflate the menu; this adds items to the action bar if it is present.
|
||||
getMenuInflater().inflate(R.menu.menu_history, menu);
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(MenuItem item) {
|
||||
switch (item.getItemId()) {
|
||||
case android.R.id.home:
|
||||
finish();
|
||||
return true;
|
||||
case R.id.action_settings:
|
||||
Intent intent = new Intent(this, SettingsActivity.class);
|
||||
startActivity(intent);
|
||||
return true;
|
||||
}
|
||||
return super.onOptionsItemSelected(item);
|
||||
}
|
||||
|
||||
/**
|
||||
* A {@link FragmentPagerAdapter} that returns a fragment corresponding to
|
||||
* one of the sections/tabs/pages.
|
||||
*/
|
||||
public class SectionsPagerAdapter extends FragmentStatePagerAdapter {
|
||||
|
||||
public SectionsPagerAdapter(FragmentManager fm) {
|
||||
super(fm);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Fragment getItem(int position) {
|
||||
Fragment fragment;
|
||||
switch (position) {
|
||||
case 0:
|
||||
fragment = SearchHistoryFragment.newInstance();
|
||||
break;
|
||||
case 1:
|
||||
fragment = WatchHistoryFragment.newInstance();
|
||||
break;
|
||||
default:
|
||||
throw new IllegalArgumentException("position: " + position);
|
||||
}
|
||||
return fragment;
|
||||
}
|
||||
|
||||
@Override
|
||||
public CharSequence getPageTitle(int position) {
|
||||
switch (position) {
|
||||
case 0:
|
||||
return getString(R.string.title_history_search);
|
||||
case 1:
|
||||
return getString(R.string.title_history_view);
|
||||
}
|
||||
throw new IllegalArgumentException("position: " + position);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getCount() {
|
||||
// Show 3 total pages.
|
||||
return 2;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,286 +0,0 @@
|
||||
package org.schabi.newpipe.history;
|
||||
|
||||
|
||||
import android.content.SharedPreferences;
|
||||
import android.os.Bundle;
|
||||
import android.os.Parcelable;
|
||||
import android.preference.PreferenceManager;
|
||||
import android.support.annotation.CallSuper;
|
||||
import android.support.annotation.MainThread;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.support.annotation.StringRes;
|
||||
import android.support.design.widget.Snackbar;
|
||||
import android.support.v7.app.AlertDialog;
|
||||
import android.support.v7.widget.LinearLayoutManager;
|
||||
import android.support.v7.widget.RecyclerView;
|
||||
import android.util.Log;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
|
||||
import org.reactivestreams.Subscriber;
|
||||
import org.reactivestreams.Subscription;
|
||||
import org.schabi.newpipe.BaseFragment;
|
||||
import org.schabi.newpipe.R;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
|
||||
import icepick.State;
|
||||
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 static org.schabi.newpipe.util.AnimationUtils.animateView;
|
||||
|
||||
public abstract class HistoryFragment<E> extends BaseFragment
|
||||
implements HistoryEntryAdapter.OnHistoryItemClickListener<E> {
|
||||
|
||||
private SharedPreferences mSharedPreferences;
|
||||
private String mHistoryIsEnabledKey;
|
||||
private boolean mHistoryIsEnabled;
|
||||
private HistoryIsEnabledChangeListener mHistoryIsEnabledChangeListener;
|
||||
|
||||
private View mDisabledView;
|
||||
private View mEmptyHistoryView;
|
||||
|
||||
@State
|
||||
Parcelable mRecyclerViewState;
|
||||
private RecyclerView mRecyclerView;
|
||||
private HistoryEntryAdapter<E, ? extends RecyclerView.ViewHolder> mHistoryAdapter;
|
||||
|
||||
private Subscription historySubscription;
|
||||
|
||||
protected HistoryRecordManager historyRecordManager;
|
||||
protected CompositeDisposable disposables;
|
||||
|
||||
@StringRes
|
||||
abstract int getEnabledConfigKey();
|
||||
|
||||
@CallSuper
|
||||
@Override
|
||||
public void onCreate(@Nullable Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
mHistoryIsEnabledKey = getString(getEnabledConfigKey());
|
||||
|
||||
mSharedPreferences = PreferenceManager.getDefaultSharedPreferences(getContext());
|
||||
// Read history enabled from preferences
|
||||
mHistoryIsEnabled = isHistoryEnabled();
|
||||
// Register history enabled listener
|
||||
mSharedPreferences.registerOnSharedPreferenceChangeListener(mHistoryIsEnabledChangeListener);
|
||||
|
||||
historyRecordManager = new HistoryRecordManager(getContext());
|
||||
disposables = new CompositeDisposable();
|
||||
}
|
||||
|
||||
@NonNull
|
||||
protected abstract HistoryEntryAdapter<E, ? extends RecyclerView.ViewHolder> createAdapter();
|
||||
|
||||
protected abstract Single<List<Long>> insert(final Collection<E> entries);
|
||||
|
||||
protected abstract Single<Integer> delete(final Collection<E> entries);
|
||||
|
||||
@NonNull
|
||||
protected abstract Flowable<List<E>> getAll();
|
||||
|
||||
@Override
|
||||
public void onResume() {
|
||||
super.onResume();
|
||||
|
||||
getAll().observeOn(AndroidSchedulers.mainThread()).subscribe(getHistorySubscriber());
|
||||
|
||||
final boolean newEnabled = isHistoryEnabled();
|
||||
if (newEnabled != mHistoryIsEnabled) {
|
||||
onHistoryIsEnabledChanged(newEnabled);
|
||||
}
|
||||
}
|
||||
|
||||
@NonNull
|
||||
private Subscriber<List<E>> getHistorySubscriber() {
|
||||
return new Subscriber<List<E>>() {
|
||||
@Override
|
||||
public void onSubscribe(Subscription s) {
|
||||
if (historySubscription != null) historySubscription.cancel();
|
||||
|
||||
historySubscription = s;
|
||||
historySubscription.request(1);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onNext(List<E> entries) {
|
||||
if (!entries.isEmpty()) {
|
||||
mHistoryAdapter.setEntries(entries);
|
||||
animateView(mEmptyHistoryView, false, 200);
|
||||
|
||||
if (mRecyclerViewState != null) {
|
||||
mRecyclerView.getLayoutManager().onRestoreInstanceState(mRecyclerViewState);
|
||||
mRecyclerViewState = null;
|
||||
}
|
||||
} else {
|
||||
mHistoryAdapter.clear();
|
||||
showEmptyHistory();
|
||||
}
|
||||
|
||||
if (historySubscription != null) historySubscription.request(1);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(Throwable t) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onComplete() {
|
||||
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private boolean isHistoryEnabled() {
|
||||
return mSharedPreferences.getBoolean(mHistoryIsEnabledKey, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when the history is cleared to update the views
|
||||
*/
|
||||
@MainThread
|
||||
public void onHistoryCleared() {
|
||||
if (getContext() == null) return;
|
||||
|
||||
new AlertDialog.Builder(getContext())
|
||||
.setTitle(R.string.delete_all)
|
||||
.setMessage(R.string.delete_all_history_prompt)
|
||||
.setCancelable(true)
|
||||
.setNegativeButton(R.string.cancel, null)
|
||||
.setPositiveButton(R.string.delete_all, (dialog, i) -> clearHistory())
|
||||
.show();
|
||||
}
|
||||
|
||||
protected void makeSnackbar(@StringRes final int text) {
|
||||
if (getActivity() == null) return;
|
||||
|
||||
View view = getActivity().findViewById(R.id.main_content);
|
||||
if (view == null) view = mRecyclerView.getRootView();
|
||||
Snackbar.make(view, text, Snackbar.LENGTH_LONG).show();
|
||||
}
|
||||
|
||||
private void clearHistory() {
|
||||
final Collection<E> itemsToDelete = new ArrayList<>(mHistoryAdapter.getItems());
|
||||
|
||||
final Disposable deletion = delete(itemsToDelete)
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(
|
||||
ignored -> Log.d(TAG, "Clear history deleted [" +
|
||||
itemsToDelete.size() + "] items."),
|
||||
error -> Log.e(TAG, "Clear history delete step failed", error)
|
||||
);
|
||||
|
||||
final Disposable cleanUp = historyRecordManager.removeOrphanedRecords()
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(
|
||||
ignored -> Log.d(TAG, "Clear history deleted orphaned stream records"),
|
||||
error -> Log.e(TAG, "Clear history remove orphaned records failed", error)
|
||||
);
|
||||
|
||||
disposables.addAll(deletion, cleanUp);
|
||||
|
||||
makeSnackbar(R.string.history_cleared);
|
||||
mHistoryAdapter.clear();
|
||||
showEmptyHistory();
|
||||
}
|
||||
|
||||
private void showEmptyHistory() {
|
||||
if (mHistoryIsEnabled) {
|
||||
animateView(mEmptyHistoryView, true, 200);
|
||||
}
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@CallSuper
|
||||
@Override
|
||||
public View onCreateView(@NonNull LayoutInflater inflater,
|
||||
@Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
|
||||
View rootView = inflater.inflate(R.layout.fragment_history, container, false);
|
||||
mRecyclerView = rootView.findViewById(R.id.history_view);
|
||||
|
||||
RecyclerView.LayoutManager layoutManager = new LinearLayoutManager(getContext(),
|
||||
LinearLayoutManager.VERTICAL, false);
|
||||
mRecyclerView.setLayoutManager(layoutManager);
|
||||
|
||||
mHistoryAdapter = createAdapter();
|
||||
mHistoryAdapter.setOnHistoryItemClickListener(this);
|
||||
mRecyclerView.setAdapter(mHistoryAdapter);
|
||||
mDisabledView = rootView.findViewById(R.id.history_disabled_view);
|
||||
mEmptyHistoryView = rootView.findViewById(R.id.history_empty);
|
||||
|
||||
if (mHistoryIsEnabled) {
|
||||
mRecyclerView.setVisibility(View.VISIBLE);
|
||||
} else {
|
||||
mRecyclerView.setVisibility(View.GONE);
|
||||
mDisabledView.setVisibility(View.VISIBLE);
|
||||
}
|
||||
|
||||
return rootView;
|
||||
}
|
||||
|
||||
@CallSuper
|
||||
@Override
|
||||
public void onDestroy() {
|
||||
super.onDestroy();
|
||||
|
||||
if (disposables != null) disposables.dispose();
|
||||
if (historySubscription != null) historySubscription.cancel();
|
||||
|
||||
mSharedPreferences.unregisterOnSharedPreferenceChangeListener(mHistoryIsEnabledChangeListener);
|
||||
mSharedPreferences = null;
|
||||
mHistoryIsEnabledChangeListener = null;
|
||||
mHistoryIsEnabledKey = null;
|
||||
historySubscription = null;
|
||||
disposables = null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPause() {
|
||||
super.onPause();
|
||||
mRecyclerViewState = mRecyclerView.getLayoutManager().onSaveInstanceState();
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when history enabled flag is changed.
|
||||
*
|
||||
* @param historyIsEnabled the new value
|
||||
*/
|
||||
@CallSuper
|
||||
public void onHistoryIsEnabledChanged(boolean historyIsEnabled) {
|
||||
mHistoryIsEnabled = historyIsEnabled;
|
||||
if (historyIsEnabled) {
|
||||
animateView(mRecyclerView, true, 300);
|
||||
animateView(mDisabledView, false, 300);
|
||||
if (mHistoryAdapter.isEmpty()) {
|
||||
animateView(mEmptyHistoryView, true, 300);
|
||||
}
|
||||
} else {
|
||||
animateView(mRecyclerView, false, 300);
|
||||
animateView(mDisabledView, true, 300);
|
||||
animateView(mEmptyHistoryView, false, 300);
|
||||
}
|
||||
}
|
||||
|
||||
private class HistoryIsEnabledChangeListener
|
||||
implements SharedPreferences.OnSharedPreferenceChangeListener {
|
||||
@Override
|
||||
public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) {
|
||||
if (key.equals(mHistoryIsEnabledKey)) {
|
||||
boolean enabled = sharedPreferences.getBoolean(key, false);
|
||||
if (mHistoryIsEnabled != enabled) {
|
||||
onHistoryIsEnabledChanged(enabled);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,145 +0,0 @@
|
||||
package org.schabi.newpipe.history;
|
||||
|
||||
import android.content.Context;
|
||||
import android.os.Bundle;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.support.annotation.StringRes;
|
||||
import android.support.v7.app.AlertDialog;
|
||||
import android.support.v7.widget.RecyclerView;
|
||||
import android.util.Log;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.TextView;
|
||||
|
||||
import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.database.history.model.SearchHistoryEntry;
|
||||
import org.schabi.newpipe.extractor.NewPipe;
|
||||
import org.schabi.newpipe.util.Localization;
|
||||
import org.schabi.newpipe.util.NavigationHelper;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
import io.reactivex.Flowable;
|
||||
import io.reactivex.Single;
|
||||
import io.reactivex.android.schedulers.AndroidSchedulers;
|
||||
import io.reactivex.disposables.Disposable;
|
||||
|
||||
public class SearchHistoryFragment extends HistoryFragment<SearchHistoryEntry> {
|
||||
|
||||
@NonNull
|
||||
public static SearchHistoryFragment newInstance() {
|
||||
return new SearchHistoryFragment();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate(@Nullable Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
protected SearchHistoryAdapter createAdapter() {
|
||||
return new SearchHistoryAdapter(getContext());
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Single<List<Long>> insert(Collection<SearchHistoryEntry> entries) {
|
||||
return historyRecordManager.insertSearches(entries);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Single<Integer> delete(Collection<SearchHistoryEntry> entries) {
|
||||
return historyRecordManager.deleteSearches(entries);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
protected Flowable<List<SearchHistoryEntry>> getAll() {
|
||||
return historyRecordManager.getSearchHistory();
|
||||
}
|
||||
|
||||
@StringRes
|
||||
@Override
|
||||
int getEnabledConfigKey() {
|
||||
return R.string.enable_search_history_key;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onHistoryItemClick(final SearchHistoryEntry historyItem) {
|
||||
NavigationHelper.openSearch(getContext(), historyItem.getServiceId(),
|
||||
historyItem.getSearch());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onHistoryItemLongClick(final SearchHistoryEntry item) {
|
||||
if (activity == null) return;
|
||||
|
||||
new AlertDialog.Builder(activity)
|
||||
.setTitle(item.getSearch())
|
||||
.setMessage(R.string.delete_item_search_history)
|
||||
.setCancelable(true)
|
||||
.setNeutralButton(R.string.cancel, null)
|
||||
.setPositiveButton(R.string.delete_one, (dialog, i) -> {
|
||||
final Disposable onDelete = historyRecordManager
|
||||
.deleteSearches(Collections.singleton(item))
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(
|
||||
ignored -> {/*successful*/},
|
||||
error -> Log.e(TAG, "Search history Delete One failed:", error)
|
||||
);
|
||||
disposables.add(onDelete);
|
||||
makeSnackbar(R.string.item_deleted);
|
||||
})
|
||||
.setNegativeButton(R.string.delete_all, (dialog, i) -> {
|
||||
final Disposable onDeleteAll = historyRecordManager
|
||||
.deleteSearchHistory(item.getSearch())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(
|
||||
ignored -> {/*successful*/},
|
||||
error -> Log.e(TAG, "Search history Delete All failed:", error)
|
||||
);
|
||||
disposables.add(onDeleteAll);
|
||||
makeSnackbar(R.string.item_deleted);
|
||||
})
|
||||
.show();
|
||||
}
|
||||
|
||||
private static class ViewHolder extends RecyclerView.ViewHolder {
|
||||
private final TextView search;
|
||||
private final TextView info;
|
||||
|
||||
public ViewHolder(View itemView) {
|
||||
super(itemView);
|
||||
search = itemView.findViewById(R.id.search);
|
||||
info = itemView.findViewById(R.id.info);
|
||||
}
|
||||
}
|
||||
|
||||
protected class SearchHistoryAdapter extends HistoryEntryAdapter<SearchHistoryEntry, ViewHolder> {
|
||||
|
||||
SearchHistoryAdapter(Context context) {
|
||||
super(context);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
|
||||
LayoutInflater inflater = LayoutInflater.from(parent.getContext());
|
||||
View rootView = inflater.inflate(R.layout.item_search_history, parent, false);
|
||||
return new ViewHolder(rootView);
|
||||
}
|
||||
|
||||
@Override
|
||||
void onBindViewHolder(ViewHolder holder, SearchHistoryEntry entry, int position) {
|
||||
holder.search.setText(entry.getSearch());
|
||||
|
||||
final String info = Localization.concatenateStrings(
|
||||
getFormattedDate(entry.getCreationDate()),
|
||||
NewPipe.getNameOfService(entry.getServiceId()));
|
||||
holder.info.setText(info);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,170 +0,0 @@
|
||||
package org.schabi.newpipe.history;
|
||||
|
||||
|
||||
import android.content.Context;
|
||||
import android.os.Bundle;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.support.annotation.StringRes;
|
||||
import android.support.v7.app.AlertDialog;
|
||||
import android.support.v7.widget.RecyclerView;
|
||||
import android.util.Log;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.TextView;
|
||||
|
||||
import com.nostra13.universalimageloader.core.ImageLoader;
|
||||
|
||||
import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.database.history.model.StreamHistoryEntry;
|
||||
import org.schabi.newpipe.info_list.holder.StreamInfoItemHolder;
|
||||
import org.schabi.newpipe.util.Localization;
|
||||
import org.schabi.newpipe.util.NavigationHelper;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
import io.reactivex.Flowable;
|
||||
import io.reactivex.Single;
|
||||
import io.reactivex.android.schedulers.AndroidSchedulers;
|
||||
import io.reactivex.disposables.Disposable;
|
||||
|
||||
|
||||
public class WatchHistoryFragment extends HistoryFragment<StreamHistoryEntry> {
|
||||
|
||||
@NonNull
|
||||
public static WatchHistoryFragment newInstance() {
|
||||
return new WatchHistoryFragment();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate(@Nullable Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
}
|
||||
|
||||
@StringRes
|
||||
@Override
|
||||
int getEnabledConfigKey() {
|
||||
return R.string.enable_watch_history_key;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
protected StreamHistoryAdapter createAdapter() {
|
||||
return new StreamHistoryAdapter(getContext());
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Single<List<Long>> insert(Collection<StreamHistoryEntry> entries) {
|
||||
return historyRecordManager.insertStreamHistory(entries);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Single<Integer> delete(Collection<StreamHistoryEntry> entries) {
|
||||
return historyRecordManager.deleteStreamHistory(entries);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
protected Flowable<List<StreamHistoryEntry>> getAll() {
|
||||
return historyRecordManager.getStreamHistory();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onHistoryItemClick(StreamHistoryEntry historyItem) {
|
||||
NavigationHelper.openVideoDetail(getContext(), historyItem.serviceId, historyItem.url,
|
||||
historyItem.title);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onHistoryItemLongClick(StreamHistoryEntry item) {
|
||||
new AlertDialog.Builder(activity)
|
||||
.setTitle(item.title)
|
||||
.setMessage(R.string.delete_stream_history_prompt)
|
||||
.setCancelable(true)
|
||||
.setNeutralButton(R.string.cancel, null)
|
||||
.setPositiveButton(R.string.delete_one, (dialog, i) -> {
|
||||
final Disposable onDelete = historyRecordManager
|
||||
.deleteStreamHistory(Collections.singleton(item))
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(
|
||||
ignored -> {/*successful*/},
|
||||
error -> Log.e(TAG, "Watch history Delete One failed:", error)
|
||||
);
|
||||
disposables.add(onDelete);
|
||||
makeSnackbar(R.string.item_deleted);
|
||||
})
|
||||
.setNegativeButton(R.string.delete_all, (dialog, i) -> {
|
||||
final Disposable onDeleteAll = historyRecordManager
|
||||
.deleteStreamHistory(item.streamId)
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(
|
||||
ignored -> {/*successful*/},
|
||||
error -> Log.e(TAG, "Watch history Delete All failed:", error)
|
||||
);
|
||||
disposables.add(onDeleteAll);
|
||||
makeSnackbar(R.string.item_deleted);
|
||||
})
|
||||
.show();
|
||||
}
|
||||
|
||||
private static class StreamHistoryAdapter extends HistoryEntryAdapter<StreamHistoryEntry, ViewHolder> {
|
||||
|
||||
StreamHistoryAdapter(Context context) {
|
||||
super(context);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
|
||||
LayoutInflater inflater = LayoutInflater.from(parent.getContext());
|
||||
View itemView = inflater.inflate(R.layout.list_stream_item, parent, false);
|
||||
return new ViewHolder(itemView);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onViewRecycled(ViewHolder holder) {
|
||||
holder.itemView.setOnClickListener(null);
|
||||
ImageLoader.getInstance()
|
||||
.cancelDisplayTask(holder.thumbnailView);
|
||||
}
|
||||
|
||||
@Override
|
||||
void onBindViewHolder(ViewHolder holder, StreamHistoryEntry entry, int position) {
|
||||
final String formattedDate = getFormattedDate(entry.accessDate);
|
||||
final String info;
|
||||
if (entry.repeatCount > 1) {
|
||||
info = Localization.concatenateStrings(formattedDate,
|
||||
getFormattedViewString(entry.repeatCount));
|
||||
} else {
|
||||
info = formattedDate;
|
||||
}
|
||||
|
||||
holder.info.setText(info);
|
||||
holder.streamTitle.setText(entry.title);
|
||||
holder.uploader.setText(entry.uploader);
|
||||
holder.duration.setText(Localization.getDurationString(entry.duration));
|
||||
ImageLoader.getInstance().displayImage(entry.thumbnailUrl, holder.thumbnailView,
|
||||
StreamInfoItemHolder.DISPLAY_THUMBNAIL_OPTIONS);
|
||||
}
|
||||
}
|
||||
|
||||
private static class ViewHolder extends RecyclerView.ViewHolder {
|
||||
private final TextView info;
|
||||
private final TextView streamTitle;
|
||||
private final ImageView thumbnailView;
|
||||
private final TextView uploader;
|
||||
private final TextView duration;
|
||||
|
||||
public ViewHolder(View itemView) {
|
||||
super(itemView);
|
||||
thumbnailView = itemView.findViewById(R.id.itemThumbnailView);
|
||||
info = itemView.findViewById(R.id.itemAdditionalDetails);
|
||||
streamTitle = itemView.findViewById(R.id.itemVideoTitleView);
|
||||
uploader = itemView.findViewById(R.id.itemUploaderView);
|
||||
duration = itemView.findViewById(R.id.itemDurationView);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -60,7 +60,7 @@ public class InfoItemBuilder {
|
||||
}
|
||||
|
||||
public View buildView(@NonNull ViewGroup parent, @NonNull final InfoItem infoItem, boolean useMiniVariant) {
|
||||
InfoItemHolder holder = holderFromInfoType(parent, infoItem.info_type, useMiniVariant);
|
||||
InfoItemHolder holder = holderFromInfoType(parent, infoItem.getInfoType(), useMiniVariant);
|
||||
holder.updateFromItem(infoItem);
|
||||
return holder.itemView;
|
||||
}
|
||||
|
||||
@@ -17,6 +17,7 @@ import org.schabi.newpipe.info_list.holder.PlaylistInfoItemHolder;
|
||||
import org.schabi.newpipe.info_list.holder.PlaylistMiniInfoItemHolder;
|
||||
import org.schabi.newpipe.info_list.holder.StreamInfoItemHolder;
|
||||
import org.schabi.newpipe.info_list.holder.StreamMiniInfoItemHolder;
|
||||
import org.schabi.newpipe.util.FallbackViewHolder;
|
||||
import org.schabi.newpipe.util.OnClickGesture;
|
||||
|
||||
import java.util.ArrayList;
|
||||
@@ -203,7 +204,7 @@ public class InfoListAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolde
|
||||
return FOOTER_TYPE;
|
||||
}
|
||||
final InfoItem item = infoItemList.get(position);
|
||||
switch (item.info_type) {
|
||||
switch (item.getInfoType()) {
|
||||
case STREAM:
|
||||
return useMiniVariant ? MINI_STREAM_HOLDER_TYPE : STREAM_HOLDER_TYPE;
|
||||
case CHANNEL:
|
||||
@@ -238,7 +239,7 @@ public class InfoListAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolde
|
||||
return new PlaylistInfoItemHolder(infoItemBuilder, parent);
|
||||
default:
|
||||
Log.e(TAG, "Trollolo");
|
||||
return null;
|
||||
return new FallbackViewHolder(new View(parent.getContext()));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -44,15 +44,16 @@ public class ChannelInfoItemHolder extends ChannelMiniInfoItemHolder {
|
||||
if (!(infoItem instanceof ChannelInfoItem)) return;
|
||||
final ChannelInfoItem item = (ChannelInfoItem) infoItem;
|
||||
|
||||
itemChannelDescriptionView.setText(item.description);
|
||||
itemChannelDescriptionView.setText(item.getDescription());
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String getDetailLine(final ChannelInfoItem item) {
|
||||
String details = super.getDetailLine(item);
|
||||
|
||||
if (item.stream_count >= 0) {
|
||||
String formattedVideoAmount = Localization.localizeStreamCount(itemBuilder.getContext(), item.stream_count);
|
||||
if (item.getStreamCount() >= 0) {
|
||||
String formattedVideoAmount = Localization.localizeStreamCount(itemBuilder.getContext(),
|
||||
item.getStreamCount());
|
||||
|
||||
if (!details.isEmpty()) {
|
||||
details += " • " + formattedVideoAmount;
|
||||
|
||||
@@ -1,15 +1,13 @@
|
||||
package org.schabi.newpipe.info_list.holder;
|
||||
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.TextView;
|
||||
|
||||
import com.nostra13.universalimageloader.core.DisplayImageOptions;
|
||||
|
||||
import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.extractor.InfoItem;
|
||||
import org.schabi.newpipe.extractor.channel.ChannelInfoItem;
|
||||
import org.schabi.newpipe.info_list.InfoItemBuilder;
|
||||
import org.schabi.newpipe.util.ImageDisplayConstants;
|
||||
import org.schabi.newpipe.util.Localization;
|
||||
|
||||
import de.hdodenhof.circleimageview.CircleImageView;
|
||||
@@ -40,34 +38,23 @@ public class ChannelMiniInfoItemHolder extends InfoItemHolder {
|
||||
itemAdditionalDetailView.setText(getDetailLine(item));
|
||||
|
||||
itemBuilder.getImageLoader()
|
||||
.displayImage(item.thumbnail_url, itemThumbnailView, ChannelInfoItemHolder.DISPLAY_THUMBNAIL_OPTIONS);
|
||||
.displayImage(item.getThumbnailUrl(),
|
||||
itemThumbnailView,
|
||||
ImageDisplayConstants.DISPLAY_THUMBNAIL_OPTIONS);
|
||||
|
||||
itemView.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View view) {
|
||||
if (itemBuilder.getOnChannelSelectedListener() != null) {
|
||||
itemBuilder.getOnChannelSelectedListener().selected(item);
|
||||
}
|
||||
itemView.setOnClickListener(view -> {
|
||||
if (itemBuilder.getOnChannelSelectedListener() != null) {
|
||||
itemBuilder.getOnChannelSelectedListener().selected(item);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
protected String getDetailLine(final ChannelInfoItem item) {
|
||||
String details = "";
|
||||
if (item.subscriber_count >= 0) {
|
||||
details += Localization.shortSubscriberCount(itemBuilder.getContext(), item.subscriber_count);
|
||||
if (item.getSubscriberCount() >= 0) {
|
||||
details += Localization.shortSubscriberCount(itemBuilder.getContext(),
|
||||
item.getSubscriberCount());
|
||||
}
|
||||
return details;
|
||||
}
|
||||
|
||||
/**
|
||||
* Display options for channel thumbnails
|
||||
*/
|
||||
public static final DisplayImageOptions DISPLAY_THUMBNAIL_OPTIONS =
|
||||
new DisplayImageOptions.Builder()
|
||||
.cloneFrom(BASE_DISPLAY_IMAGE_OPTIONS)
|
||||
.showImageOnLoading(R.drawable.buddy_channel_item)
|
||||
.showImageForEmptyUri(R.drawable.buddy_channel_item)
|
||||
.showImageOnFail(R.drawable.buddy_channel_item)
|
||||
.build();
|
||||
}
|
||||
|
||||
@@ -4,8 +4,6 @@ import android.support.v7.widget.RecyclerView;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.ViewGroup;
|
||||
|
||||
import com.nostra13.universalimageloader.core.DisplayImageOptions;
|
||||
|
||||
import org.schabi.newpipe.extractor.InfoItem;
|
||||
import org.schabi.newpipe.info_list.InfoItemBuilder;
|
||||
|
||||
@@ -38,16 +36,4 @@ public abstract class InfoItemHolder extends RecyclerView.ViewHolder {
|
||||
}
|
||||
|
||||
public abstract void updateFromItem(final InfoItem infoItem);
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// ImageLoaderOptions
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
/**
|
||||
* Base display options
|
||||
*/
|
||||
public static final DisplayImageOptions BASE_DISPLAY_IMAGE_OPTIONS =
|
||||
new DisplayImageOptions.Builder()
|
||||
.cacheInMemory(true)
|
||||
.build();
|
||||
}
|
||||
|
||||
@@ -4,12 +4,11 @@ import android.view.ViewGroup;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.TextView;
|
||||
|
||||
import com.nostra13.universalimageloader.core.DisplayImageOptions;
|
||||
|
||||
import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.extractor.InfoItem;
|
||||
import org.schabi.newpipe.extractor.playlist.PlaylistInfoItem;
|
||||
import org.schabi.newpipe.info_list.InfoItemBuilder;
|
||||
import org.schabi.newpipe.util.ImageDisplayConstants;
|
||||
|
||||
public class PlaylistMiniInfoItemHolder extends InfoItemHolder {
|
||||
public final ImageView itemThumbnailView;
|
||||
@@ -40,7 +39,8 @@ public class PlaylistMiniInfoItemHolder extends InfoItemHolder {
|
||||
itemUploaderView.setText(item.getUploaderName());
|
||||
|
||||
itemBuilder.getImageLoader()
|
||||
.displayImage(item.thumbnail_url, itemThumbnailView, DISPLAY_THUMBNAIL_OPTIONS);
|
||||
.displayImage(item.getThumbnailUrl(), itemThumbnailView,
|
||||
ImageDisplayConstants.DISPLAY_THUMBNAIL_OPTIONS);
|
||||
|
||||
itemView.setOnClickListener(view -> {
|
||||
if (itemBuilder.getOnPlaylistSelectedListener() != null) {
|
||||
@@ -56,15 +56,4 @@ public class PlaylistMiniInfoItemHolder extends InfoItemHolder {
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Display options for playlist thumbnails
|
||||
*/
|
||||
public static final DisplayImageOptions DISPLAY_THUMBNAIL_OPTIONS =
|
||||
new DisplayImageOptions.Builder()
|
||||
.cloneFrom(BASE_DISPLAY_IMAGE_OPTIONS)
|
||||
.showImageOnLoading(R.drawable.dummy_thumbnail_playlist)
|
||||
.showImageForEmptyUri(R.drawable.dummy_thumbnail_playlist)
|
||||
.showImageOnFail(R.drawable.dummy_thumbnail_playlist)
|
||||
.build();
|
||||
}
|
||||
|
||||
@@ -51,14 +51,14 @@ public class StreamInfoItemHolder extends StreamMiniInfoItemHolder {
|
||||
|
||||
private String getStreamInfoDetailLine(final StreamInfoItem infoItem) {
|
||||
String viewsAndDate = "";
|
||||
if (infoItem.view_count >= 0) {
|
||||
viewsAndDate = Localization.shortViewCount(itemBuilder.getContext(), infoItem.view_count);
|
||||
if (infoItem.getViewCount() >= 0) {
|
||||
viewsAndDate = Localization.shortViewCount(itemBuilder.getContext(), infoItem.getViewCount());
|
||||
}
|
||||
if (!TextUtils.isEmpty(infoItem.upload_date)) {
|
||||
if (!TextUtils.isEmpty(infoItem.getUploadDate())) {
|
||||
if (viewsAndDate.isEmpty()) {
|
||||
viewsAndDate = infoItem.upload_date;
|
||||
viewsAndDate = infoItem.getUploadDate();
|
||||
} else {
|
||||
viewsAndDate += " • " + infoItem.upload_date;
|
||||
viewsAndDate += " • " + infoItem.getUploadDate();
|
||||
}
|
||||
}
|
||||
return viewsAndDate;
|
||||
|
||||
@@ -6,13 +6,12 @@ import android.view.ViewGroup;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.TextView;
|
||||
|
||||
import com.nostra13.universalimageloader.core.DisplayImageOptions;
|
||||
|
||||
import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.extractor.InfoItem;
|
||||
import org.schabi.newpipe.extractor.stream.StreamInfoItem;
|
||||
import org.schabi.newpipe.extractor.stream.StreamType;
|
||||
import org.schabi.newpipe.info_list.InfoItemBuilder;
|
||||
import org.schabi.newpipe.util.ImageDisplayConstants;
|
||||
import org.schabi.newpipe.util.Localization;
|
||||
|
||||
public class StreamMiniInfoItemHolder extends InfoItemHolder {
|
||||
@@ -41,15 +40,17 @@ public class StreamMiniInfoItemHolder extends InfoItemHolder {
|
||||
final StreamInfoItem item = (StreamInfoItem) infoItem;
|
||||
|
||||
itemVideoTitleView.setText(item.getName());
|
||||
itemUploaderView.setText(item.uploader_name);
|
||||
itemUploaderView.setText(item.getUploaderName());
|
||||
|
||||
if (item.duration > 0) {
|
||||
itemDurationView.setText(Localization.getDurationString(item.duration));
|
||||
itemDurationView.setBackgroundColor(ContextCompat.getColor(itemBuilder.getContext(), R.color.duration_background_color));
|
||||
if (item.getDuration() > 0) {
|
||||
itemDurationView.setText(Localization.getDurationString(item.getDuration()));
|
||||
itemDurationView.setBackgroundColor(ContextCompat.getColor(itemBuilder.getContext(),
|
||||
R.color.duration_background_color));
|
||||
itemDurationView.setVisibility(View.VISIBLE);
|
||||
} else if (item.stream_type == StreamType.LIVE_STREAM) {
|
||||
} else if (item.getStreamType() == StreamType.LIVE_STREAM) {
|
||||
itemDurationView.setText(R.string.duration_live);
|
||||
itemDurationView.setBackgroundColor(ContextCompat.getColor(itemBuilder.getContext(), R.color.live_duration_background_color));
|
||||
itemDurationView.setBackgroundColor(ContextCompat.getColor(itemBuilder.getContext(),
|
||||
R.color.live_duration_background_color));
|
||||
itemDurationView.setVisibility(View.VISIBLE);
|
||||
} else {
|
||||
itemDurationView.setVisibility(View.GONE);
|
||||
@@ -57,25 +58,24 @@ public class StreamMiniInfoItemHolder extends InfoItemHolder {
|
||||
|
||||
// Default thumbnail is shown on error, while loading and if the url is empty
|
||||
itemBuilder.getImageLoader()
|
||||
.displayImage(item.thumbnail_url, itemThumbnailView, StreamInfoItemHolder.DISPLAY_THUMBNAIL_OPTIONS);
|
||||
.displayImage(item.getThumbnailUrl(),
|
||||
itemThumbnailView,
|
||||
ImageDisplayConstants.DISPLAY_THUMBNAIL_OPTIONS);
|
||||
|
||||
itemView.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View view) {
|
||||
if (itemBuilder.getOnStreamSelectedListener() != null) {
|
||||
itemBuilder.getOnStreamSelectedListener().selected(item);
|
||||
}
|
||||
itemView.setOnClickListener(view -> {
|
||||
if (itemBuilder.getOnStreamSelectedListener() != null) {
|
||||
itemBuilder.getOnStreamSelectedListener().selected(item);
|
||||
}
|
||||
});
|
||||
|
||||
switch (item.stream_type) {
|
||||
switch (item.getStreamType()) {
|
||||
case AUDIO_STREAM:
|
||||
case VIDEO_STREAM:
|
||||
case FILE:
|
||||
enableLongClick(item);
|
||||
break;
|
||||
case LIVE_STREAM:
|
||||
case AUDIO_LIVE_STREAM:
|
||||
enableLongClick(item);
|
||||
break;
|
||||
case FILE:
|
||||
case NONE:
|
||||
default:
|
||||
disableLongClick();
|
||||
@@ -85,14 +85,11 @@ public class StreamMiniInfoItemHolder extends InfoItemHolder {
|
||||
|
||||
private void enableLongClick(final StreamInfoItem item) {
|
||||
itemView.setLongClickable(true);
|
||||
itemView.setOnLongClickListener(new View.OnLongClickListener() {
|
||||
@Override
|
||||
public boolean onLongClick(View view) {
|
||||
if (itemBuilder.getOnStreamSelectedListener() != null) {
|
||||
itemBuilder.getOnStreamSelectedListener().held(item);
|
||||
}
|
||||
return true;
|
||||
itemView.setOnLongClickListener(view -> {
|
||||
if (itemBuilder.getOnStreamSelectedListener() != null) {
|
||||
itemBuilder.getOnStreamSelectedListener().held(item);
|
||||
}
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
@@ -100,15 +97,4 @@ public class StreamMiniInfoItemHolder extends InfoItemHolder {
|
||||
itemView.setLongClickable(false);
|
||||
itemView.setOnLongClickListener(null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Display options for stream thumbnails
|
||||
*/
|
||||
public static final DisplayImageOptions DISPLAY_THUMBNAIL_OPTIONS =
|
||||
new DisplayImageOptions.Builder()
|
||||
.cloneFrom(BASE_DISPLAY_IMAGE_OPTIONS)
|
||||
.showImageOnFail(R.drawable.dummy_thumbnail)
|
||||
.showImageForEmptyUri(R.drawable.dummy_thumbnail)
|
||||
.showImageOnLoading(R.drawable.dummy_thumbnail)
|
||||
.build();
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package org.schabi.newpipe.fragments.local.bookmark;
|
||||
package org.schabi.newpipe.local;
|
||||
|
||||
import android.os.Bundle;
|
||||
import android.support.v4.app.Fragment;
|
||||
@@ -13,7 +13,6 @@ import android.view.View;
|
||||
import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.fragments.BaseStateFragment;
|
||||
import org.schabi.newpipe.fragments.list.ListViewContract;
|
||||
import org.schabi.newpipe.fragments.local.LocalItemListAdapter;
|
||||
|
||||
import static org.schabi.newpipe.util.AnimationUtils.animateView;
|
||||
|
||||
@@ -151,7 +150,10 @@ public abstract class BaseLocalListFragment<I, N> extends BaseStateFragment<I>
|
||||
|
||||
@Override
|
||||
public void showListFooter(final boolean show) {
|
||||
itemsList.post(() -> itemListAdapter.showFooter(show));
|
||||
if (itemsList == null) return;
|
||||
itemsList.post(() -> {
|
||||
if (itemListAdapter != null) itemListAdapter.showFooter(show);
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -1,4 +1,4 @@
|
||||
package org.schabi.newpipe.fragments.local;
|
||||
package org.schabi.newpipe.local;
|
||||
|
||||
import android.support.v7.widget.RecyclerView;
|
||||
import android.view.View;
|
||||
@@ -1,12 +1,10 @@
|
||||
package org.schabi.newpipe.fragments.local;
|
||||
package org.schabi.newpipe.local;
|
||||
|
||||
import android.content.Context;
|
||||
import android.graphics.Bitmap;
|
||||
import android.widget.ImageView;
|
||||
|
||||
import com.nostra13.universalimageloader.core.DisplayImageOptions;
|
||||
import com.nostra13.universalimageloader.core.ImageLoader;
|
||||
import com.nostra13.universalimageloader.core.process.BitmapProcessor;
|
||||
|
||||
import org.schabi.newpipe.database.LocalItem;
|
||||
import org.schabi.newpipe.util.OnClickGesture;
|
||||
@@ -1,4 +1,4 @@
|
||||
package org.schabi.newpipe.fragments.local;
|
||||
package org.schabi.newpipe.local;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.support.v7.widget.RecyclerView;
|
||||
@@ -7,11 +7,14 @@ import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
|
||||
import org.schabi.newpipe.database.LocalItem;
|
||||
import org.schabi.newpipe.fragments.local.holder.LocalItemHolder;
|
||||
import org.schabi.newpipe.fragments.local.holder.LocalPlaylistItemHolder;
|
||||
import org.schabi.newpipe.fragments.local.holder.LocalPlaylistStreamItemHolder;
|
||||
import org.schabi.newpipe.fragments.local.holder.LocalStatisticStreamItemHolder;
|
||||
import org.schabi.newpipe.fragments.local.holder.RemotePlaylistItemHolder;
|
||||
import org.schabi.newpipe.local.HeaderFooterHolder;
|
||||
import org.schabi.newpipe.local.LocalItemBuilder;
|
||||
import org.schabi.newpipe.local.holder.LocalItemHolder;
|
||||
import org.schabi.newpipe.local.holder.LocalPlaylistItemHolder;
|
||||
import org.schabi.newpipe.local.holder.LocalPlaylistStreamItemHolder;
|
||||
import org.schabi.newpipe.local.holder.LocalStatisticStreamItemHolder;
|
||||
import org.schabi.newpipe.local.holder.RemotePlaylistItemHolder;
|
||||
import org.schabi.newpipe.util.FallbackViewHolder;
|
||||
import org.schabi.newpipe.util.Localization;
|
||||
import org.schabi.newpipe.util.OnClickGesture;
|
||||
|
||||
@@ -223,7 +226,7 @@ public class LocalItemListAdapter extends RecyclerView.Adapter<RecyclerView.View
|
||||
return new LocalStatisticStreamItemHolder(localItemBuilder, parent);
|
||||
default:
|
||||
Log.e(TAG, "No view type has been considered for holder: [" + type + "]");
|
||||
return null;
|
||||
return new FallbackViewHolder(new View(parent.getContext()));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package org.schabi.newpipe.fragments.local.bookmark;
|
||||
package org.schabi.newpipe.local.bookmark;
|
||||
|
||||
import android.app.AlertDialog;
|
||||
import android.os.Bundle;
|
||||
@@ -19,8 +19,9 @@ import org.schabi.newpipe.database.LocalItem;
|
||||
import org.schabi.newpipe.database.playlist.PlaylistLocalItem;
|
||||
import org.schabi.newpipe.database.playlist.PlaylistMetadataEntry;
|
||||
import org.schabi.newpipe.database.playlist.model.PlaylistRemoteEntity;
|
||||
import org.schabi.newpipe.fragments.local.LocalPlaylistManager;
|
||||
import org.schabi.newpipe.fragments.local.RemotePlaylistManager;
|
||||
import org.schabi.newpipe.local.BaseLocalListFragment;
|
||||
import org.schabi.newpipe.local.playlist.LocalPlaylistManager;
|
||||
import org.schabi.newpipe.local.playlist.RemotePlaylistManager;
|
||||
import org.schabi.newpipe.report.UserAction;
|
||||
import org.schabi.newpipe.util.NavigationHelper;
|
||||
import org.schabi.newpipe.util.OnClickGesture;
|
||||
@@ -38,9 +39,6 @@ import io.reactivex.disposables.CompositeDisposable;
|
||||
public final class BookmarkFragment
|
||||
extends BaseLocalListFragment<List<PlaylistLocalItem>, Void> {
|
||||
|
||||
private View lastPlayedButton;
|
||||
private View mostPlayedButton;
|
||||
|
||||
@State
|
||||
protected Parcelable itemsListState;
|
||||
|
||||
@@ -56,7 +54,8 @@ public final class BookmarkFragment
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
final AppDatabase database = NewPipeDatabase.getInstance(getContext());
|
||||
if (activity == null) return;
|
||||
final AppDatabase database = NewPipeDatabase.getInstance(activity);
|
||||
localPlaylistManager = new LocalPlaylistManager(database);
|
||||
remotePlaylistManager = new RemotePlaylistManager(database);
|
||||
disposables = new CompositeDisposable();
|
||||
@@ -79,7 +78,9 @@ public final class BookmarkFragment
|
||||
@Override
|
||||
public void setUserVisibleHint(boolean isVisibleToUser) {
|
||||
super.setUserVisibleHint(isVisibleToUser);
|
||||
if (isVisibleToUser) setTitle(getString(R.string.tab_bookmarks));
|
||||
if (activity != null && isVisibleToUser) {
|
||||
setTitle(activity.getString(R.string.tab_bookmarks));
|
||||
}
|
||||
}
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////
|
||||
@@ -91,15 +92,6 @@ public final class BookmarkFragment
|
||||
super.initViews(rootView, savedInstanceState);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected View getListHeader() {
|
||||
final View headerRootLayout = activity.getLayoutInflater()
|
||||
.inflate(R.layout.bookmark_header, itemsList, false);
|
||||
lastPlayedButton = headerRootLayout.findViewById(R.id.lastPlayed);
|
||||
mostPlayedButton = headerRootLayout.findViewById(R.id.mostPlayed);
|
||||
return headerRootLayout;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void initListeners() {
|
||||
super.initListeners();
|
||||
@@ -133,18 +125,6 @@ public final class BookmarkFragment
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
lastPlayedButton.setOnClickListener(view -> {
|
||||
if (getParentFragment() != null) {
|
||||
NavigationHelper.openLastPlayedFragment(getParentFragment().getFragmentManager());
|
||||
}
|
||||
});
|
||||
|
||||
mostPlayedButton.setOnClickListener(view -> {
|
||||
if (getParentFragment() != null) {
|
||||
NavigationHelper.openMostPlayedFragment(getParentFragment().getFragmentManager());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////
|
||||
@@ -177,8 +157,6 @@ public final class BookmarkFragment
|
||||
@Override
|
||||
public void onDestroyView() {
|
||||
super.onDestroyView();
|
||||
if (mostPlayedButton != null) mostPlayedButton.setOnClickListener(null);
|
||||
if (lastPlayedButton != null) lastPlayedButton.setOnClickListener(null);
|
||||
|
||||
if (disposables != null) disposables.clear();
|
||||
if (databaseSubscription != null) databaseSubscription.cancel();
|
||||
@@ -1,4 +1,4 @@
|
||||
package org.schabi.newpipe.fragments.local.dialog;
|
||||
package org.schabi.newpipe.local.dialog;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.os.Bundle;
|
||||
@@ -18,17 +18,15 @@ import org.schabi.newpipe.database.playlist.PlaylistMetadataEntry;
|
||||
import org.schabi.newpipe.database.stream.model.StreamEntity;
|
||||
import org.schabi.newpipe.extractor.stream.StreamInfo;
|
||||
import org.schabi.newpipe.extractor.stream.StreamInfoItem;
|
||||
import org.schabi.newpipe.fragments.local.LocalItemListAdapter;
|
||||
import org.schabi.newpipe.fragments.local.LocalPlaylistManager;
|
||||
import org.schabi.newpipe.playlist.PlayQueueItem;
|
||||
import org.schabi.newpipe.local.LocalItemListAdapter;
|
||||
import org.schabi.newpipe.local.playlist.LocalPlaylistManager;
|
||||
import org.schabi.newpipe.player.playqueue.PlayQueueItem;
|
||||
import org.schabi.newpipe.util.OnClickGesture;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
import javax.annotation.Nonnull;
|
||||
|
||||
import io.reactivex.android.schedulers.AndroidSchedulers;
|
||||
import io.reactivex.disposables.Disposable;
|
||||
|
||||
@@ -147,7 +145,7 @@ public final class PlaylistAppendDialog extends PlaylistDialog {
|
||||
|
||||
private void onPlaylistSelected(@NonNull LocalPlaylistManager manager,
|
||||
@NonNull PlaylistMetadataEntry playlist,
|
||||
@Nonnull List<StreamEntity> streams) {
|
||||
@NonNull List<StreamEntity> streams) {
|
||||
if (getStreams() == null) return;
|
||||
|
||||
@SuppressLint("ShowToast")
|
||||
@@ -1,4 +1,4 @@
|
||||
package org.schabi.newpipe.fragments.local.dialog;
|
||||
package org.schabi.newpipe.local.dialog;
|
||||
|
||||
import android.app.AlertDialog;
|
||||
import android.app.Dialog;
|
||||
@@ -12,7 +12,7 @@ import android.widget.Toast;
|
||||
import org.schabi.newpipe.NewPipeDatabase;
|
||||
import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.database.stream.model.StreamEntity;
|
||||
import org.schabi.newpipe.fragments.local.LocalPlaylistManager;
|
||||
import org.schabi.newpipe.local.playlist.LocalPlaylistManager;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package org.schabi.newpipe.fragments.local.dialog;
|
||||
package org.schabi.newpipe.local.dialog;
|
||||
|
||||
import android.os.Bundle;
|
||||
import android.support.annotation.NonNull;
|
||||
@@ -1,4 +1,4 @@
|
||||
package org.schabi.newpipe.fragments.list.feed;
|
||||
package org.schabi.newpipe.local.feed;
|
||||
|
||||
import android.os.Bundle;
|
||||
import android.os.Handler;
|
||||
@@ -21,8 +21,8 @@ import org.schabi.newpipe.extractor.NewPipe;
|
||||
import org.schabi.newpipe.extractor.channel.ChannelInfo;
|
||||
import org.schabi.newpipe.extractor.exceptions.ExtractionException;
|
||||
import org.schabi.newpipe.fragments.list.BaseListFragment;
|
||||
import org.schabi.newpipe.fragments.subscription.SubscriptionService;
|
||||
import org.schabi.newpipe.report.UserAction;
|
||||
import org.schabi.newpipe.local.subscription.SubscriptionService;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.HashSet;
|
||||
@@ -64,7 +64,7 @@ public class FeedFragment extends BaseListFragment<List<SubscriptionEntity>, Voi
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
subscriptionService = SubscriptionService.getInstance();
|
||||
subscriptionService = SubscriptionService.getInstance(activity);
|
||||
|
||||
FEED_LOAD_COUNT = howManyItemsToLoad();
|
||||
}
|
||||
@@ -297,12 +297,12 @@ public class FeedFragment extends BaseListFragment<List<SubscriptionEntity>, Voi
|
||||
// Called only when response is non-empty
|
||||
@Override
|
||||
public void onSuccess(final ChannelInfo channelInfo) {
|
||||
if (infoListAdapter == null || channelInfo.getRelatedStreams().isEmpty()) {
|
||||
if (infoListAdapter == null || channelInfo.getRelatedItems().isEmpty()) {
|
||||
onDone();
|
||||
return;
|
||||
}
|
||||
|
||||
final InfoItem item = channelInfo.getRelatedStreams().get(0);
|
||||
final InfoItem item = channelInfo.getRelatedItems().get(0);
|
||||
// Keep requesting new items if the current one already exists
|
||||
boolean itemExists = doesItemExist(infoListAdapter.getItemsList(), item);
|
||||
if (!itemExists) {
|
||||
@@ -411,7 +411,7 @@ public class FeedFragment extends BaseListFragment<List<SubscriptionEntity>, Voi
|
||||
|
||||
private boolean doesItemExist(final List<InfoItem> items, final InfoItem item) {
|
||||
for (final InfoItem existingItem : items) {
|
||||
if (existingItem.info_type == item.info_type &&
|
||||
if (existingItem.getInfoType() == item.getInfoType() &&
|
||||
existingItem.getServiceId() == item.getServiceId() &&
|
||||
existingItem.getName().equals(item.getName()) &&
|
||||
existingItem.getUrl().equals(item.getUrl())) return true;
|
||||
@@ -1,4 +1,4 @@
|
||||
package org.schabi.newpipe.history;
|
||||
package org.schabi.newpipe.local.history;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.res.Resources;
|
||||
@@ -1,4 +1,4 @@
|
||||
package org.schabi.newpipe.history;
|
||||
package org.schabi.newpipe.local.history;
|
||||
|
||||
import android.support.annotation.Nullable;
|
||||
|
||||
@@ -1,4 +1,22 @@
|
||||
package org.schabi.newpipe.history;
|
||||
package org.schabi.newpipe.local.history;
|
||||
|
||||
/*
|
||||
* Copyright (C) Mauricio Colli 2018
|
||||
* HistoryRecordManager.java is part of NewPipe.
|
||||
*
|
||||
* NewPipe is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* NewPipe is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with NewPipe. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.SharedPreferences;
|
||||
@@ -27,6 +45,7 @@ import java.util.List;
|
||||
|
||||
import io.reactivex.Flowable;
|
||||
import io.reactivex.Maybe;
|
||||
import io.reactivex.Scheduler;
|
||||
import io.reactivex.Single;
|
||||
import io.reactivex.schedulers.Schedulers;
|
||||
|
||||
@@ -80,6 +99,11 @@ public class HistoryRecordManager {
|
||||
.subscribeOn(Schedulers.io());
|
||||
}
|
||||
|
||||
public Single<Integer> deleteWholeStreamHistory() {
|
||||
return Single.fromCallable(() -> streamHistoryTable.deleteAll())
|
||||
.subscribeOn(Schedulers.io());
|
||||
}
|
||||
|
||||
public Flowable<List<StreamHistoryEntry>> getStreamHistory() {
|
||||
return streamHistoryTable.getHistory().subscribeOn(Schedulers.io());
|
||||
}
|
||||
@@ -114,20 +138,6 @@ public class HistoryRecordManager {
|
||||
// Search History
|
||||
///////////////////////////////////////////////////////
|
||||
|
||||
public Single<List<Long>> insertSearches(final Collection<SearchHistoryEntry> entries) {
|
||||
return Single.fromCallable(() -> searchHistoryTable.insertAll(entries))
|
||||
.subscribeOn(Schedulers.io());
|
||||
}
|
||||
|
||||
public Single<Integer> deleteSearches(final Collection<SearchHistoryEntry> entries) {
|
||||
return Single.fromCallable(() -> searchHistoryTable.delete(entries))
|
||||
.subscribeOn(Schedulers.io());
|
||||
}
|
||||
|
||||
public Flowable<List<SearchHistoryEntry>> getSearchHistory() {
|
||||
return searchHistoryTable.getAll();
|
||||
}
|
||||
|
||||
public Maybe<Long> onSearched(final int serviceId, final String search) {
|
||||
if (!isSearchHistoryEnabled()) return Maybe.empty();
|
||||
|
||||
@@ -150,6 +160,11 @@ public class HistoryRecordManager {
|
||||
.subscribeOn(Schedulers.io());
|
||||
}
|
||||
|
||||
public Single<Integer> deleteWholeSearchHistory() {
|
||||
return Single.fromCallable(() -> searchHistoryTable.deleteAll())
|
||||
.subscribeOn(Schedulers.io());
|
||||
}
|
||||
|
||||
public Flowable<List<SearchHistoryEntry>> getRelatedSearches(final String query,
|
||||
final int similarQueryLimit,
|
||||
final int uniqueQueryLimit) {
|
||||
@@ -1,4 +1,4 @@
|
||||
package org.schabi.newpipe.fragments.local.bookmark;
|
||||
package org.schabi.newpipe.local.history;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.Context;
|
||||
@@ -7,9 +7,13 @@ import android.os.Bundle;
|
||||
import android.os.Parcelable;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.support.design.widget.Snackbar;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
|
||||
import org.reactivestreams.Subscriber;
|
||||
import org.reactivestreams.Subscription;
|
||||
@@ -17,13 +21,14 @@ import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.database.LocalItem;
|
||||
import org.schabi.newpipe.database.stream.StreamStatisticsEntry;
|
||||
import org.schabi.newpipe.extractor.stream.StreamInfoItem;
|
||||
import org.schabi.newpipe.history.HistoryRecordManager;
|
||||
import org.schabi.newpipe.local.BaseLocalListFragment;
|
||||
import org.schabi.newpipe.info_list.InfoItemDialog;
|
||||
import org.schabi.newpipe.playlist.PlayQueue;
|
||||
import org.schabi.newpipe.playlist.SinglePlayQueue;
|
||||
import org.schabi.newpipe.player.playqueue.PlayQueue;
|
||||
import org.schabi.newpipe.player.playqueue.SinglePlayQueue;
|
||||
import org.schabi.newpipe.report.UserAction;
|
||||
import org.schabi.newpipe.util.NavigationHelper;
|
||||
import org.schabi.newpipe.util.OnClickGesture;
|
||||
import org.schabi.newpipe.util.ThemeHelper;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
@@ -31,13 +36,19 @@ import java.util.List;
|
||||
|
||||
import icepick.State;
|
||||
import io.reactivex.android.schedulers.AndroidSchedulers;
|
||||
import io.reactivex.disposables.CompositeDisposable;
|
||||
import io.reactivex.disposables.Disposable;
|
||||
|
||||
public abstract class StatisticsPlaylistFragment
|
||||
public class StatisticsPlaylistFragment
|
||||
extends BaseLocalListFragment<List<StreamStatisticsEntry>, Void> {
|
||||
|
||||
private View headerPlayAllButton;
|
||||
private View headerPopupButton;
|
||||
private View headerBackgroundButton;
|
||||
private View playlistCtrl;
|
||||
private View sortButton;
|
||||
private ImageView sortButtonIcon;
|
||||
private TextView sortButtonText;
|
||||
|
||||
@State
|
||||
protected Parcelable itemsListState;
|
||||
@@ -45,14 +56,28 @@ public abstract class StatisticsPlaylistFragment
|
||||
/* Used for independent events */
|
||||
private Subscription databaseSubscription;
|
||||
private HistoryRecordManager recordManager;
|
||||
private CompositeDisposable disposables = new CompositeDisposable();
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////
|
||||
// Abstracts
|
||||
///////////////////////////////////////////////////////////////////////////
|
||||
private enum StatisticSortMode {
|
||||
LAST_PLAYED,
|
||||
MOST_PLAYED,
|
||||
}
|
||||
|
||||
protected abstract String getName();
|
||||
StatisticSortMode sortMode = StatisticSortMode.LAST_PLAYED;
|
||||
|
||||
protected abstract List<StreamStatisticsEntry> processResult(final List<StreamStatisticsEntry> results);
|
||||
protected List<StreamStatisticsEntry> processResult(final List<StreamStatisticsEntry> results) {
|
||||
switch (sortMode) {
|
||||
case LAST_PLAYED:
|
||||
Collections.sort(results, (left, right) ->
|
||||
right.latestAccessDate.compareTo(left.latestAccessDate));
|
||||
return results;
|
||||
case MOST_PLAYED:
|
||||
Collections.sort(results, (left, right) ->
|
||||
((Long) right.watchCount).compareTo(left.watchCount));
|
||||
return results;
|
||||
default: return null;
|
||||
}
|
||||
}
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////
|
||||
// Fragment LifeCycle - Creation
|
||||
@@ -78,16 +103,20 @@ public abstract class StatisticsPlaylistFragment
|
||||
@Override
|
||||
protected void initViews(View rootView, Bundle savedInstanceState) {
|
||||
super.initViews(rootView, savedInstanceState);
|
||||
setTitle(getName());
|
||||
setTitle(getString(R.string.title_last_played));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected View getListHeader() {
|
||||
final View headerRootLayout = activity.getLayoutInflater().inflate(R.layout.playlist_control,
|
||||
final View headerRootLayout = activity.getLayoutInflater().inflate(R.layout.statistic_playlist_control,
|
||||
itemsList, false);
|
||||
playlistCtrl = headerRootLayout.findViewById(R.id.playlist_control);
|
||||
headerPlayAllButton = headerRootLayout.findViewById(R.id.playlist_ctrl_play_all_button);
|
||||
headerPopupButton = headerRootLayout.findViewById(R.id.playlist_ctrl_play_popup_button);
|
||||
headerBackgroundButton = headerRootLayout.findViewById(R.id.playlist_ctrl_play_bg_button);
|
||||
sortButton = headerRootLayout.findViewById(R.id.sortButton);
|
||||
sortButtonIcon = headerRootLayout.findViewById(R.id.sortButtonIcon);
|
||||
sortButtonText = headerRootLayout.findViewById(R.id.sortButtonText);
|
||||
return headerRootLayout;
|
||||
}
|
||||
|
||||
@@ -193,6 +222,8 @@ public abstract class StatisticsPlaylistFragment
|
||||
super.handleResult(result);
|
||||
if (itemListAdapter == null) return;
|
||||
|
||||
playlistCtrl.setVisibility(View.VISIBLE);
|
||||
|
||||
itemListAdapter.clearStreamItemList();
|
||||
|
||||
if (result.isEmpty()) {
|
||||
@@ -212,6 +243,7 @@ public abstract class StatisticsPlaylistFragment
|
||||
NavigationHelper.playOnPopupPlayer(activity, getPlayQueue()));
|
||||
headerBackgroundButton.setOnClickListener(view ->
|
||||
NavigationHelper.playOnBackgroundPlayer(activity, getPlayQueue()));
|
||||
sortButton.setOnClickListener(view -> toggleSortMode());
|
||||
|
||||
hideLoading();
|
||||
}
|
||||
@@ -238,6 +270,21 @@ public abstract class StatisticsPlaylistFragment
|
||||
// Utils
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
private void toggleSortMode() {
|
||||
if(sortMode == StatisticSortMode.LAST_PLAYED) {
|
||||
sortMode = StatisticSortMode.MOST_PLAYED;
|
||||
setTitle(getString(R.string.title_most_played));
|
||||
sortButtonIcon.setImageResource(ThemeHelper.getIconByAttr(R.attr.history, getContext()));
|
||||
sortButtonText.setText(R.string.title_last_played);
|
||||
} else {
|
||||
sortMode = StatisticSortMode.LAST_PLAYED;
|
||||
setTitle(getString(R.string.title_last_played));
|
||||
sortButtonIcon.setImageResource(ThemeHelper.getIconByAttr(R.attr.filter, getContext()));
|
||||
sortButtonText.setText(R.string.title_most_played);
|
||||
}
|
||||
startLoading(true);
|
||||
}
|
||||
|
||||
private void showStreamDialog(final StreamStatisticsEntry item) {
|
||||
final Context context = getContext();
|
||||
final Activity activity = getActivity();
|
||||
@@ -250,6 +297,7 @@ public abstract class StatisticsPlaylistFragment
|
||||
context.getResources().getString(R.string.start_here_on_main),
|
||||
context.getResources().getString(R.string.start_here_on_background),
|
||||
context.getResources().getString(R.string.start_here_on_popup),
|
||||
context.getResources().getString(R.string.delete),
|
||||
};
|
||||
|
||||
final DialogInterface.OnClickListener actions = (dialogInterface, i) -> {
|
||||
@@ -270,6 +318,9 @@ public abstract class StatisticsPlaylistFragment
|
||||
case 4:
|
||||
NavigationHelper.playOnPopupPlayer(activity, getPlayQueue(index));
|
||||
break;
|
||||
case 5:
|
||||
deleteEntry(index);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
@@ -278,6 +329,32 @@ public abstract class StatisticsPlaylistFragment
|
||||
new InfoItemDialog(getActivity(), infoItem, commands, actions).show();
|
||||
}
|
||||
|
||||
private void deleteEntry(final int index) {
|
||||
final LocalItem infoItem = itemListAdapter.getItemsList()
|
||||
.get(index);
|
||||
if(infoItem instanceof StreamStatisticsEntry) {
|
||||
final StreamStatisticsEntry entry = (StreamStatisticsEntry) infoItem;
|
||||
final Disposable onDelete = recordManager.deleteStreamHistory(entry.streamId)
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(
|
||||
howManyDelted -> {
|
||||
if(getView() != null) {
|
||||
Snackbar.make(getView(), R.string.one_item_deleted,
|
||||
Snackbar.LENGTH_SHORT).show();
|
||||
} else {
|
||||
Toast.makeText(getContext(),
|
||||
R.string.one_item_deleted,
|
||||
Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
},
|
||||
throwable -> showSnackBarError(throwable,
|
||||
UserAction.DELETE_FROM_HISTORY, "none",
|
||||
"Deleting item failed", R.string.general_error));
|
||||
|
||||
disposables.add(onDelete);
|
||||
}
|
||||
}
|
||||
|
||||
private PlayQueue getPlayQueue() {
|
||||
return getPlayQueue(0);
|
||||
}
|
||||
@@ -1,17 +1,11 @@
|
||||
package org.schabi.newpipe.fragments.local.holder;
|
||||
package org.schabi.newpipe.local.holder;
|
||||
|
||||
import android.graphics.Bitmap;
|
||||
import android.support.annotation.DimenRes;
|
||||
import android.support.v7.widget.RecyclerView;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.ImageView;
|
||||
|
||||
import com.nostra13.universalimageloader.core.DisplayImageOptions;
|
||||
import com.nostra13.universalimageloader.core.process.BitmapProcessor;
|
||||
|
||||
import org.schabi.newpipe.database.LocalItem;
|
||||
import org.schabi.newpipe.fragments.local.LocalItemBuilder;
|
||||
import org.schabi.newpipe.local.LocalItemBuilder;
|
||||
|
||||
import java.text.DateFormat;
|
||||
|
||||
@@ -45,19 +39,4 @@ public abstract class LocalItemHolder extends RecyclerView.ViewHolder {
|
||||
}
|
||||
|
||||
public abstract void updateFromItem(final LocalItem item, final DateFormat dateFormat);
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// ImageLoaderOptions
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
/**
|
||||
* Base display options
|
||||
*/
|
||||
public static final DisplayImageOptions BASE_DISPLAY_IMAGE_OPTIONS =
|
||||
new DisplayImageOptions.Builder()
|
||||
.cacheInMemory(true)
|
||||
.cacheOnDisk(true)
|
||||
.bitmapConfig(Bitmap.Config.RGB_565)
|
||||
.resetViewBeforeLoading(false)
|
||||
.build();
|
||||
}
|
||||
@@ -1,16 +1,12 @@
|
||||
package org.schabi.newpipe.fragments.local.holder;
|
||||
package org.schabi.newpipe.local.holder;
|
||||
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.TextView;
|
||||
|
||||
import com.nostra13.universalimageloader.core.DisplayImageOptions;
|
||||
|
||||
import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.database.LocalItem;
|
||||
import org.schabi.newpipe.database.playlist.PlaylistMetadataEntry;
|
||||
import org.schabi.newpipe.fragments.local.LocalItemBuilder;
|
||||
import org.schabi.newpipe.local.LocalItemBuilder;
|
||||
import org.schabi.newpipe.util.ImageDisplayConstants;
|
||||
|
||||
import java.text.DateFormat;
|
||||
|
||||
@@ -29,7 +25,8 @@ public class LocalPlaylistItemHolder extends PlaylistItemHolder {
|
||||
itemStreamCountView.setText(String.valueOf(item.streamCount));
|
||||
itemUploaderView.setVisibility(View.INVISIBLE);
|
||||
|
||||
itemBuilder.displayImage(item.thumbnailUrl, itemThumbnailView, DISPLAY_THUMBNAIL_OPTIONS);
|
||||
itemBuilder.displayImage(item.thumbnailUrl, itemThumbnailView,
|
||||
ImageDisplayConstants.DISPLAY_PLAYLIST_OPTIONS);
|
||||
|
||||
super.updateFromItem(localItem, dateFormat);
|
||||
}
|
||||
@@ -1,6 +1,5 @@
|
||||
package org.schabi.newpipe.fragments.local.holder;
|
||||
package org.schabi.newpipe.local.holder;
|
||||
|
||||
import android.graphics.Bitmap;
|
||||
import android.support.v4.content.ContextCompat;
|
||||
import android.view.MotionEvent;
|
||||
import android.view.View;
|
||||
@@ -8,14 +7,12 @@ import android.view.ViewGroup;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.TextView;
|
||||
|
||||
import com.nostra13.universalimageloader.core.DisplayImageOptions;
|
||||
import com.nostra13.universalimageloader.core.assist.ImageScaleType;
|
||||
|
||||
import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.database.LocalItem;
|
||||
import org.schabi.newpipe.database.playlist.PlaylistStreamEntry;
|
||||
import org.schabi.newpipe.extractor.NewPipe;
|
||||
import org.schabi.newpipe.fragments.local.LocalItemBuilder;
|
||||
import org.schabi.newpipe.local.LocalItemBuilder;
|
||||
import org.schabi.newpipe.util.ImageDisplayConstants;
|
||||
import org.schabi.newpipe.util.Localization;
|
||||
|
||||
import java.text.DateFormat;
|
||||
@@ -61,7 +58,8 @@ public class LocalPlaylistStreamItemHolder extends LocalItemHolder {
|
||||
}
|
||||
|
||||
// Default thumbnail is shown on error, while loading and if the url is empty
|
||||
itemBuilder.displayImage(item.thumbnailUrl, itemThumbnailView, DISPLAY_THUMBNAIL_OPTIONS);
|
||||
itemBuilder.displayImage(item.thumbnailUrl, itemThumbnailView,
|
||||
ImageDisplayConstants.DISPLAY_THUMBNAIL_OPTIONS);
|
||||
|
||||
itemView.setOnClickListener(view -> {
|
||||
if (itemBuilder.getOnItemSelectedListener() != null) {
|
||||
@@ -92,15 +90,4 @@ public class LocalPlaylistStreamItemHolder extends LocalItemHolder {
|
||||
return false;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Display options for stream thumbnails
|
||||
*/
|
||||
private static final DisplayImageOptions DISPLAY_THUMBNAIL_OPTIONS =
|
||||
new DisplayImageOptions.Builder()
|
||||
.cloneFrom(BASE_DISPLAY_IMAGE_OPTIONS)
|
||||
.showImageOnFail(R.drawable.dummy_thumbnail)
|
||||
.showImageForEmptyUri(R.drawable.dummy_thumbnail)
|
||||
.showImageOnLoading(R.drawable.dummy_thumbnail)
|
||||
.build();
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package org.schabi.newpipe.fragments.local.holder;
|
||||
package org.schabi.newpipe.local.holder;
|
||||
|
||||
import android.support.v4.content.ContextCompat;
|
||||
import android.view.View;
|
||||
@@ -6,13 +6,12 @@ import android.view.ViewGroup;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.TextView;
|
||||
|
||||
import com.nostra13.universalimageloader.core.DisplayImageOptions;
|
||||
|
||||
import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.database.LocalItem;
|
||||
import org.schabi.newpipe.database.stream.StreamStatisticsEntry;
|
||||
import org.schabi.newpipe.extractor.NewPipe;
|
||||
import org.schabi.newpipe.fragments.local.LocalItemBuilder;
|
||||
import org.schabi.newpipe.local.LocalItemBuilder;
|
||||
import org.schabi.newpipe.util.ImageDisplayConstants;
|
||||
import org.schabi.newpipe.util.Localization;
|
||||
|
||||
import java.text.DateFormat;
|
||||
@@ -84,7 +83,8 @@ public class LocalStatisticStreamItemHolder extends LocalItemHolder {
|
||||
itemAdditionalDetails.setText(getStreamInfoDetailLine(item, dateFormat));
|
||||
|
||||
// Default thumbnail is shown on error, while loading and if the url is empty
|
||||
itemBuilder.displayImage(item.thumbnailUrl, itemThumbnailView, DISPLAY_THUMBNAIL_OPTIONS);
|
||||
itemBuilder.displayImage(item.thumbnailUrl, itemThumbnailView,
|
||||
ImageDisplayConstants.DISPLAY_THUMBNAIL_OPTIONS);
|
||||
|
||||
itemView.setOnClickListener(view -> {
|
||||
if (itemBuilder.getOnItemSelectedListener() != null) {
|
||||
@@ -100,15 +100,4 @@ public class LocalStatisticStreamItemHolder extends LocalItemHolder {
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Display options for stream thumbnails
|
||||
*/
|
||||
public static final DisplayImageOptions DISPLAY_THUMBNAIL_OPTIONS =
|
||||
new DisplayImageOptions.Builder()
|
||||
.cloneFrom(BASE_DISPLAY_IMAGE_OPTIONS)
|
||||
.showImageOnFail(R.drawable.dummy_thumbnail)
|
||||
.showImageForEmptyUri(R.drawable.dummy_thumbnail)
|
||||
.showImageOnLoading(R.drawable.dummy_thumbnail)
|
||||
.build();
|
||||
}
|
||||
@@ -1,14 +1,12 @@
|
||||
package org.schabi.newpipe.fragments.local.holder;
|
||||
package org.schabi.newpipe.local.holder;
|
||||
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.TextView;
|
||||
|
||||
import com.nostra13.universalimageloader.core.DisplayImageOptions;
|
||||
|
||||
import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.database.LocalItem;
|
||||
import org.schabi.newpipe.fragments.local.LocalItemBuilder;
|
||||
import org.schabi.newpipe.local.LocalItemBuilder;
|
||||
|
||||
import java.text.DateFormat;
|
||||
|
||||
@@ -48,15 +46,4 @@ public abstract class PlaylistItemHolder extends LocalItemHolder {
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Display options for playlist thumbnails
|
||||
*/
|
||||
public static final DisplayImageOptions DISPLAY_THUMBNAIL_OPTIONS =
|
||||
new DisplayImageOptions.Builder()
|
||||
.cloneFrom(BASE_DISPLAY_IMAGE_OPTIONS)
|
||||
.showImageOnLoading(R.drawable.dummy_thumbnail_playlist)
|
||||
.showImageForEmptyUri(R.drawable.dummy_thumbnail_playlist)
|
||||
.showImageOnFail(R.drawable.dummy_thumbnail_playlist)
|
||||
.build();
|
||||
}
|
||||
@@ -1,11 +1,12 @@
|
||||
package org.schabi.newpipe.fragments.local.holder;
|
||||
package org.schabi.newpipe.local.holder;
|
||||
|
||||
import android.view.ViewGroup;
|
||||
|
||||
import org.schabi.newpipe.database.LocalItem;
|
||||
import org.schabi.newpipe.database.playlist.model.PlaylistRemoteEntity;
|
||||
import org.schabi.newpipe.extractor.NewPipe;
|
||||
import org.schabi.newpipe.fragments.local.LocalItemBuilder;
|
||||
import org.schabi.newpipe.local.LocalItemBuilder;
|
||||
import org.schabi.newpipe.util.ImageDisplayConstants;
|
||||
import org.schabi.newpipe.util.Localization;
|
||||
|
||||
import java.text.DateFormat;
|
||||
@@ -26,7 +27,7 @@ public class RemotePlaylistItemHolder extends PlaylistItemHolder {
|
||||
NewPipe.getNameOfService(item.getServiceId())));
|
||||
|
||||
itemBuilder.displayImage(item.getThumbnailUrl(), itemThumbnailView,
|
||||
DISPLAY_THUMBNAIL_OPTIONS);
|
||||
ImageDisplayConstants.DISPLAY_PLAYLIST_OPTIONS);
|
||||
|
||||
super.updateFromItem(localItem, dateFormat);
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package org.schabi.newpipe.fragments.local.bookmark;
|
||||
package org.schabi.newpipe.local.playlist;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.Context;
|
||||
@@ -26,10 +26,10 @@ import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.database.LocalItem;
|
||||
import org.schabi.newpipe.database.playlist.PlaylistStreamEntry;
|
||||
import org.schabi.newpipe.extractor.stream.StreamInfoItem;
|
||||
import org.schabi.newpipe.fragments.local.LocalPlaylistManager;
|
||||
import org.schabi.newpipe.local.BaseLocalListFragment;
|
||||
import org.schabi.newpipe.info_list.InfoItemDialog;
|
||||
import org.schabi.newpipe.playlist.PlayQueue;
|
||||
import org.schabi.newpipe.playlist.SinglePlayQueue;
|
||||
import org.schabi.newpipe.player.playqueue.PlayQueue;
|
||||
import org.schabi.newpipe.player.playqueue.SinglePlayQueue;
|
||||
import org.schabi.newpipe.report.UserAction;
|
||||
import org.schabi.newpipe.util.Localization;
|
||||
import org.schabi.newpipe.util.NavigationHelper;
|
||||
@@ -173,7 +173,7 @@ public class LocalPlaylistFragment extends BaseLocalListFragment<List<PlaylistSt
|
||||
@Override
|
||||
public void held(LocalItem selectedItem) {
|
||||
if (selectedItem instanceof PlaylistStreamEntry) {
|
||||
showStreamDialog((PlaylistStreamEntry) selectedItem);
|
||||
showStreamItemDialog((PlaylistStreamEntry) selectedItem);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -506,7 +506,7 @@ public class LocalPlaylistFragment extends BaseLocalListFragment<List<PlaylistSt
|
||||
// Utils
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
protected void showStreamDialog(final PlaylistStreamEntry item) {
|
||||
protected void showStreamItemDialog(final PlaylistStreamEntry item) {
|
||||
final Context context = getContext();
|
||||
final Activity activity = getActivity();
|
||||
if (context == null || context.getResources() == null || getActivity() == null) return;
|
||||
@@ -1,4 +1,4 @@
|
||||
package org.schabi.newpipe.fragments.local;
|
||||
package org.schabi.newpipe.local.playlist;
|
||||
|
||||
import android.support.annotation.Nullable;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package org.schabi.newpipe.fragments.local;
|
||||
package org.schabi.newpipe.local.playlist;
|
||||
|
||||
import org.schabi.newpipe.database.AppDatabase;
|
||||
import org.schabi.newpipe.database.playlist.dao.PlaylistRemoteDAO;
|
||||
@@ -13,11 +13,9 @@ import io.reactivex.schedulers.Schedulers;
|
||||
|
||||
public class RemotePlaylistManager {
|
||||
|
||||
private final AppDatabase database;
|
||||
private final PlaylistRemoteDAO playlistRemoteTable;
|
||||
|
||||
public RemotePlaylistManager(final AppDatabase db) {
|
||||
database = db;
|
||||
playlistRemoteTable = db.playlistRemoteDAO();
|
||||
}
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
package org.schabi.newpipe.local.subscription;
|
||||
|
||||
import android.app.AlertDialog;
|
||||
import android.app.Dialog;
|
||||
import android.content.Intent;
|
||||
import android.os.Bundle;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.support.v4.app.DialogFragment;
|
||||
import android.support.v4.app.Fragment;
|
||||
|
||||
import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.util.ThemeHelper;
|
||||
|
||||
import icepick.Icepick;
|
||||
import icepick.State;
|
||||
|
||||
public class ImportConfirmationDialog extends DialogFragment {
|
||||
@State
|
||||
protected Intent resultServiceIntent;
|
||||
|
||||
public void setResultServiceIntent(Intent resultServiceIntent) {
|
||||
this.resultServiceIntent = resultServiceIntent;
|
||||
}
|
||||
|
||||
public static void show(@NonNull Fragment fragment, @NonNull Intent resultServiceIntent) {
|
||||
if (fragment.getFragmentManager() == null) return;
|
||||
|
||||
final ImportConfirmationDialog confirmationDialog = new ImportConfirmationDialog();
|
||||
confirmationDialog.setResultServiceIntent(resultServiceIntent);
|
||||
confirmationDialog.show(fragment.getFragmentManager(), null);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public Dialog onCreateDialog(@Nullable Bundle savedInstanceState) {
|
||||
return new AlertDialog.Builder(getContext(), ThemeHelper.getDialogTheme(getContext()))
|
||||
.setMessage(R.string.import_network_expensive_warning)
|
||||
.setCancelable(true)
|
||||
.setNegativeButton(R.string.cancel, null)
|
||||
.setPositiveButton(android.R.string.ok, (dialogInterface, i) -> {
|
||||
if (resultServiceIntent != null && getContext() != null) {
|
||||
getContext().startService(resultServiceIntent);
|
||||
}
|
||||
dismiss();
|
||||
})
|
||||
.create();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate(@Nullable Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
if (resultServiceIntent == null) throw new IllegalStateException("Result intent is null");
|
||||
|
||||
Icepick.restoreInstanceState(this, savedInstanceState);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSaveInstanceState(Bundle outState) {
|
||||
super.onSaveInstanceState(outState);
|
||||
Icepick.saveInstanceState(this, outState);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
package org.schabi.newpipe.local.subscription;
|
||||
|
||||
public interface ImportExportEventListener {
|
||||
/**
|
||||
* Called when the size has been resolved.
|
||||
*
|
||||
* @param size how many items there are to import/export
|
||||
*/
|
||||
void onSizeReceived(int size);
|
||||
|
||||
/**
|
||||
* Called everytime an item has been parsed/resolved.
|
||||
*
|
||||
* @param itemName the name of the subscription item
|
||||
*/
|
||||
void onItemCompleted(String itemName);
|
||||
}
|
||||
@@ -0,0 +1,138 @@
|
||||
/*
|
||||
* Copyright 2018 Mauricio Colli <mauriciocolli@outlook.com>
|
||||
* ImportExportJsonHelper.java is part of NewPipe
|
||||
*
|
||||
* License: GPL-3.0+
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.schabi.newpipe.local.subscription;
|
||||
|
||||
import android.support.annotation.Nullable;
|
||||
|
||||
import com.grack.nanojson.JsonAppendableWriter;
|
||||
import com.grack.nanojson.JsonArray;
|
||||
import com.grack.nanojson.JsonObject;
|
||||
import com.grack.nanojson.JsonParser;
|
||||
import com.grack.nanojson.JsonSink;
|
||||
import com.grack.nanojson.JsonWriter;
|
||||
|
||||
import org.schabi.newpipe.BuildConfig;
|
||||
import org.schabi.newpipe.extractor.subscription.SubscriptionExtractor.InvalidSourceException;
|
||||
import org.schabi.newpipe.extractor.subscription.SubscriptionItem;
|
||||
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* A JSON implementation capable of importing and exporting subscriptions, it has the advantage
|
||||
* of being able to transfer subscriptions to any device.
|
||||
*/
|
||||
public class ImportExportJsonHelper {
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Json implementation
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
private static final String JSON_APP_VERSION_KEY = "app_version";
|
||||
private static final String JSON_APP_VERSION_INT_KEY = "app_version_int";
|
||||
|
||||
private static final String JSON_SUBSCRIPTIONS_ARRAY_KEY = "subscriptions";
|
||||
|
||||
private static final String JSON_SERVICE_ID_KEY = "service_id";
|
||||
private static final String JSON_URL_KEY = "url";
|
||||
private static final String JSON_NAME_KEY = "name";
|
||||
|
||||
/**
|
||||
* Read a JSON source through the input stream and return the parsed subscription items.
|
||||
*
|
||||
* @param in the input stream (e.g. a file)
|
||||
* @param eventListener listener for the events generated
|
||||
*/
|
||||
public static List<SubscriptionItem> readFrom(InputStream in, @Nullable ImportExportEventListener eventListener) throws InvalidSourceException {
|
||||
if (in == null) throw new InvalidSourceException("input is null");
|
||||
|
||||
final List<SubscriptionItem> channels = new ArrayList<>();
|
||||
|
||||
try {
|
||||
JsonObject parentObject = JsonParser.object().from(in);
|
||||
JsonArray channelsArray = parentObject.getArray(JSON_SUBSCRIPTIONS_ARRAY_KEY);
|
||||
if (eventListener != null) eventListener.onSizeReceived(channelsArray.size());
|
||||
|
||||
if (channelsArray == null) {
|
||||
throw new InvalidSourceException("Channels array is null");
|
||||
}
|
||||
|
||||
for (Object o : channelsArray) {
|
||||
if (o instanceof JsonObject) {
|
||||
JsonObject itemObject = (JsonObject) o;
|
||||
int serviceId = itemObject.getInt(JSON_SERVICE_ID_KEY, 0);
|
||||
String url = itemObject.getString(JSON_URL_KEY);
|
||||
String name = itemObject.getString(JSON_NAME_KEY);
|
||||
|
||||
if (url != null && name != null && !url.isEmpty() && !name.isEmpty()) {
|
||||
channels.add(new SubscriptionItem(serviceId, url, name));
|
||||
if (eventListener != null) eventListener.onItemCompleted(name);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (Throwable e) {
|
||||
throw new InvalidSourceException("Couldn't parse json", e);
|
||||
}
|
||||
|
||||
return channels;
|
||||
}
|
||||
|
||||
/**
|
||||
* Write the subscriptions items list as JSON to the output.
|
||||
*
|
||||
* @param items the list of subscriptions items
|
||||
* @param out the output stream (e.g. a file)
|
||||
* @param eventListener listener for the events generated
|
||||
*/
|
||||
public static void writeTo(List<SubscriptionItem> items, OutputStream out, @Nullable ImportExportEventListener eventListener) {
|
||||
JsonAppendableWriter writer = JsonWriter.on(out);
|
||||
writeTo(items, writer, eventListener);
|
||||
writer.done();
|
||||
}
|
||||
|
||||
/**
|
||||
* @see #writeTo(List, OutputStream, ImportExportEventListener)
|
||||
*/
|
||||
public static void writeTo(List<SubscriptionItem> items, JsonSink writer, @Nullable ImportExportEventListener eventListener) {
|
||||
if (eventListener != null) eventListener.onSizeReceived(items.size());
|
||||
|
||||
writer.object();
|
||||
|
||||
writer.value(JSON_APP_VERSION_KEY, BuildConfig.VERSION_NAME);
|
||||
writer.value(JSON_APP_VERSION_INT_KEY, BuildConfig.VERSION_CODE);
|
||||
|
||||
writer.array(JSON_SUBSCRIPTIONS_ARRAY_KEY);
|
||||
for (SubscriptionItem item : items) {
|
||||
writer.object();
|
||||
writer.value(JSON_SERVICE_ID_KEY, item.getServiceId());
|
||||
writer.value(JSON_URL_KEY, item.getUrl());
|
||||
writer.value(JSON_NAME_KEY, item.getName());
|
||||
writer.end();
|
||||
|
||||
if (eventListener != null) eventListener.onItemCompleted(item.getName());
|
||||
}
|
||||
writer.end();
|
||||
|
||||
writer.end();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,435 @@
|
||||
package org.schabi.newpipe.local.subscription;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.BroadcastReceiver;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.IntentFilter;
|
||||
import android.graphics.Color;
|
||||
import android.graphics.PorterDuff;
|
||||
import android.os.Bundle;
|
||||
import android.os.Environment;
|
||||
import android.os.Parcelable;
|
||||
import android.support.annotation.DrawableRes;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.support.v4.content.LocalBroadcastManager;
|
||||
import android.support.v7.app.ActionBar;
|
||||
import android.support.v7.widget.LinearLayoutManager;
|
||||
import android.support.v7.widget.RecyclerView;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
|
||||
import com.nononsenseapps.filepicker.Utils;
|
||||
|
||||
import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.database.subscription.SubscriptionEntity;
|
||||
import org.schabi.newpipe.extractor.InfoItem;
|
||||
import org.schabi.newpipe.extractor.NewPipe;
|
||||
import org.schabi.newpipe.extractor.StreamingService;
|
||||
import org.schabi.newpipe.extractor.channel.ChannelInfoItem;
|
||||
import org.schabi.newpipe.extractor.exceptions.ExtractionException;
|
||||
import org.schabi.newpipe.extractor.subscription.SubscriptionExtractor;
|
||||
import org.schabi.newpipe.fragments.BaseStateFragment;
|
||||
import org.schabi.newpipe.info_list.InfoListAdapter;
|
||||
import org.schabi.newpipe.report.UserAction;
|
||||
import org.schabi.newpipe.local.subscription.services.SubscriptionsExportService;
|
||||
import org.schabi.newpipe.local.subscription.services.SubscriptionsImportService;
|
||||
import org.schabi.newpipe.util.FilePickerActivityHelper;
|
||||
import org.schabi.newpipe.util.NavigationHelper;
|
||||
import org.schabi.newpipe.util.OnClickGesture;
|
||||
import org.schabi.newpipe.util.ServiceHelper;
|
||||
import org.schabi.newpipe.util.ThemeHelper;
|
||||
import org.schabi.newpipe.views.CollapsibleView;
|
||||
|
||||
import java.io.File;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
|
||||
import icepick.State;
|
||||
import io.reactivex.Observer;
|
||||
import io.reactivex.android.schedulers.AndroidSchedulers;
|
||||
import io.reactivex.disposables.CompositeDisposable;
|
||||
import io.reactivex.disposables.Disposable;
|
||||
import io.reactivex.schedulers.Schedulers;
|
||||
|
||||
import static org.schabi.newpipe.local.subscription.services.SubscriptionsImportService.KEY_MODE;
|
||||
import static org.schabi.newpipe.local.subscription.services.SubscriptionsImportService.KEY_VALUE;
|
||||
import static org.schabi.newpipe.local.subscription.services.SubscriptionsImportService.PREVIOUS_EXPORT_MODE;
|
||||
import static org.schabi.newpipe.util.AnimationUtils.animateRotation;
|
||||
import static org.schabi.newpipe.util.AnimationUtils.animateView;
|
||||
|
||||
public class SubscriptionFragment extends BaseStateFragment<List<SubscriptionEntity>> {
|
||||
private static final int REQUEST_EXPORT_CODE = 666;
|
||||
private static final int REQUEST_IMPORT_CODE = 667;
|
||||
|
||||
private RecyclerView itemsList;
|
||||
@State
|
||||
protected Parcelable itemsListState;
|
||||
private InfoListAdapter infoListAdapter;
|
||||
|
||||
private View headerRootLayout;
|
||||
private View whatsNewItemListHeader;
|
||||
private View importExportListHeader;
|
||||
|
||||
@State
|
||||
protected Parcelable importExportOptionsState;
|
||||
private CollapsibleView importExportOptions;
|
||||
|
||||
private CompositeDisposable disposables = new CompositeDisposable();
|
||||
private SubscriptionService subscriptionService;
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////
|
||||
// Fragment LifeCycle
|
||||
///////////////////////////////////////////////////////////////////////////
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
setHasOptionsMenu(true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setUserVisibleHint(boolean isVisibleToUser) {
|
||||
super.setUserVisibleHint(isVisibleToUser);
|
||||
if (activity != null && isVisibleToUser) {
|
||||
setTitle(activity.getString(R.string.tab_subscriptions));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAttach(Context context) {
|
||||
super.onAttach(context);
|
||||
infoListAdapter = new InfoListAdapter(activity);
|
||||
subscriptionService = SubscriptionService.getInstance(activity);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, Bundle savedInstanceState) {
|
||||
return inflater.inflate(R.layout.fragment_subscription, container, false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onResume() {
|
||||
super.onResume();
|
||||
setupBroadcastReceiver();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPause() {
|
||||
super.onPause();
|
||||
itemsListState = itemsList.getLayoutManager().onSaveInstanceState();
|
||||
importExportOptionsState = importExportOptions.onSaveInstanceState();
|
||||
|
||||
if (subscriptionBroadcastReceiver != null && activity != null) {
|
||||
LocalBroadcastManager.getInstance(activity).unregisterReceiver(subscriptionBroadcastReceiver);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroyView() {
|
||||
if (disposables != null) disposables.clear();
|
||||
|
||||
super.onDestroyView();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroy() {
|
||||
if (disposables != null) disposables.dispose();
|
||||
disposables = null;
|
||||
subscriptionService = null;
|
||||
|
||||
super.onDestroy();
|
||||
}
|
||||
|
||||
/*/////////////////////////////////////////////////////////////////////////
|
||||
// Menu
|
||||
/////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
@Override
|
||||
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
|
||||
super.onCreateOptionsMenu(menu, inflater);
|
||||
|
||||
ActionBar supportActionBar = activity.getSupportActionBar();
|
||||
if (supportActionBar != null) {
|
||||
supportActionBar.setDisplayShowTitleEnabled(true);
|
||||
setTitle(getString(R.string.tab_subscriptions));
|
||||
}
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Subscriptions import/export
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
private BroadcastReceiver subscriptionBroadcastReceiver;
|
||||
|
||||
private void setupBroadcastReceiver() {
|
||||
if (activity == null) return;
|
||||
|
||||
if (subscriptionBroadcastReceiver != null) {
|
||||
LocalBroadcastManager.getInstance(activity).unregisterReceiver(subscriptionBroadcastReceiver);
|
||||
}
|
||||
|
||||
final IntentFilter filters = new IntentFilter();
|
||||
filters.addAction(SubscriptionsExportService.EXPORT_COMPLETE_ACTION);
|
||||
filters.addAction(SubscriptionsImportService.IMPORT_COMPLETE_ACTION);
|
||||
subscriptionBroadcastReceiver = new BroadcastReceiver() {
|
||||
@Override
|
||||
public void onReceive(Context context, Intent intent) {
|
||||
if (importExportOptions != null) importExportOptions.collapse();
|
||||
}
|
||||
};
|
||||
|
||||
LocalBroadcastManager.getInstance(activity).registerReceiver(subscriptionBroadcastReceiver, filters);
|
||||
}
|
||||
|
||||
private View addItemView(final String title, @DrawableRes final int icon, ViewGroup container) {
|
||||
final View itemRoot = View.inflate(getContext(), R.layout.subscription_import_export_item, null);
|
||||
final TextView titleView = itemRoot.findViewById(android.R.id.text1);
|
||||
final ImageView iconView = itemRoot.findViewById(android.R.id.icon1);
|
||||
|
||||
titleView.setText(title);
|
||||
iconView.setImageResource(icon);
|
||||
|
||||
container.addView(itemRoot);
|
||||
return itemRoot;
|
||||
}
|
||||
|
||||
private void setupImportFromItems(final ViewGroup listHolder) {
|
||||
final View previousBackupItem = addItemView(getString(R.string.previous_export), ThemeHelper.resolveResourceIdFromAttr(getContext(), R.attr.ic_backup), listHolder);
|
||||
previousBackupItem.setOnClickListener(item -> onImportPreviousSelected());
|
||||
|
||||
final int iconColor = ThemeHelper.isLightThemeSelected(getContext()) ? Color.BLACK : Color.WHITE;
|
||||
final String[] services = getResources().getStringArray(R.array.service_list);
|
||||
for (String serviceName : services) {
|
||||
try {
|
||||
final StreamingService service = NewPipe.getService(serviceName);
|
||||
|
||||
final SubscriptionExtractor subscriptionExtractor = service.getSubscriptionExtractor();
|
||||
if (subscriptionExtractor == null) continue;
|
||||
|
||||
final List<SubscriptionExtractor.ContentSource> supportedSources = subscriptionExtractor.getSupportedSources();
|
||||
if (supportedSources.isEmpty()) continue;
|
||||
|
||||
final View itemView = addItemView(serviceName, ServiceHelper.getIcon(service.getServiceId()), listHolder);
|
||||
final ImageView iconView = itemView.findViewById(android.R.id.icon1);
|
||||
iconView.setColorFilter(iconColor, PorterDuff.Mode.SRC_IN);
|
||||
|
||||
itemView.setOnClickListener(selectedItem -> onImportFromServiceSelected(service.getServiceId()));
|
||||
} catch (ExtractionException e) {
|
||||
throw new RuntimeException("Services array contains an entry that it's not a valid service name (" + serviceName + ")", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void setupExportToItems(final ViewGroup listHolder) {
|
||||
final View previousBackupItem = addItemView(getString(R.string.file), ThemeHelper.resolveResourceIdFromAttr(getContext(), R.attr.ic_save), listHolder);
|
||||
previousBackupItem.setOnClickListener(item -> onExportSelected());
|
||||
}
|
||||
|
||||
private void onImportFromServiceSelected(int serviceId) {
|
||||
if (getParentFragment() == null) return;
|
||||
NavigationHelper.openSubscriptionsImportFragment(getParentFragment().getFragmentManager(), serviceId);
|
||||
}
|
||||
|
||||
private void onImportPreviousSelected() {
|
||||
startActivityForResult(FilePickerActivityHelper.chooseSingleFile(activity), REQUEST_IMPORT_CODE);
|
||||
}
|
||||
|
||||
private void onExportSelected() {
|
||||
final String date = new SimpleDateFormat("yyyyMMddHHmm", Locale.ENGLISH).format(new Date());
|
||||
final String exportName = "newpipe_subscriptions_" + date + ".json";
|
||||
final File exportFile = new File(Environment.getExternalStorageDirectory(), exportName);
|
||||
|
||||
startActivityForResult(FilePickerActivityHelper.chooseFileToSave(activity, exportFile.getAbsolutePath()), REQUEST_EXPORT_CODE);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onActivityResult(int requestCode, int resultCode, Intent data) {
|
||||
super.onActivityResult(requestCode, resultCode, data);
|
||||
if (data != null && data.getData() != null && resultCode == Activity.RESULT_OK) {
|
||||
if (requestCode == REQUEST_EXPORT_CODE) {
|
||||
final File exportFile = Utils.getFileForUri(data.getData());
|
||||
if (!exportFile.getParentFile().canWrite() || !exportFile.getParentFile().canRead()) {
|
||||
Toast.makeText(activity, R.string.invalid_directory, Toast.LENGTH_SHORT).show();
|
||||
} else {
|
||||
activity.startService(new Intent(activity, SubscriptionsExportService.class)
|
||||
.putExtra(SubscriptionsExportService.KEY_FILE_PATH, exportFile.getAbsolutePath()));
|
||||
}
|
||||
} else if (requestCode == REQUEST_IMPORT_CODE) {
|
||||
final String path = Utils.getFileForUri(data.getData()).getAbsolutePath();
|
||||
ImportConfirmationDialog.show(this, new Intent(activity, SubscriptionsImportService.class)
|
||||
.putExtra(KEY_MODE, PREVIOUS_EXPORT_MODE)
|
||||
.putExtra(KEY_VALUE, path));
|
||||
}
|
||||
}
|
||||
}
|
||||
/*/////////////////////////////////////////////////////////////////////////
|
||||
// Fragment Views
|
||||
/////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
@Override
|
||||
protected void initViews(View rootView, Bundle savedInstanceState) {
|
||||
super.initViews(rootView, savedInstanceState);
|
||||
|
||||
infoListAdapter = new InfoListAdapter(getActivity());
|
||||
itemsList = rootView.findViewById(R.id.items_list);
|
||||
itemsList.setLayoutManager(new LinearLayoutManager(activity));
|
||||
|
||||
infoListAdapter.setHeader(headerRootLayout = activity.getLayoutInflater().inflate(R.layout.subscription_header, itemsList, false));
|
||||
whatsNewItemListHeader = headerRootLayout.findViewById(R.id.whats_new);
|
||||
importExportListHeader = headerRootLayout.findViewById(R.id.import_export);
|
||||
importExportOptions = headerRootLayout.findViewById(R.id.import_export_options);
|
||||
|
||||
infoListAdapter.useMiniItemVariants(true);
|
||||
itemsList.setAdapter(infoListAdapter);
|
||||
|
||||
setupImportFromItems(headerRootLayout.findViewById(R.id.import_from_options));
|
||||
setupExportToItems(headerRootLayout.findViewById(R.id.export_to_options));
|
||||
|
||||
if (importExportOptionsState != null) {
|
||||
importExportOptions.onRestoreInstanceState(importExportOptionsState);
|
||||
importExportOptionsState = null;
|
||||
}
|
||||
|
||||
importExportOptions.addListener(getExpandIconSyncListener(headerRootLayout.findViewById(R.id.import_export_expand_icon)));
|
||||
importExportOptions.ready();
|
||||
}
|
||||
|
||||
private CollapsibleView.StateListener getExpandIconSyncListener(final ImageView iconView) {
|
||||
return newState -> animateRotation(iconView, 250, newState == CollapsibleView.COLLAPSED ? 0 : 180);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void initListeners() {
|
||||
super.initListeners();
|
||||
|
||||
infoListAdapter.setOnChannelSelectedListener(new OnClickGesture<ChannelInfoItem>() {
|
||||
@Override
|
||||
public void selected(ChannelInfoItem selectedItem) {
|
||||
// Requires the parent fragment to find holder for fragment replacement
|
||||
NavigationHelper.openChannelFragment(getParentFragment().getFragmentManager(),
|
||||
selectedItem.getServiceId(), selectedItem.getUrl(), selectedItem.getName());
|
||||
}
|
||||
});
|
||||
|
||||
//noinspection ConstantConditions
|
||||
whatsNewItemListHeader.setOnClickListener(v ->
|
||||
NavigationHelper.openWhatsNewFragment(getParentFragment().getFragmentManager()));
|
||||
importExportListHeader.setOnClickListener(v -> importExportOptions.switchState());
|
||||
}
|
||||
|
||||
private void resetFragment() {
|
||||
if (disposables != null) disposables.clear();
|
||||
if (infoListAdapter != null) infoListAdapter.clearStreamItemList();
|
||||
}
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////
|
||||
// Subscriptions Loader
|
||||
///////////////////////////////////////////////////////////////////////////
|
||||
|
||||
@Override
|
||||
public void startLoading(boolean forceLoad) {
|
||||
super.startLoading(forceLoad);
|
||||
resetFragment();
|
||||
|
||||
subscriptionService.getSubscription().toObservable()
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(getSubscriptionObserver());
|
||||
}
|
||||
|
||||
private Observer<List<SubscriptionEntity>> getSubscriptionObserver() {
|
||||
return new Observer<List<SubscriptionEntity>>() {
|
||||
@Override
|
||||
public void onSubscribe(Disposable d) {
|
||||
showLoading();
|
||||
disposables.add(d);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onNext(List<SubscriptionEntity> subscriptions) {
|
||||
handleResult(subscriptions);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(Throwable exception) {
|
||||
SubscriptionFragment.this.onError(exception);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onComplete() {
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleResult(@NonNull List<SubscriptionEntity> result) {
|
||||
super.handleResult(result);
|
||||
|
||||
infoListAdapter.clearStreamItemList();
|
||||
|
||||
if (result.isEmpty()) {
|
||||
whatsNewItemListHeader.setVisibility(View.GONE);
|
||||
showEmptyState();
|
||||
} else {
|
||||
infoListAdapter.addInfoItemList(getSubscriptionItems(result));
|
||||
if (itemsListState != null) {
|
||||
itemsList.getLayoutManager().onRestoreInstanceState(itemsListState);
|
||||
itemsListState = null;
|
||||
}
|
||||
whatsNewItemListHeader.setVisibility(View.VISIBLE);
|
||||
hideLoading();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private List<InfoItem> getSubscriptionItems(List<SubscriptionEntity> subscriptions) {
|
||||
List<InfoItem> items = new ArrayList<>();
|
||||
for (final SubscriptionEntity subscription : subscriptions) items.add(subscription.toChannelInfoItem());
|
||||
|
||||
Collections.sort(items,
|
||||
(InfoItem o1, InfoItem o2) -> o1.getName().compareToIgnoreCase(o2.getName()));
|
||||
return items;
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Contract
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
@Override
|
||||
public void showLoading() {
|
||||
super.showLoading();
|
||||
animateView(itemsList, false, 100);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void hideLoading() {
|
||||
super.hideLoading();
|
||||
animateView(itemsList, true, 200);
|
||||
}
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////
|
||||
// Fragment Error Handling
|
||||
///////////////////////////////////////////////////////////////////////////
|
||||
|
||||
@Override
|
||||
protected boolean onError(Throwable exception) {
|
||||
resetFragment();
|
||||
if (super.onError(exception)) return true;
|
||||
|
||||
onUnrecoverableError(exception, UserAction.SOMETHING_ELSE, "none", "Subscriptions", R.string.general_error);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,7 @@
|
||||
package org.schabi.newpipe.fragments.subscription;
|
||||
package org.schabi.newpipe.local.subscription;
|
||||
|
||||
import android.content.Context;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.util.Log;
|
||||
|
||||
import org.schabi.newpipe.MainActivity;
|
||||
@@ -10,6 +12,7 @@ import org.schabi.newpipe.database.subscription.SubscriptionEntity;
|
||||
import org.schabi.newpipe.extractor.channel.ChannelInfo;
|
||||
import org.schabi.newpipe.util.ExtractorHelper;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.Executor;
|
||||
import java.util.concurrent.Executors;
|
||||
@@ -20,7 +23,6 @@ import io.reactivex.CompletableSource;
|
||||
import io.reactivex.Flowable;
|
||||
import io.reactivex.Maybe;
|
||||
import io.reactivex.Scheduler;
|
||||
import io.reactivex.annotations.NonNull;
|
||||
import io.reactivex.functions.Function;
|
||||
import io.reactivex.schedulers.Schedulers;
|
||||
|
||||
@@ -32,10 +34,20 @@ import io.reactivex.schedulers.Schedulers;
|
||||
*/
|
||||
public class SubscriptionService {
|
||||
|
||||
private static final SubscriptionService sInstance = new SubscriptionService();
|
||||
private static volatile SubscriptionService instance;
|
||||
|
||||
public static SubscriptionService getInstance() {
|
||||
return sInstance;
|
||||
public static SubscriptionService getInstance(@NonNull Context context) {
|
||||
SubscriptionService result = instance;
|
||||
if (result == null) {
|
||||
synchronized (SubscriptionService.class) {
|
||||
result = instance;
|
||||
if (result == null) {
|
||||
instance = (result = new SubscriptionService(context));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
protected final String TAG = "SubscriptionService@" + Integer.toHexString(hashCode());
|
||||
@@ -48,8 +60,8 @@ public class SubscriptionService {
|
||||
|
||||
private Scheduler subscriptionScheduler;
|
||||
|
||||
private SubscriptionService() {
|
||||
db = NewPipeDatabase.getInstance();
|
||||
private SubscriptionService(Context context) {
|
||||
db = NewPipeDatabase.getInstance(context.getApplicationContext());
|
||||
subscription = getSubscriptionInfos();
|
||||
|
||||
final Executor subscriptionExecutor = Executors.newFixedThreadPool(SUBSCRIPTION_THREAD_POOL_SIZE);
|
||||
@@ -114,7 +126,7 @@ public class SubscriptionService {
|
||||
if (!isSubscriptionUpToDate(info, subscription)) {
|
||||
subscription.setData(info.getName(), info.getAvatarUrl(), info.getDescription(), info.getSubscriberCount());
|
||||
|
||||
return update(subscription);
|
||||
return Completable.fromRunnable(() -> subscriptionTable().update(subscription));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -127,13 +139,11 @@ public class SubscriptionService {
|
||||
.flatMapCompletable(update);
|
||||
}
|
||||
|
||||
private Completable update(final SubscriptionEntity updatedSubscription) {
|
||||
return Completable.fromRunnable(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
subscriptionTable().update(updatedSubscription);
|
||||
}
|
||||
});
|
||||
public List<SubscriptionEntity> upsertAll(final List<ChannelInfo> infoList) {
|
||||
final List<SubscriptionEntity> entityList = new ArrayList<>();
|
||||
for (ChannelInfo info : infoList) entityList.add(SubscriptionEntity.from(info));
|
||||
|
||||
return subscriptionTable().upsertAll(entityList);
|
||||
}
|
||||
|
||||
private boolean isSubscriptionUpToDate(final ChannelInfo info, final SubscriptionEntity entity) {
|
||||
@@ -0,0 +1,210 @@
|
||||
package org.schabi.newpipe.local.subscription;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.Intent;
|
||||
import android.os.Bundle;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.support.annotation.StringRes;
|
||||
import android.support.v4.text.util.LinkifyCompat;
|
||||
import android.support.v7.app.ActionBar;
|
||||
import android.text.TextUtils;
|
||||
import android.text.util.Linkify;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.Button;
|
||||
import android.widget.EditText;
|
||||
import android.widget.TextView;
|
||||
|
||||
import com.nononsenseapps.filepicker.Utils;
|
||||
|
||||
import org.schabi.newpipe.BaseFragment;
|
||||
import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.extractor.NewPipe;
|
||||
import org.schabi.newpipe.extractor.exceptions.ExtractionException;
|
||||
import org.schabi.newpipe.extractor.subscription.SubscriptionExtractor;
|
||||
import org.schabi.newpipe.report.ErrorActivity;
|
||||
import org.schabi.newpipe.report.UserAction;
|
||||
import org.schabi.newpipe.local.subscription.services.SubscriptionsImportService;
|
||||
import org.schabi.newpipe.util.Constants;
|
||||
import org.schabi.newpipe.util.FilePickerActivityHelper;
|
||||
import org.schabi.newpipe.util.ServiceHelper;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
import icepick.State;
|
||||
|
||||
import static org.schabi.newpipe.extractor.subscription.SubscriptionExtractor.ContentSource.CHANNEL_URL;
|
||||
import static org.schabi.newpipe.local.subscription.services.SubscriptionsImportService.CHANNEL_URL_MODE;
|
||||
import static org.schabi.newpipe.local.subscription.services.SubscriptionsImportService.INPUT_STREAM_MODE;
|
||||
import static org.schabi.newpipe.local.subscription.services.SubscriptionsImportService.KEY_MODE;
|
||||
import static org.schabi.newpipe.local.subscription.services.SubscriptionsImportService.KEY_VALUE;
|
||||
|
||||
public class SubscriptionsImportFragment extends BaseFragment {
|
||||
private static final int REQUEST_IMPORT_FILE_CODE = 666;
|
||||
|
||||
@State
|
||||
protected int currentServiceId = Constants.NO_SERVICE_ID;
|
||||
|
||||
private List<SubscriptionExtractor.ContentSource> supportedSources;
|
||||
private String relatedUrl;
|
||||
@StringRes
|
||||
private int instructionsString;
|
||||
|
||||
public static SubscriptionsImportFragment getInstance(int serviceId) {
|
||||
SubscriptionsImportFragment instance = new SubscriptionsImportFragment();
|
||||
instance.setInitialData(serviceId);
|
||||
return instance;
|
||||
}
|
||||
|
||||
public void setInitialData(int serviceId) {
|
||||
this.currentServiceId = serviceId;
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Views
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
private TextView infoTextView;
|
||||
|
||||
private EditText inputText;
|
||||
private Button inputButton;
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////
|
||||
// Fragment LifeCycle
|
||||
///////////////////////////////////////////////////////////////////////////
|
||||
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
setupServiceVariables();
|
||||
if (supportedSources.isEmpty() && currentServiceId != Constants.NO_SERVICE_ID) {
|
||||
ErrorActivity.reportError(activity, Collections.emptyList(), null, null, ErrorActivity.ErrorInfo.make(UserAction.SOMETHING_ELSE,
|
||||
NewPipe.getNameOfService(currentServiceId), "Service don't support importing", R.string.general_error));
|
||||
activity.finish();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setUserVisibleHint(boolean isVisibleToUser) {
|
||||
super.setUserVisibleHint(isVisibleToUser);
|
||||
if (isVisibleToUser) {
|
||||
setTitle(getString(R.string.import_title));
|
||||
}
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, Bundle savedInstanceState) {
|
||||
return inflater.inflate(R.layout.fragment_import, container, false);
|
||||
}
|
||||
|
||||
/*/////////////////////////////////////////////////////////////////////////
|
||||
// Fragment Views
|
||||
/////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
@Override
|
||||
protected void initViews(View rootView, Bundle savedInstanceState) {
|
||||
super.initViews(rootView, savedInstanceState);
|
||||
|
||||
inputButton = rootView.findViewById(R.id.input_button);
|
||||
inputText = rootView.findViewById(R.id.input_text);
|
||||
|
||||
infoTextView = rootView.findViewById(R.id.info_text_view);
|
||||
|
||||
// TODO: Support services that can import from more than one source (show the option to the user)
|
||||
if (supportedSources.contains(CHANNEL_URL)) {
|
||||
inputButton.setText(R.string.import_title);
|
||||
inputText.setVisibility(View.VISIBLE);
|
||||
inputText.setHint(ServiceHelper.getImportInstructionsHint(currentServiceId));
|
||||
} else {
|
||||
inputButton.setText(R.string.import_file_title);
|
||||
}
|
||||
|
||||
if (instructionsString != 0) {
|
||||
if (TextUtils.isEmpty(relatedUrl)) {
|
||||
setInfoText(getString(instructionsString));
|
||||
} else {
|
||||
setInfoText(getString(instructionsString, relatedUrl));
|
||||
}
|
||||
} else {
|
||||
setInfoText("");
|
||||
}
|
||||
|
||||
ActionBar supportActionBar = activity.getSupportActionBar();
|
||||
if (supportActionBar != null) {
|
||||
supportActionBar.setDisplayShowTitleEnabled(true);
|
||||
setTitle(getString(R.string.import_title));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void initListeners() {
|
||||
super.initListeners();
|
||||
inputButton.setOnClickListener(v -> onImportClicked());
|
||||
}
|
||||
|
||||
private void onImportClicked() {
|
||||
if (inputText.getVisibility() == View.VISIBLE) {
|
||||
final String value = inputText.getText().toString();
|
||||
if (!value.isEmpty()) onImportUrl(value);
|
||||
} else {
|
||||
onImportFile();
|
||||
}
|
||||
}
|
||||
|
||||
public void onImportUrl(String value) {
|
||||
ImportConfirmationDialog.show(this, new Intent(activity, SubscriptionsImportService.class)
|
||||
.putExtra(KEY_MODE, CHANNEL_URL_MODE)
|
||||
.putExtra(KEY_VALUE, value)
|
||||
.putExtra(Constants.KEY_SERVICE_ID, currentServiceId));
|
||||
}
|
||||
|
||||
public void onImportFile() {
|
||||
startActivityForResult(FilePickerActivityHelper.chooseSingleFile(activity), REQUEST_IMPORT_FILE_CODE);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onActivityResult(int requestCode, int resultCode, Intent data) {
|
||||
super.onActivityResult(requestCode, resultCode, data);
|
||||
if (data == null) return;
|
||||
|
||||
if (resultCode == Activity.RESULT_OK && requestCode == REQUEST_IMPORT_FILE_CODE && data.getData() != null) {
|
||||
final String path = Utils.getFileForUri(data.getData()).getAbsolutePath();
|
||||
ImportConfirmationDialog.show(this, new Intent(activity, SubscriptionsImportService.class)
|
||||
.putExtra(KEY_MODE, INPUT_STREAM_MODE)
|
||||
.putExtra(KEY_VALUE, path)
|
||||
.putExtra(Constants.KEY_SERVICE_ID, currentServiceId));
|
||||
}
|
||||
}
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////
|
||||
// Subscriptions
|
||||
///////////////////////////////////////////////////////////////////////////
|
||||
|
||||
private void setupServiceVariables() {
|
||||
if (currentServiceId != Constants.NO_SERVICE_ID) {
|
||||
try {
|
||||
final SubscriptionExtractor extractor = NewPipe.getService(currentServiceId).getSubscriptionExtractor();
|
||||
supportedSources = extractor.getSupportedSources();
|
||||
relatedUrl = extractor.getRelatedUrl();
|
||||
instructionsString = ServiceHelper.getImportInstructions(currentServiceId);
|
||||
return;
|
||||
} catch (ExtractionException ignored) {
|
||||
}
|
||||
}
|
||||
|
||||
supportedSources = Collections.emptyList();
|
||||
relatedUrl = null;
|
||||
instructionsString = 0;
|
||||
}
|
||||
|
||||
private void setInfoText(String infoString) {
|
||||
infoTextView.setText(infoString);
|
||||
LinkifyCompat.addLinks(infoTextView, Linkify.WEB_URLS);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,227 @@
|
||||
/*
|
||||
* Copyright 2018 Mauricio Colli <mauriciocolli@outlook.com>
|
||||
* BaseImportExportService.java is part of NewPipe
|
||||
*
|
||||
* License: GPL-3.0+
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.schabi.newpipe.local.subscription.services;
|
||||
|
||||
import android.app.Service;
|
||||
import android.content.Intent;
|
||||
import android.os.Build;
|
||||
import android.os.IBinder;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.support.annotation.StringRes;
|
||||
import android.support.v4.app.NotificationCompat;
|
||||
import android.support.v4.app.NotificationManagerCompat;
|
||||
import android.text.TextUtils;
|
||||
import android.widget.Toast;
|
||||
|
||||
import org.reactivestreams.Publisher;
|
||||
import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.extractor.subscription.SubscriptionExtractor;
|
||||
import org.schabi.newpipe.report.ErrorActivity;
|
||||
import org.schabi.newpipe.report.UserAction;
|
||||
import org.schabi.newpipe.local.subscription.ImportExportEventListener;
|
||||
import org.schabi.newpipe.local.subscription.SubscriptionService;
|
||||
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.IOException;
|
||||
import java.util.Collections;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
import io.reactivex.Flowable;
|
||||
import io.reactivex.android.schedulers.AndroidSchedulers;
|
||||
import io.reactivex.disposables.CompositeDisposable;
|
||||
import io.reactivex.functions.Function;
|
||||
import io.reactivex.processors.PublishProcessor;
|
||||
|
||||
public abstract class BaseImportExportService extends Service {
|
||||
protected final String TAG = this.getClass().getSimpleName();
|
||||
|
||||
protected NotificationManagerCompat notificationManager;
|
||||
protected NotificationCompat.Builder notificationBuilder;
|
||||
|
||||
protected SubscriptionService subscriptionService;
|
||||
protected CompositeDisposable disposables = new CompositeDisposable();
|
||||
protected PublishProcessor<String> notificationUpdater = PublishProcessor.create();
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public IBinder onBind(Intent intent) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate() {
|
||||
super.onCreate();
|
||||
subscriptionService = SubscriptionService.getInstance(this);
|
||||
setupNotification();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroy() {
|
||||
super.onDestroy();
|
||||
disposeAll();
|
||||
}
|
||||
|
||||
protected void disposeAll() {
|
||||
disposables.clear();
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Notification Impl
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
private static final int NOTIFICATION_SAMPLING_PERIOD = 2500;
|
||||
|
||||
protected AtomicInteger currentProgress = new AtomicInteger(-1);
|
||||
protected AtomicInteger maxProgress = new AtomicInteger(-1);
|
||||
protected ImportExportEventListener eventListener = new ImportExportEventListener() {
|
||||
@Override
|
||||
public void onSizeReceived(int size) {
|
||||
maxProgress.set(size);
|
||||
currentProgress.set(0);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onItemCompleted(String itemName) {
|
||||
currentProgress.incrementAndGet();
|
||||
notificationUpdater.onNext(itemName);
|
||||
}
|
||||
};
|
||||
|
||||
protected abstract int getNotificationId();
|
||||
@StringRes
|
||||
public abstract int getTitle();
|
||||
|
||||
protected void setupNotification() {
|
||||
notificationManager = NotificationManagerCompat.from(this);
|
||||
notificationBuilder = createNotification();
|
||||
startForeground(getNotificationId(), notificationBuilder.build());
|
||||
|
||||
final Function<Flowable<String>, Publisher<String>> throttleAfterFirstEmission = flow -> flow.limit(1)
|
||||
.concatWith(flow.skip(1).throttleLast(NOTIFICATION_SAMPLING_PERIOD, TimeUnit.MILLISECONDS));
|
||||
|
||||
disposables.add(notificationUpdater
|
||||
.filter(s -> !s.isEmpty())
|
||||
.publish(throttleAfterFirstEmission)
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(this::updateNotification));
|
||||
}
|
||||
|
||||
protected void updateNotification(String text) {
|
||||
notificationBuilder.setProgress(maxProgress.get(), currentProgress.get(), maxProgress.get() == -1);
|
||||
|
||||
final String progressText = currentProgress + "/" + maxProgress;
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
||||
if (!TextUtils.isEmpty(text)) text = text + " (" + progressText + ")";
|
||||
} else {
|
||||
notificationBuilder.setContentInfo(progressText);
|
||||
}
|
||||
|
||||
if (!TextUtils.isEmpty(text)) notificationBuilder.setContentText(text);
|
||||
notificationManager.notify(getNotificationId(), notificationBuilder.build());
|
||||
}
|
||||
|
||||
protected void stopService() {
|
||||
postErrorResult(null, null);
|
||||
}
|
||||
|
||||
protected void stopAndReportError(@Nullable Throwable error, String request) {
|
||||
stopService();
|
||||
|
||||
final ErrorActivity.ErrorInfo errorInfo = ErrorActivity.ErrorInfo.make(UserAction.SUBSCRIPTION, "unknown",
|
||||
request, R.string.general_error);
|
||||
ErrorActivity.reportError(this, error != null ? Collections.singletonList(error) : Collections.emptyList(),
|
||||
null, null, errorInfo);
|
||||
}
|
||||
|
||||
protected void postErrorResult(String title, String text) {
|
||||
disposeAll();
|
||||
stopForeground(true);
|
||||
stopSelf();
|
||||
|
||||
if (title == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
text = text == null ? "" : text;
|
||||
notificationBuilder = new NotificationCompat.Builder(this, getString(R.string.notification_channel_id))
|
||||
.setSmallIcon(R.drawable.ic_newpipe_triangle_white)
|
||||
.setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
|
||||
.setContentTitle(title)
|
||||
.setStyle(new NotificationCompat.BigTextStyle().bigText(text))
|
||||
.setContentText(text);
|
||||
notificationManager.notify(getNotificationId(), notificationBuilder.build());
|
||||
}
|
||||
|
||||
protected NotificationCompat.Builder createNotification() {
|
||||
return new NotificationCompat.Builder(this, getString(R.string.notification_channel_id))
|
||||
.setOngoing(true)
|
||||
.setProgress(-1, -1, true)
|
||||
.setSmallIcon(R.drawable.ic_newpipe_triangle_white)
|
||||
.setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
|
||||
.setContentTitle(getString(getTitle()));
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Toast
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
protected Toast toast;
|
||||
|
||||
protected void showToast(@StringRes int message) {
|
||||
showToast(getString(message), Toast.LENGTH_SHORT);
|
||||
}
|
||||
|
||||
protected void showToast(String message, int duration) {
|
||||
if (toast != null) toast.cancel();
|
||||
|
||||
toast = Toast.makeText(this, message, duration);
|
||||
toast.show();
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Error handling
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
protected void handleError(@StringRes int errorTitle, @NonNull Throwable error) {
|
||||
String message = getErrorMessage(error);
|
||||
|
||||
if (TextUtils.isEmpty(message)) {
|
||||
final String errorClassName = error.getClass().getName();
|
||||
message = getString(R.string.error_occurred_detail, errorClassName);
|
||||
}
|
||||
|
||||
showToast(errorTitle);
|
||||
postErrorResult(getString(errorTitle), message);
|
||||
}
|
||||
|
||||
protected String getErrorMessage(Throwable error) {
|
||||
String message = null;
|
||||
if (error instanceof SubscriptionExtractor.InvalidSourceException) {
|
||||
message = getString(R.string.invalid_source);
|
||||
} else if (error instanceof FileNotFoundException) {
|
||||
message = getString(R.string.invalid_file);
|
||||
} else if (error instanceof IOException) {
|
||||
message = getString(R.string.network_error);
|
||||
}
|
||||
return message;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,153 @@
|
||||
/*
|
||||
* Copyright 2018 Mauricio Colli <mauriciocolli@outlook.com>
|
||||
* SubscriptionsExportService.java is part of NewPipe
|
||||
*
|
||||
* License: GPL-3.0+
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.schabi.newpipe.local.subscription.services;
|
||||
|
||||
import android.content.Intent;
|
||||
import android.support.v4.content.LocalBroadcastManager;
|
||||
import android.text.TextUtils;
|
||||
import android.util.Log;
|
||||
|
||||
import org.reactivestreams.Subscriber;
|
||||
import org.reactivestreams.Subscription;
|
||||
import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.database.subscription.SubscriptionEntity;
|
||||
import org.schabi.newpipe.extractor.subscription.SubscriptionItem;
|
||||
import org.schabi.newpipe.local.subscription.ImportExportJsonHelper;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.FileOutputStream;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import io.reactivex.android.schedulers.AndroidSchedulers;
|
||||
import io.reactivex.functions.Function;
|
||||
import io.reactivex.schedulers.Schedulers;
|
||||
|
||||
import static org.schabi.newpipe.MainActivity.DEBUG;
|
||||
|
||||
public class SubscriptionsExportService extends BaseImportExportService {
|
||||
public static final String KEY_FILE_PATH = "key_file_path";
|
||||
|
||||
/**
|
||||
* A {@link LocalBroadcastManager local broadcast} will be made with this action when the export is successfully completed.
|
||||
*/
|
||||
public static final String EXPORT_COMPLETE_ACTION = "org.schabi.newpipe.local.subscription.services.SubscriptionsExportService.EXPORT_COMPLETE";
|
||||
|
||||
private Subscription subscription;
|
||||
private File outFile;
|
||||
private FileOutputStream outputStream;
|
||||
|
||||
@Override
|
||||
public int onStartCommand(Intent intent, int flags, int startId) {
|
||||
if (intent == null || subscription != null) return START_NOT_STICKY;
|
||||
|
||||
final String path = intent.getStringExtra(KEY_FILE_PATH);
|
||||
if (TextUtils.isEmpty(path)) {
|
||||
stopAndReportError(new IllegalStateException("Exporting to a file, but the path is empty or null"), "Exporting subscriptions");
|
||||
return START_NOT_STICKY;
|
||||
}
|
||||
|
||||
try {
|
||||
outputStream = new FileOutputStream(outFile = new File(path));
|
||||
} catch (FileNotFoundException e) {
|
||||
handleError(e);
|
||||
return START_NOT_STICKY;
|
||||
}
|
||||
|
||||
startExport();
|
||||
|
||||
return START_NOT_STICKY;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected int getNotificationId() {
|
||||
return 4567;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getTitle() {
|
||||
return R.string.export_ongoing;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void disposeAll() {
|
||||
super.disposeAll();
|
||||
if (subscription != null) subscription.cancel();
|
||||
}
|
||||
|
||||
private void startExport() {
|
||||
showToast(R.string.export_ongoing);
|
||||
|
||||
subscriptionService.subscriptionTable()
|
||||
.getAll()
|
||||
.take(1)
|
||||
.map(subscriptionEntities -> {
|
||||
final List<SubscriptionItem> result = new ArrayList<>(subscriptionEntities.size());
|
||||
for (SubscriptionEntity entity : subscriptionEntities) {
|
||||
result.add(new SubscriptionItem(entity.getServiceId(), entity.getUrl(), entity.getName()));
|
||||
}
|
||||
return result;
|
||||
})
|
||||
.map(exportToFile())
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(getSubscriber());
|
||||
}
|
||||
|
||||
private Subscriber<File> getSubscriber() {
|
||||
return new Subscriber<File>() {
|
||||
@Override
|
||||
public void onSubscribe(Subscription s) {
|
||||
subscription = s;
|
||||
s.request(1);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onNext(File file) {
|
||||
if (DEBUG) Log.d(TAG, "startExport() success: file = " + file);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(Throwable error) {
|
||||
Log.e(TAG, "onError() called with: error = [" + error + "]", error);
|
||||
handleError(error);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onComplete() {
|
||||
LocalBroadcastManager.getInstance(SubscriptionsExportService.this).sendBroadcast(new Intent(EXPORT_COMPLETE_ACTION));
|
||||
showToast(R.string.export_complete_toast);
|
||||
stopService();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private Function<List<SubscriptionItem>, File> exportToFile() {
|
||||
return subscriptionItems -> {
|
||||
ImportExportJsonHelper.writeTo(subscriptionItems, outputStream, eventListener);
|
||||
return outFile;
|
||||
};
|
||||
}
|
||||
|
||||
protected void handleError(Throwable error) {
|
||||
super.handleError(R.string.subscriptions_export_unsuccessful, error);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,264 @@
|
||||
/*
|
||||
* Copyright 2018 Mauricio Colli <mauriciocolli@outlook.com>
|
||||
* SubscriptionsImportService.java is part of NewPipe
|
||||
*
|
||||
* License: GPL-3.0+
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.schabi.newpipe.local.subscription.services;
|
||||
|
||||
import android.content.Intent;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.support.v4.content.LocalBroadcastManager;
|
||||
import android.text.TextUtils;
|
||||
import android.util.Log;
|
||||
|
||||
import org.reactivestreams.Subscriber;
|
||||
import org.reactivestreams.Subscription;
|
||||
import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.database.subscription.SubscriptionEntity;
|
||||
import org.schabi.newpipe.extractor.NewPipe;
|
||||
import org.schabi.newpipe.extractor.channel.ChannelInfo;
|
||||
import org.schabi.newpipe.extractor.subscription.SubscriptionItem;
|
||||
import org.schabi.newpipe.local.subscription.ImportExportJsonHelper;
|
||||
import org.schabi.newpipe.util.Constants;
|
||||
import org.schabi.newpipe.util.ExtractorHelper;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import io.reactivex.Flowable;
|
||||
import io.reactivex.Notification;
|
||||
import io.reactivex.android.schedulers.AndroidSchedulers;
|
||||
import io.reactivex.functions.Consumer;
|
||||
import io.reactivex.functions.Function;
|
||||
import io.reactivex.schedulers.Schedulers;
|
||||
|
||||
import static org.schabi.newpipe.MainActivity.DEBUG;
|
||||
|
||||
public class SubscriptionsImportService extends BaseImportExportService {
|
||||
public static final int CHANNEL_URL_MODE = 0;
|
||||
public static final int INPUT_STREAM_MODE = 1;
|
||||
public static final int PREVIOUS_EXPORT_MODE = 2;
|
||||
public static final String KEY_MODE = "key_mode";
|
||||
public static final String KEY_VALUE = "key_value";
|
||||
|
||||
/**
|
||||
* A {@link LocalBroadcastManager local broadcast} will be made with this action when the import is successfully completed.
|
||||
*/
|
||||
public static final String IMPORT_COMPLETE_ACTION = "org.schabi.newpipe.local.subscription.services.SubscriptionsImportService.IMPORT_COMPLETE";
|
||||
|
||||
private Subscription subscription;
|
||||
private int currentMode;
|
||||
private int currentServiceId;
|
||||
|
||||
@Nullable
|
||||
private String channelUrl;
|
||||
@Nullable
|
||||
private InputStream inputStream;
|
||||
|
||||
@Override
|
||||
public int onStartCommand(Intent intent, int flags, int startId) {
|
||||
if (intent == null || subscription != null) return START_NOT_STICKY;
|
||||
|
||||
currentMode = intent.getIntExtra(KEY_MODE, -1);
|
||||
currentServiceId = intent.getIntExtra(Constants.KEY_SERVICE_ID, Constants.NO_SERVICE_ID);
|
||||
|
||||
if (currentMode == CHANNEL_URL_MODE) {
|
||||
channelUrl = intent.getStringExtra(KEY_VALUE);
|
||||
} else {
|
||||
final String filePath = intent.getStringExtra(KEY_VALUE);
|
||||
if (TextUtils.isEmpty(filePath)) {
|
||||
stopAndReportError(new IllegalStateException("Importing from input stream, but file path is empty or null"), "Importing subscriptions");
|
||||
return START_NOT_STICKY;
|
||||
}
|
||||
|
||||
try {
|
||||
inputStream = new FileInputStream(new File(filePath));
|
||||
} catch (FileNotFoundException e) {
|
||||
handleError(e);
|
||||
return START_NOT_STICKY;
|
||||
}
|
||||
}
|
||||
|
||||
if (currentMode == -1 || currentMode == CHANNEL_URL_MODE && channelUrl == null) {
|
||||
final String errorDescription = "Some important field is null or in illegal state: currentMode=[" + currentMode + "], channelUrl=[" + channelUrl + "], inputStream=[" + inputStream + "]";
|
||||
stopAndReportError(new IllegalStateException(errorDescription), "Importing subscriptions");
|
||||
return START_NOT_STICKY;
|
||||
}
|
||||
|
||||
startImport();
|
||||
return START_NOT_STICKY;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected int getNotificationId() {
|
||||
return 4568;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getTitle() {
|
||||
return R.string.import_ongoing;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void disposeAll() {
|
||||
super.disposeAll();
|
||||
if (subscription != null) subscription.cancel();
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Imports
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
/**
|
||||
* How many extractions running in parallel.
|
||||
*/
|
||||
public static final int PARALLEL_EXTRACTIONS = 8;
|
||||
|
||||
/**
|
||||
* Number of items to buffer to mass-insert in the subscriptions table, this leads to
|
||||
* a better performance as we can then use db transactions.
|
||||
*/
|
||||
public static final int BUFFER_COUNT_BEFORE_INSERT = 50;
|
||||
|
||||
private void startImport() {
|
||||
showToast(R.string.import_ongoing);
|
||||
|
||||
Flowable<List<SubscriptionItem>> flowable = null;
|
||||
if (currentMode == CHANNEL_URL_MODE) {
|
||||
flowable = importFromChannelUrl();
|
||||
} else if (currentMode == INPUT_STREAM_MODE) {
|
||||
flowable = importFromInputStream();
|
||||
} else if (currentMode == PREVIOUS_EXPORT_MODE) {
|
||||
flowable = importFromPreviousExport();
|
||||
}
|
||||
|
||||
if (flowable == null) {
|
||||
final String message = "Flowable given by \"importFrom\" is null (current mode: " + currentMode + ")";
|
||||
stopAndReportError(new IllegalStateException(message), "Importing subscriptions");
|
||||
return;
|
||||
}
|
||||
|
||||
flowable.doOnNext(subscriptionItems -> eventListener.onSizeReceived(subscriptionItems.size()))
|
||||
.flatMap(Flowable::fromIterable)
|
||||
|
||||
.parallel(PARALLEL_EXTRACTIONS)
|
||||
.runOn(Schedulers.io())
|
||||
.map((Function<SubscriptionItem, Notification<ChannelInfo>>) subscriptionItem -> {
|
||||
try {
|
||||
return Notification.createOnNext(ExtractorHelper
|
||||
.getChannelInfo(subscriptionItem.getServiceId(), subscriptionItem.getUrl(), true)
|
||||
.blockingGet());
|
||||
} catch (Throwable e) {
|
||||
return Notification.createOnError(e);
|
||||
}
|
||||
})
|
||||
.sequential()
|
||||
|
||||
.observeOn(Schedulers.io())
|
||||
.doOnNext(getNotificationsConsumer())
|
||||
.buffer(BUFFER_COUNT_BEFORE_INSERT)
|
||||
.map(upsertBatch())
|
||||
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(getSubscriber());
|
||||
}
|
||||
|
||||
private Subscriber<List<SubscriptionEntity>> getSubscriber() {
|
||||
return new Subscriber<List<SubscriptionEntity>>() {
|
||||
|
||||
@Override
|
||||
public void onSubscribe(Subscription s) {
|
||||
subscription = s;
|
||||
s.request(Long.MAX_VALUE);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onNext(List<SubscriptionEntity> successfulInserted) {
|
||||
if (DEBUG) Log.d(TAG, "startImport() " + successfulInserted.size() + " items successfully inserted into the database");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(Throwable error) {
|
||||
handleError(error);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onComplete() {
|
||||
LocalBroadcastManager.getInstance(SubscriptionsImportService.this).sendBroadcast(new Intent(IMPORT_COMPLETE_ACTION));
|
||||
showToast(R.string.import_complete_toast);
|
||||
stopService();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private Consumer<Notification<ChannelInfo>> getNotificationsConsumer() {
|
||||
return notification -> {
|
||||
if (notification.isOnNext()) {
|
||||
String name = notification.getValue().getName();
|
||||
eventListener.onItemCompleted(!TextUtils.isEmpty(name) ? name : "");
|
||||
} else if (notification.isOnError()) {
|
||||
final Throwable error = notification.getError();
|
||||
final Throwable cause = error.getCause();
|
||||
if (error instanceof IOException) {
|
||||
throw (IOException) error;
|
||||
} else if (cause != null && cause instanceof IOException) {
|
||||
throw (IOException) cause;
|
||||
}
|
||||
|
||||
eventListener.onItemCompleted("");
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private Function<List<Notification<ChannelInfo>>, List<SubscriptionEntity>> upsertBatch() {
|
||||
return notificationList -> {
|
||||
final List<ChannelInfo> infoList = new ArrayList<>(notificationList.size());
|
||||
for (Notification<ChannelInfo> n : notificationList) {
|
||||
if (n.isOnNext()) infoList.add(n.getValue());
|
||||
}
|
||||
|
||||
return subscriptionService.upsertAll(infoList);
|
||||
};
|
||||
}
|
||||
|
||||
private Flowable<List<SubscriptionItem>> importFromChannelUrl() {
|
||||
return Flowable.fromCallable(() -> NewPipe.getService(currentServiceId)
|
||||
.getSubscriptionExtractor()
|
||||
.fromChannelUrl(channelUrl));
|
||||
}
|
||||
|
||||
private Flowable<List<SubscriptionItem>> importFromInputStream() {
|
||||
return Flowable.fromCallable(() -> NewPipe.getService(currentServiceId)
|
||||
.getSubscriptionExtractor()
|
||||
.fromInputStream(inputStream));
|
||||
}
|
||||
|
||||
private Flowable<List<SubscriptionItem>> importFromPreviousExport() {
|
||||
return Flowable.fromCallable(() -> ImportExportJsonHelper.readFrom(inputStream, null));
|
||||
}
|
||||
|
||||
protected void handleError(@NonNull Throwable error) {
|
||||
super.handleError(R.string.subscriptions_import_unsuccessful, error);
|
||||
}
|
||||
}
|
||||
@@ -33,6 +33,7 @@ import android.support.annotation.NonNull;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.support.v4.app.NotificationCompat;
|
||||
import android.util.Log;
|
||||
import android.view.View;
|
||||
import android.widget.RemoteViews;
|
||||
|
||||
import com.google.android.exoplayer2.PlaybackParameters;
|
||||
@@ -46,7 +47,8 @@ import org.schabi.newpipe.extractor.stream.AudioStream;
|
||||
import org.schabi.newpipe.extractor.stream.StreamInfo;
|
||||
import org.schabi.newpipe.player.event.PlayerEventListener;
|
||||
import org.schabi.newpipe.player.helper.LockManager;
|
||||
import org.schabi.newpipe.playlist.PlayQueueItem;
|
||||
import org.schabi.newpipe.player.helper.PlayerHelper;
|
||||
import org.schabi.newpipe.player.playqueue.PlayQueueItem;
|
||||
import org.schabi.newpipe.util.ListHelper;
|
||||
import org.schabi.newpipe.util.NavigationHelper;
|
||||
import org.schabi.newpipe.util.ThemeHelper;
|
||||
@@ -75,6 +77,7 @@ public final class BackgroundPlayer extends Service {
|
||||
|
||||
private BasePlayerImpl basePlayerImpl;
|
||||
private LockManager lockManager;
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Service-Activity Binder
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
@@ -115,8 +118,12 @@ public final class BackgroundPlayer extends Service {
|
||||
|
||||
@Override
|
||||
public int onStartCommand(Intent intent, int flags, int startId) {
|
||||
if (DEBUG) Log.d(TAG, "onStartCommand() called with: intent = [" + intent + "], flags = [" + flags + "], startId = [" + startId + "]");
|
||||
if (DEBUG) Log.d(TAG, "onStartCommand() called with: intent = [" + intent +
|
||||
"], flags = [" + flags + "], startId = [" + startId + "]");
|
||||
basePlayerImpl.handleIntent(intent);
|
||||
if (basePlayerImpl.mediaSessionManager != null) {
|
||||
basePlayerImpl.mediaSessionManager.handleMediaButtonIntent(intent);
|
||||
}
|
||||
return START_NOT_STICKY;
|
||||
}
|
||||
|
||||
@@ -157,6 +164,11 @@ public final class BackgroundPlayer extends Service {
|
||||
if (DEBUG) Log.d(TAG, "onScreenOnOff() called with: on = [" + on + "]");
|
||||
shouldUpdateOnProgress = on;
|
||||
basePlayerImpl.triggerProgressUpdate();
|
||||
if (on) {
|
||||
basePlayerImpl.startProgressLoop();
|
||||
} else {
|
||||
basePlayerImpl.stopProgressLoop();
|
||||
}
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
@@ -291,15 +303,15 @@ public final class BackgroundPlayer extends Service {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onThumbnailReceived(Bitmap thumbnail) {
|
||||
super.onThumbnailReceived(thumbnail);
|
||||
public void onLoadingComplete(String imageUri, View view, Bitmap loadedImage) {
|
||||
super.onLoadingComplete(imageUri, view, loadedImage);
|
||||
|
||||
if (thumbnail != null) {
|
||||
if (loadedImage != null) {
|
||||
// rebuild notification here since remote view does not release bitmaps, causing memory leaks
|
||||
resetNotification();
|
||||
|
||||
if (notRemoteView != null) notRemoteView.setImageViewBitmap(R.id.notificationCover, thumbnail);
|
||||
if (bigNotRemoteView != null) bigNotRemoteView.setImageViewBitmap(R.id.notificationCover, thumbnail);
|
||||
if (notRemoteView != null) notRemoteView.setImageViewBitmap(R.id.notificationCover, loadedImage);
|
||||
if (bigNotRemoteView != null) bigNotRemoteView.setImageViewBitmap(R.id.notificationCover, loadedImage);
|
||||
|
||||
updateNotification(-1);
|
||||
}
|
||||
@@ -378,29 +390,34 @@ public final class BackgroundPlayer extends Service {
|
||||
// Playback Listener
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
@Override
|
||||
public void sync(@NonNull final PlayQueueItem item, @Nullable final StreamInfo info) {
|
||||
if (currentItem == item && currentInfo == info) return;
|
||||
super.sync(item, info);
|
||||
|
||||
resetNotification();
|
||||
updateNotification(-1);
|
||||
updateMetadata();
|
||||
protected void onMetadataChanged(@NonNull final PlayQueueItem item,
|
||||
@Nullable final StreamInfo info,
|
||||
final int newPlayQueueIndex,
|
||||
final boolean hasPlayQueueItemChanged) {
|
||||
if (shouldUpdateOnProgress || hasPlayQueueItemChanged) {
|
||||
resetNotification();
|
||||
updateNotification(-1);
|
||||
updateMetadata();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@Nullable
|
||||
public MediaSource sourceOf(final PlayQueueItem item, final StreamInfo info) {
|
||||
final int index = ListHelper.getDefaultAudioFormat(context, info.audio_streams);
|
||||
if (index < 0 || index >= info.audio_streams.size()) return null;
|
||||
final MediaSource liveSource = super.sourceOf(item, info);
|
||||
if (liveSource != null) return liveSource;
|
||||
|
||||
final AudioStream audio = info.audio_streams.get(index);
|
||||
return buildMediaSource(audio.getUrl(), MediaFormat.getSuffixById(audio.getFormatId()));
|
||||
final int index = ListHelper.getDefaultAudioFormat(context, info.getAudioStreams());
|
||||
if (index < 0 || index >= info.getAudioStreams().size()) return null;
|
||||
|
||||
final AudioStream audio = info.getAudioStreams().get(index);
|
||||
return buildMediaSource(audio.getUrl(), PlayerHelper.cacheKeyOf(info, audio),
|
||||
MediaFormat.getSuffixById(audio.getFormatId()));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void shutdown() {
|
||||
super.shutdown();
|
||||
public void onPlaybackShutdown() {
|
||||
super.onPlaybackShutdown();
|
||||
onClose();
|
||||
}
|
||||
|
||||
@@ -429,7 +446,8 @@ public final class BackgroundPlayer extends Service {
|
||||
|
||||
private void updatePlayback() {
|
||||
if (activityListener != null && simpleExoPlayer != null && playQueue != null) {
|
||||
activityListener.onPlaybackUpdate(currentState, getRepeatMode(), playQueue.isShuffled(), getPlaybackParameters());
|
||||
activityListener.onPlaybackUpdate(currentState, getRepeatMode(),
|
||||
playQueue.isShuffled(), getPlaybackParameters());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -477,7 +495,7 @@ public final class BackgroundPlayer extends Service {
|
||||
onClose();
|
||||
break;
|
||||
case ACTION_PLAY_PAUSE:
|
||||
onVideoPlayPause();
|
||||
onPlayPause();
|
||||
break;
|
||||
case ACTION_REPEAT:
|
||||
onRepeatClicked();
|
||||
@@ -536,7 +554,6 @@ public final class BackgroundPlayer extends Service {
|
||||
super.onPaused();
|
||||
|
||||
updateNotification(R.drawable.ic_play_arrow_white);
|
||||
if (isProgressLoopRunning()) stopProgressLoop();
|
||||
|
||||
lockManager.releaseWifiAndCpu();
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -19,20 +19,23 @@
|
||||
|
||||
package org.schabi.newpipe.player;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.SharedPreferences;
|
||||
import android.content.pm.ActivityInfo;
|
||||
import android.content.res.Configuration;
|
||||
import android.graphics.Color;
|
||||
import android.graphics.drawable.ColorDrawable;
|
||||
import android.media.AudioManager;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.preference.PreferenceManager;
|
||||
import android.provider.Settings;
|
||||
import android.support.annotation.ColorInt;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.support.v4.app.ActivityCompat;
|
||||
import android.support.v7.app.AppCompatActivity;
|
||||
import android.support.v7.widget.RecyclerView;
|
||||
import android.support.v7.widget.helper.ItemTouchHelper;
|
||||
import android.util.DisplayMetrics;
|
||||
@@ -50,6 +53,7 @@ import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
|
||||
import com.google.android.exoplayer2.Player;
|
||||
import com.google.android.exoplayer2.text.CaptionStyleCompat;
|
||||
import com.google.android.exoplayer2.ui.AspectRatioFrameLayout;
|
||||
import com.google.android.exoplayer2.ui.SubtitleView;
|
||||
|
||||
@@ -57,37 +61,50 @@ import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.extractor.stream.StreamInfo;
|
||||
import org.schabi.newpipe.extractor.stream.VideoStream;
|
||||
import org.schabi.newpipe.fragments.OnScrollBelowItemsListener;
|
||||
import org.schabi.newpipe.player.helper.PlaybackParameterDialog;
|
||||
import org.schabi.newpipe.player.helper.PlayerHelper;
|
||||
import org.schabi.newpipe.playlist.PlayQueueItem;
|
||||
import org.schabi.newpipe.playlist.PlayQueueItemBuilder;
|
||||
import org.schabi.newpipe.playlist.PlayQueueItemHolder;
|
||||
import org.schabi.newpipe.player.playqueue.PlayQueueItem;
|
||||
import org.schabi.newpipe.player.playqueue.PlayQueueItemBuilder;
|
||||
import org.schabi.newpipe.player.playqueue.PlayQueueItemHolder;
|
||||
import org.schabi.newpipe.player.playqueue.PlayQueueItemTouchCallback;
|
||||
import org.schabi.newpipe.util.AnimationUtils;
|
||||
import org.schabi.newpipe.util.ListHelper;
|
||||
import org.schabi.newpipe.util.NavigationHelper;
|
||||
import org.schabi.newpipe.util.PermissionHelper;
|
||||
import org.schabi.newpipe.util.StateSaver;
|
||||
import org.schabi.newpipe.util.ThemeHelper;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Queue;
|
||||
import java.util.UUID;
|
||||
|
||||
import static org.schabi.newpipe.player.BasePlayer.STATE_PLAYING;
|
||||
import static org.schabi.newpipe.player.VideoPlayer.DEFAULT_CONTROLS_DURATION;
|
||||
import static org.schabi.newpipe.player.VideoPlayer.DEFAULT_CONTROLS_HIDE_TIME;
|
||||
import static org.schabi.newpipe.util.AnimationUtils.Type.SLIDE_AND_ALPHA;
|
||||
import static org.schabi.newpipe.util.AnimationUtils.animateRotation;
|
||||
import static org.schabi.newpipe.util.AnimationUtils.animateView;
|
||||
import static org.schabi.newpipe.util.StateSaver.KEY_SAVED_STATE;
|
||||
|
||||
/**
|
||||
* Activity Player implementing VideoPlayer
|
||||
*
|
||||
* @author mauriciocolli
|
||||
*/
|
||||
public final class MainVideoPlayer extends Activity {
|
||||
public final class MainVideoPlayer extends AppCompatActivity
|
||||
implements StateSaver.WriteRead, PlaybackParameterDialog.Callback {
|
||||
private static final String TAG = ".MainVideoPlayer";
|
||||
private static final boolean DEBUG = BasePlayer.DEBUG;
|
||||
private static final String PLAYER_STATE_INTENT = "player_state_intent";
|
||||
|
||||
private GestureDetector gestureDetector;
|
||||
|
||||
private boolean activityPaused;
|
||||
private VideoPlayerImpl playerImpl;
|
||||
|
||||
private SharedPreferences defaultPreferences;
|
||||
|
||||
@Nullable private PlayerState playerState;
|
||||
private boolean isInMultiWindow;
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Activity LifeCycle
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
@@ -98,44 +115,37 @@ public final class MainVideoPlayer extends Activity {
|
||||
if (DEBUG) Log.d(TAG, "onCreate() called with: savedInstanceState = [" + savedInstanceState + "]");
|
||||
defaultPreferences = PreferenceManager.getDefaultSharedPreferences(this);
|
||||
ThemeHelper.setTheme(this);
|
||||
getWindow().setBackgroundDrawable(new ColorDrawable(Color.BLACK));
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) getWindow().setStatusBarColor(Color.BLACK);
|
||||
setVolumeControlStream(AudioManager.STREAM_MUSIC);
|
||||
|
||||
final Intent intent;
|
||||
if (savedInstanceState != null && savedInstanceState.getParcelable(PLAYER_STATE_INTENT) != null) {
|
||||
intent = savedInstanceState.getParcelable(PLAYER_STATE_INTENT);
|
||||
} else {
|
||||
intent = getIntent();
|
||||
}
|
||||
WindowManager.LayoutParams lp = getWindow().getAttributes();
|
||||
lp.screenBrightness = PlayerHelper.getScreenBrightness(getApplicationContext());
|
||||
getWindow().setAttributes(lp);
|
||||
|
||||
if (intent == null) {
|
||||
Toast.makeText(this, R.string.general_error, Toast.LENGTH_SHORT).show();
|
||||
finish();
|
||||
return;
|
||||
}
|
||||
|
||||
showSystemUi();
|
||||
hideSystemUi();
|
||||
setContentView(R.layout.activity_main_player);
|
||||
playerImpl = new VideoPlayerImpl(this);
|
||||
playerImpl.setup(findViewById(android.R.id.content));
|
||||
playerImpl.handleIntent(intent);
|
||||
|
||||
if (savedInstanceState != null && savedInstanceState.get(KEY_SAVED_STATE) != null) {
|
||||
return; // We have saved states, stop here to restore it
|
||||
}
|
||||
|
||||
final Intent intent = getIntent();
|
||||
if (intent != null) {
|
||||
playerImpl.handleIntent(intent);
|
||||
} else {
|
||||
Toast.makeText(this, R.string.general_error, Toast.LENGTH_SHORT).show();
|
||||
finish();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onSaveInstanceState(Bundle outState) {
|
||||
super.onSaveInstanceState(outState);
|
||||
if (this.playerImpl == null) return;
|
||||
|
||||
final Intent intent = NavigationHelper.getPlayerIntent(
|
||||
getApplicationContext(),
|
||||
this.getClass(),
|
||||
this.playerImpl.getPlayQueue(),
|
||||
this.playerImpl.getRepeatMode(),
|
||||
this.playerImpl.getPlaybackSpeed(),
|
||||
this.playerImpl.getPlaybackPitch(),
|
||||
this.playerImpl.getPlaybackQuality()
|
||||
);
|
||||
outState.putParcelable(PLAYER_STATE_INTENT, intent);
|
||||
protected void onRestoreInstanceState(@NonNull Bundle bundle) {
|
||||
if (DEBUG) Log.d(TAG, "onRestoreInstanceState() called");
|
||||
super.onRestoreInstanceState(bundle);
|
||||
StateSaver.tryToRestore(bundle, this);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -145,51 +155,30 @@ public final class MainVideoPlayer extends Activity {
|
||||
playerImpl.handleIntent(intent);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBackPressed() {
|
||||
if (DEBUG) Log.d(TAG, "onBackPressed() called");
|
||||
super.onBackPressed();
|
||||
if (playerImpl.isPlaying()) playerImpl.getPlayer().setPlayWhenReady(false);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onStop() {
|
||||
super.onStop();
|
||||
if (DEBUG) Log.d(TAG, "onStop() called");
|
||||
activityPaused = true;
|
||||
|
||||
if (playerImpl.getPlayer() != null) {
|
||||
playerImpl.wasPlaying = playerImpl.getPlayer().getPlayWhenReady();
|
||||
playerImpl.setRecovery();
|
||||
playerImpl.destroyPlayer();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onResume() {
|
||||
super.onResume();
|
||||
if (DEBUG) Log.d(TAG, "onResume() called");
|
||||
if (activityPaused) {
|
||||
playerImpl.initPlayer();
|
||||
playerImpl.getPlayPauseButton().setImageResource(R.drawable.ic_play_arrow_white);
|
||||
super.onResume();
|
||||
|
||||
playerImpl.getPlayer().setPlayWhenReady(playerImpl.wasPlaying);
|
||||
playerImpl.initPlayback(playerImpl.playQueue);
|
||||
|
||||
activityPaused = false;
|
||||
if (globalScreenOrientationLocked()) {
|
||||
boolean lastOrientationWasLandscape = defaultPreferences.getBoolean(
|
||||
getString(R.string.last_orientation_landscape_key), false);
|
||||
setLandscape(lastOrientationWasLandscape);
|
||||
}
|
||||
if(globalScreenOrientationLocked()) {
|
||||
boolean lastOrientationWasLandscape
|
||||
= defaultPreferences.getBoolean(getString(R.string.last_orientation_landscape_key), false);
|
||||
setLandScape(lastOrientationWasLandscape);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDestroy() {
|
||||
super.onDestroy();
|
||||
if (DEBUG) Log.d(TAG, "onDestroy() called");
|
||||
if (playerImpl != null) playerImpl.destroy();
|
||||
// Upon going in or out of multiwindow mode, isInMultiWindow will always be false,
|
||||
// since the first onResume needs to restore the player.
|
||||
// Subsequent onResume calls while multiwindow mode remains the same and the player is
|
||||
// prepared should be ignored.
|
||||
if (isInMultiWindow) return;
|
||||
isInMultiWindow = isInMultiWindow();
|
||||
|
||||
if (playerState != null) {
|
||||
playerImpl.setPlaybackQuality(playerState.getPlaybackQuality());
|
||||
playerImpl.initPlayback(playerState.getPlayQueue(), playerState.getRepeatMode(),
|
||||
playerState.getPlaybackSpeed(), playerState.getPlaybackPitch(),
|
||||
playerState.wasPlaying());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -202,49 +191,107 @@ public final class MainVideoPlayer extends Activity {
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onSaveInstanceState(Bundle outState) {
|
||||
if (DEBUG) Log.d(TAG, "onSaveInstanceState() called");
|
||||
super.onSaveInstanceState(outState);
|
||||
if (playerImpl == null) return;
|
||||
|
||||
playerImpl.setRecovery();
|
||||
playerState = new PlayerState(playerImpl.getPlayQueue(), playerImpl.getRepeatMode(),
|
||||
playerImpl.getPlaybackSpeed(), playerImpl.getPlaybackPitch(),
|
||||
playerImpl.getPlaybackQuality(), playerImpl.isPlaying());
|
||||
StateSaver.tryToSave(isChangingConfigurations(), null, outState, this);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onStop() {
|
||||
if (DEBUG) Log.d(TAG, "onStop() called");
|
||||
super.onStop();
|
||||
playerImpl.destroy();
|
||||
|
||||
PlayerHelper.setScreenBrightness(getApplicationContext(),
|
||||
getWindow().getAttributes().screenBrightness);
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Utils
|
||||
// State Saving
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
@Override
|
||||
public String generateSuffix() {
|
||||
return "." + UUID.randomUUID().toString() + ".player";
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeTo(Queue<Object> objectsToSave) {
|
||||
if (objectsToSave == null) return;
|
||||
objectsToSave.add(playerState);
|
||||
}
|
||||
|
||||
@Override
|
||||
@SuppressWarnings("unchecked")
|
||||
public void readFrom(@NonNull Queue<Object> savedObjects) {
|
||||
playerState = (PlayerState) savedObjects.poll();
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// View
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
private void showSystemUi() {
|
||||
if (DEBUG) Log.d(TAG, "showSystemUi() called");
|
||||
if (playerImpl != null && playerImpl.queueVisible) return;
|
||||
|
||||
final int visibility;
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
|
||||
getWindow().getDecorView().setSystemUiVisibility(
|
||||
View.SYSTEM_UI_FLAG_LAYOUT_STABLE
|
||||
| View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
|
||||
| View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
|
||||
);
|
||||
} else getWindow().getDecorView().setSystemUiVisibility(0);
|
||||
visibility = View.SYSTEM_UI_FLAG_LAYOUT_STABLE
|
||||
| View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
|
||||
| View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION;
|
||||
} else {
|
||||
visibility = View.STATUS_BAR_VISIBLE;
|
||||
}
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
||||
@ColorInt final int systenUiColor =
|
||||
ActivityCompat.getColor(getApplicationContext(), R.color.video_overlay_color);
|
||||
getWindow().setStatusBarColor(systenUiColor);
|
||||
getWindow().setNavigationBarColor(systenUiColor);
|
||||
}
|
||||
|
||||
getWindow().getDecorView().setSystemUiVisibility(visibility);
|
||||
getWindow().clearFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN);
|
||||
}
|
||||
|
||||
private void hideSystemUi() {
|
||||
if (DEBUG) Log.d(TAG, "hideSystemUi() called");
|
||||
if (android.os.Build.VERSION.SDK_INT >= 16) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
|
||||
int visibility = View.SYSTEM_UI_FLAG_LAYOUT_STABLE
|
||||
| View.SYSTEM_UI_FLAG_FULLSCREEN
|
||||
| View.SYSTEM_UI_FLAG_HIDE_NAVIGATION
|
||||
| View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
|
||||
| View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
|
||||
| View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN;
|
||||
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.KITKAT) visibility |= View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY;
|
||||
| View.SYSTEM_UI_FLAG_FULLSCREEN
|
||||
| View.SYSTEM_UI_FLAG_HIDE_NAVIGATION;
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
|
||||
visibility |= View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY;
|
||||
}
|
||||
getWindow().getDecorView().setSystemUiVisibility(visibility);
|
||||
}
|
||||
getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN, WindowManager.LayoutParams.FLAG_FULLSCREEN);
|
||||
getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN,
|
||||
WindowManager.LayoutParams.FLAG_FULLSCREEN);
|
||||
}
|
||||
|
||||
private void toggleOrientation() {
|
||||
setLandScape(!isLandScape());
|
||||
setLandscape(!isLandscape());
|
||||
defaultPreferences.edit()
|
||||
.putBoolean(getString(R.string.last_orientation_landscape_key), !isLandScape())
|
||||
.putBoolean(getString(R.string.last_orientation_landscape_key), !isLandscape())
|
||||
.apply();
|
||||
}
|
||||
|
||||
private boolean isLandScape() {
|
||||
private boolean isLandscape() {
|
||||
return getResources().getDisplayMetrics().heightPixels < getResources().getDisplayMetrics().widthPixels;
|
||||
}
|
||||
|
||||
private void setLandScape(boolean v) {
|
||||
private void setLandscape(boolean v) {
|
||||
setRequestedOrientation(v
|
||||
? ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE
|
||||
: ActivityInfo.SCREEN_ORIENTATION_SENSOR_PORTRAIT);
|
||||
@@ -279,6 +326,19 @@ public final class MainVideoPlayer extends Activity {
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isInMultiWindow() {
|
||||
return Build.VERSION.SDK_INT >= Build.VERSION_CODES.N && isInMultiWindowMode();
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////
|
||||
// Playback Parameters Listener
|
||||
////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
@Override
|
||||
public void onPlaybackParameterChanged(float playbackTempo, float playbackPitch) {
|
||||
if (playerImpl != null) playerImpl.setPlaybackParameters(playbackTempo, playbackPitch);
|
||||
}
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////
|
||||
|
||||
@SuppressWarnings({"unused", "WeakerAccess"})
|
||||
@@ -307,6 +367,7 @@ public final class MainVideoPlayer extends Activity {
|
||||
private ImageButton switchPopupButton;
|
||||
private ImageButton switchBackgroundButton;
|
||||
|
||||
private RelativeLayout windowRootLayout;
|
||||
private View secondaryControls;
|
||||
|
||||
VideoPlayerImpl(final Context context) {
|
||||
@@ -334,6 +395,10 @@ public final class MainVideoPlayer extends Activity {
|
||||
this.switchBackgroundButton = rootView.findViewById(R.id.switchBackground);
|
||||
this.switchPopupButton = rootView.findViewById(R.id.switchPopup);
|
||||
|
||||
this.queueLayout = findViewById(R.id.playQueuePanel);
|
||||
this.itemsListCloseButton = findViewById(R.id.playQueueClose);
|
||||
this.itemsList = findViewById(R.id.playQueue);
|
||||
|
||||
titleTextView.setSelected(true);
|
||||
channelTextView.setSelected(true);
|
||||
|
||||
@@ -342,20 +407,15 @@ public final class MainVideoPlayer extends Activity {
|
||||
|
||||
@Override
|
||||
protected void setupSubtitleView(@NonNull SubtitleView view,
|
||||
@NonNull String captionSizeKey) {
|
||||
final float captionRatioInverse;
|
||||
if (captionSizeKey.equals(getString(R.string.smaller_caption_size_key))) {
|
||||
captionRatioInverse = 22f;
|
||||
} else if (captionSizeKey.equals(getString(R.string.larger_caption_size_key))) {
|
||||
captionRatioInverse = 18f;
|
||||
} else {
|
||||
captionRatioInverse = 20f;
|
||||
}
|
||||
|
||||
final float captionScale,
|
||||
@NonNull final CaptionStyleCompat captionStyle) {
|
||||
final DisplayMetrics metrics = context.getResources().getDisplayMetrics();
|
||||
final int minimumLength = Math.min(metrics.heightPixels, metrics.widthPixels);
|
||||
final float captionRatioInverse = 20f + 4f * (1f - captionScale);
|
||||
view.setFixedTextSize(TypedValue.COMPLEX_UNIT_PX,
|
||||
(float) minimumLength / captionRatioInverse);
|
||||
view.setApplyEmbeddedStyles(captionStyle.equals(CaptionStyleCompat.DEFAULT));
|
||||
view.setStyle(captionStyle);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -391,31 +451,32 @@ public final class MainVideoPlayer extends Activity {
|
||||
updatePlaybackButtons();
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Playback Listener
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
@Override
|
||||
public void shutdown() {
|
||||
super.shutdown();
|
||||
finish();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void sync(@NonNull final PlayQueueItem item, @Nullable final StreamInfo info) {
|
||||
super.sync(item, info);
|
||||
titleTextView.setText(getVideoTitle());
|
||||
channelTextView.setText(getUploaderName());
|
||||
|
||||
//playPauseButton.setImageResource(R.drawable.ic_pause_white);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onShuffleClicked() {
|
||||
super.onShuffleClicked();
|
||||
updatePlaybackButtons();
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Playback Listener
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
protected void onMetadataChanged(@NonNull final PlayQueueItem item,
|
||||
@Nullable final StreamInfo info,
|
||||
final int newPlayQueueIndex,
|
||||
final boolean hasPlayQueueItemChanged) {
|
||||
super.onMetadataChanged(item, info, newPlayQueueIndex, false);
|
||||
|
||||
titleTextView.setText(getVideoTitle());
|
||||
channelTextView.setText(getUploaderName());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPlaybackShutdown() {
|
||||
super.onPlaybackShutdown();
|
||||
finish();
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Player Overrides
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
@@ -475,7 +536,7 @@ public final class MainVideoPlayer extends Activity {
|
||||
public void onClick(View v) {
|
||||
super.onClick(v);
|
||||
if (v.getId() == playPauseButton.getId()) {
|
||||
onVideoPlayPause();
|
||||
onPlayPause();
|
||||
|
||||
} else if (v.getId() == playPreviousButton.getId()) {
|
||||
onPlayPrevious();
|
||||
@@ -508,9 +569,9 @@ public final class MainVideoPlayer extends Activity {
|
||||
|
||||
if (getCurrentState() != STATE_COMPLETED) {
|
||||
getControlsVisibilityHandler().removeCallbacksAndMessages(null);
|
||||
animateView(getControlsRoot(), true, 300, 0, () -> {
|
||||
animateView(getControlsRoot(), true, DEFAULT_CONTROLS_DURATION, 0, () -> {
|
||||
if (getCurrentState() == STATE_PLAYING && !isSomePopupMenuVisible()) {
|
||||
hideControls(300, DEFAULT_CONTROLS_HIDE_TIME);
|
||||
hideControls(DEFAULT_CONTROLS_DURATION, DEFAULT_CONTROLS_HIDE_TIME);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -524,29 +585,28 @@ public final class MainVideoPlayer extends Activity {
|
||||
updatePlaybackButtons();
|
||||
|
||||
getControlsRoot().setVisibility(View.INVISIBLE);
|
||||
queueLayout.setVisibility(View.VISIBLE);
|
||||
animateView(queueLayout, SLIDE_AND_ALPHA, /*visible=*/true,
|
||||
DEFAULT_CONTROLS_DURATION);
|
||||
|
||||
itemsList.scrollToPosition(playQueue.getIndex());
|
||||
}
|
||||
|
||||
private void onQueueClosed() {
|
||||
queueLayout.setVisibility(View.GONE);
|
||||
animateView(queueLayout, SLIDE_AND_ALPHA, /*visible=*/false,
|
||||
DEFAULT_CONTROLS_DURATION);
|
||||
queueVisible = false;
|
||||
}
|
||||
|
||||
private void onMoreOptionsClicked() {
|
||||
if (DEBUG) Log.d(TAG, "onMoreOptionsClicked() called");
|
||||
|
||||
if (secondaryControls.getVisibility() == View.VISIBLE) {
|
||||
moreOptionsButton.setImageDrawable(getResources().getDrawable(
|
||||
R.drawable.ic_expand_more_white_24dp));
|
||||
animateView(secondaryControls, false, 200);
|
||||
} else {
|
||||
moreOptionsButton.setImageDrawable(getResources().getDrawable(
|
||||
R.drawable.ic_expand_less_white_24dp));
|
||||
animateView(secondaryControls, true, 200);
|
||||
}
|
||||
showControls(300);
|
||||
final boolean isMoreControlsVisible = secondaryControls.getVisibility() == View.VISIBLE;
|
||||
|
||||
animateRotation(moreOptionsButton, DEFAULT_CONTROLS_DURATION,
|
||||
isMoreControlsVisible ? 0 : 180);
|
||||
animateView(secondaryControls, SLIDE_AND_ALPHA, !isMoreControlsVisible,
|
||||
DEFAULT_CONTROLS_DURATION);
|
||||
showControls(DEFAULT_CONTROLS_DURATION);
|
||||
}
|
||||
|
||||
private void onScreenRotationClicked() {
|
||||
@@ -555,18 +615,23 @@ public final class MainVideoPlayer extends Activity {
|
||||
showControlsThenHide();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPlaybackSpeedClicked() {
|
||||
PlaybackParameterDialog.newInstance(getPlaybackSpeed(), getPlaybackPitch())
|
||||
.show(getSupportFragmentManager(), TAG);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStopTrackingTouch(SeekBar seekBar) {
|
||||
super.onStopTrackingTouch(seekBar);
|
||||
if (wasPlaying()) {
|
||||
hideControls(100, 0);
|
||||
}
|
||||
if (wasPlaying()) showControlsThenHide();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDismiss(PopupMenu menu) {
|
||||
super.onDismiss(menu);
|
||||
if (isPlaying()) hideControls(300, 0);
|
||||
if (isPlaying()) hideControls(DEFAULT_CONTROLS_DURATION, 0);
|
||||
hideSystemUi();
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -589,7 +654,7 @@ public final class MainVideoPlayer extends Activity {
|
||||
@Override
|
||||
protected int getOverrideResolutionIndex(final List<VideoStream> sortedVideos,
|
||||
final String playbackQuality) {
|
||||
return ListHelper.getDefaultResolutionIndex(context, sortedVideos, playbackQuality);
|
||||
return ListHelper.getResolutionIndex(context, sortedVideos, playbackQuality);
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
@@ -624,7 +689,7 @@ public final class MainVideoPlayer extends Activity {
|
||||
playPauseButton.setImageResource(R.drawable.ic_pause_white);
|
||||
animatePlayButtons(true, 200);
|
||||
});
|
||||
showSystemUi();
|
||||
|
||||
getRootView().setKeepScreenOn(true);
|
||||
}
|
||||
|
||||
@@ -650,10 +715,9 @@ public final class MainVideoPlayer extends Activity {
|
||||
|
||||
@Override
|
||||
public void onCompleted() {
|
||||
showSystemUi();
|
||||
animateView(playPauseButton, AnimationUtils.Type.SCALE_AND_ALPHA, false, 0, 0, () -> {
|
||||
playPauseButton.setImageResource(R.drawable.ic_replay_white);
|
||||
animatePlayButtons(true, 300);
|
||||
animatePlayButtons(true, DEFAULT_CONTROLS_DURATION);
|
||||
});
|
||||
|
||||
getRootView().setKeepScreenOn(false);
|
||||
@@ -683,8 +747,9 @@ public final class MainVideoPlayer extends Activity {
|
||||
if (DEBUG) Log.d(TAG, "hideControls() called with: delay = [" + delay + "]");
|
||||
getControlsVisibilityHandler().removeCallbacksAndMessages(null);
|
||||
getControlsVisibilityHandler().postDelayed(() ->
|
||||
animateView(getControlsRoot(), false, duration, 0, MainVideoPlayer.this::hideSystemUi),
|
||||
delay
|
||||
animateView(getControlsRoot(), false, duration, 0,
|
||||
MainVideoPlayer.this::hideSystemUi),
|
||||
/*delayMillis=*/delay
|
||||
);
|
||||
}
|
||||
|
||||
@@ -697,11 +762,6 @@ public final class MainVideoPlayer extends Activity {
|
||||
}
|
||||
|
||||
private void buildQueue() {
|
||||
queueLayout = findViewById(R.id.playQueuePanel);
|
||||
|
||||
itemsListCloseButton = findViewById(R.id.playQueueClose);
|
||||
|
||||
itemsList = findViewById(R.id.playQueue);
|
||||
itemsList.setAdapter(playQueueAdapter);
|
||||
itemsList.setClickable(true);
|
||||
itemsList.setLongClickable(true);
|
||||
@@ -731,31 +791,11 @@ public final class MainVideoPlayer extends Activity {
|
||||
}
|
||||
|
||||
private ItemTouchHelper.SimpleCallback getItemTouchCallback() {
|
||||
return new ItemTouchHelper.SimpleCallback(ItemTouchHelper.UP | ItemTouchHelper.DOWN, 0) {
|
||||
return new PlayQueueItemTouchCallback() {
|
||||
@Override
|
||||
public boolean onMove(RecyclerView recyclerView, RecyclerView.ViewHolder source, RecyclerView.ViewHolder target) {
|
||||
if (source.getItemViewType() != target.getItemViewType()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
final int sourceIndex = source.getLayoutPosition();
|
||||
final int targetIndex = target.getLayoutPosition();
|
||||
playQueue.move(sourceIndex, targetIndex);
|
||||
return true;
|
||||
public void onMove(int sourceIndex, int targetIndex) {
|
||||
if (playQueue != null) playQueue.move(sourceIndex, targetIndex);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isLongPressDragEnabled() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isItemViewSwipeEnabled() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSwiped(RecyclerView.ViewHolder viewHolder, int swipeDir) {}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -816,10 +856,12 @@ public final class MainVideoPlayer extends Activity {
|
||||
if (DEBUG) Log.d(TAG, "onDoubleTap() called with: e = [" + e + "]" + "rawXy = " + e.getRawX() + ", " + e.getRawY() + ", xy = " + e.getX() + ", " + e.getY());
|
||||
if (!playerImpl.isPlaying()) return false;
|
||||
|
||||
if (e.getX() > playerImpl.getRootView().getWidth() / 2) {
|
||||
if (e.getX() > playerImpl.getRootView().getWidth() * 2 / 3) {
|
||||
playerImpl.onFastForward();
|
||||
} else {
|
||||
} else if (e.getX() < playerImpl.getRootView().getWidth() / 3) {
|
||||
playerImpl.onFastRewind();
|
||||
} else {
|
||||
playerImpl.getPlayPauseButton().performClick();
|
||||
}
|
||||
|
||||
return true;
|
||||
@@ -830,18 +872,28 @@ public final class MainVideoPlayer extends Activity {
|
||||
if (DEBUG) Log.d(TAG, "onSingleTapConfirmed() called with: e = [" + e + "]");
|
||||
if (playerImpl.getCurrentState() == BasePlayer.STATE_BLOCKED) return true;
|
||||
|
||||
if (playerImpl.isControlsVisible()) playerImpl.hideControls(150, 0);
|
||||
else {
|
||||
if (playerImpl.isControlsVisible()) {
|
||||
playerImpl.hideControls(150, 0);
|
||||
} else {
|
||||
playerImpl.showControlsThenHide();
|
||||
showSystemUi();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onDown(MotionEvent e) {
|
||||
if (DEBUG) Log.d(TAG, "onDown() called with: e = [" + e + "]");
|
||||
|
||||
return super.onDown(e);
|
||||
}
|
||||
|
||||
private final boolean isPlayerGestureEnabled = PlayerHelper.isPlayerGestureEnabled(getApplicationContext());
|
||||
|
||||
private final float stepsBrightness = 15, stepBrightness = (1f / stepsBrightness), minBrightness = .01f;
|
||||
private float currentBrightness = .5f;
|
||||
private float currentBrightness = getWindow().getAttributes().screenBrightness > 0
|
||||
? getWindow().getAttributes().screenBrightness
|
||||
: 0.5f;
|
||||
|
||||
private int currentVolume, maxVolume = playerImpl.getAudioReactor().getMaxVolume();
|
||||
private final float stepsVolume = 15, stepVolume = (float) Math.ceil(maxVolume / stepsVolume), minVolume = 0;
|
||||
@@ -916,11 +968,15 @@ public final class MainVideoPlayer extends Activity {
|
||||
eventsNum = 0;
|
||||
/* if (playerImpl.getVolumeTextView().getVisibility() == View.VISIBLE) playerImpl.getVolumeTextView().setVisibility(View.GONE);
|
||||
if (playerImpl.getBrightnessTextView().getVisibility() == View.VISIBLE) playerImpl.getBrightnessTextView().setVisibility(View.GONE);*/
|
||||
if (playerImpl.getVolumeTextView().getVisibility() == View.VISIBLE) animateView(playerImpl.getVolumeTextView(), false, 200, 200);
|
||||
if (playerImpl.getBrightnessTextView().getVisibility() == View.VISIBLE) animateView(playerImpl.getBrightnessTextView(), false, 200, 200);
|
||||
if (playerImpl.getVolumeTextView().getVisibility() == View.VISIBLE) {
|
||||
animateView(playerImpl.getVolumeTextView(), false, 200, 200);
|
||||
}
|
||||
if (playerImpl.getBrightnessTextView().getVisibility() == View.VISIBLE) {
|
||||
animateView(playerImpl.getBrightnessTextView(), false, 200, 200);
|
||||
}
|
||||
|
||||
if (playerImpl.isControlsVisible() && playerImpl.getCurrentState() == BasePlayer.STATE_PLAYING) {
|
||||
playerImpl.hideControls(300, VideoPlayer.DEFAULT_CONTROLS_HIDE_TIME);
|
||||
if (playerImpl.isControlsVisible() && playerImpl.getCurrentState() == STATE_PLAYING) {
|
||||
playerImpl.hideControls(DEFAULT_CONTROLS_DURATION, DEFAULT_CONTROLS_HIDE_TIME);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
68
app/src/main/java/org/schabi/newpipe/player/PlayerState.java
Normal file
68
app/src/main/java/org/schabi/newpipe/player/PlayerState.java
Normal file
@@ -0,0 +1,68 @@
|
||||
package org.schabi.newpipe.player;
|
||||
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.annotation.Nullable;
|
||||
|
||||
import org.schabi.newpipe.player.playqueue.PlayQueue;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
public class PlayerState implements Serializable {
|
||||
|
||||
@NonNull private final PlayQueue playQueue;
|
||||
private final int repeatMode;
|
||||
private final float playbackSpeed;
|
||||
private final float playbackPitch;
|
||||
@Nullable private final String playbackQuality;
|
||||
private final boolean wasPlaying;
|
||||
|
||||
PlayerState(@NonNull final PlayQueue playQueue, final int repeatMode,
|
||||
final float playbackSpeed, final float playbackPitch, final boolean wasPlaying) {
|
||||
this(playQueue, repeatMode, playbackSpeed, playbackPitch, null, wasPlaying);
|
||||
}
|
||||
|
||||
PlayerState(@NonNull final PlayQueue playQueue, final int repeatMode,
|
||||
final float playbackSpeed, final float playbackPitch,
|
||||
@Nullable final String playbackQuality, final boolean wasPlaying) {
|
||||
this.playQueue = playQueue;
|
||||
this.repeatMode = repeatMode;
|
||||
this.playbackSpeed = playbackSpeed;
|
||||
this.playbackPitch = playbackPitch;
|
||||
this.playbackQuality = playbackQuality;
|
||||
this.wasPlaying = wasPlaying;
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Serdes
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Getters
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
@NonNull
|
||||
public PlayQueue getPlayQueue() {
|
||||
return playQueue;
|
||||
}
|
||||
|
||||
public int getRepeatMode() {
|
||||
return repeatMode;
|
||||
}
|
||||
|
||||
public float getPlaybackSpeed() {
|
||||
return playbackSpeed;
|
||||
}
|
||||
|
||||
public float getPlaybackPitch() {
|
||||
return playbackPitch;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public String getPlaybackQuality() {
|
||||
return playbackQuality;
|
||||
}
|
||||
|
||||
public boolean wasPlaying() {
|
||||
return wasPlaying;
|
||||
}
|
||||
}
|
||||
@@ -44,6 +44,7 @@ import android.view.MotionEvent;
|
||||
import android.view.View;
|
||||
import android.view.WindowManager;
|
||||
import android.widget.ImageButton;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.PopupMenu;
|
||||
import android.widget.RemoteViews;
|
||||
import android.widget.SeekBar;
|
||||
@@ -52,6 +53,7 @@ import android.widget.TextView;
|
||||
import com.google.android.exoplayer2.C;
|
||||
import com.google.android.exoplayer2.PlaybackParameters;
|
||||
import com.google.android.exoplayer2.Player;
|
||||
import com.google.android.exoplayer2.text.CaptionStyleCompat;
|
||||
import com.google.android.exoplayer2.ui.AspectRatioFrameLayout;
|
||||
import com.google.android.exoplayer2.ui.SubtitleView;
|
||||
|
||||
@@ -63,13 +65,16 @@ import org.schabi.newpipe.player.event.PlayerEventListener;
|
||||
import org.schabi.newpipe.player.helper.LockManager;
|
||||
import org.schabi.newpipe.player.helper.PlayerHelper;
|
||||
import org.schabi.newpipe.player.old.PlayVideoActivity;
|
||||
import org.schabi.newpipe.playlist.PlayQueueItem;
|
||||
import org.schabi.newpipe.player.playqueue.PlayQueueItem;
|
||||
import org.schabi.newpipe.util.ListHelper;
|
||||
import org.schabi.newpipe.util.NavigationHelper;
|
||||
import org.schabi.newpipe.util.ThemeHelper;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import static org.schabi.newpipe.player.BasePlayer.STATE_PLAYING;
|
||||
import static org.schabi.newpipe.player.VideoPlayer.DEFAULT_CONTROLS_DURATION;
|
||||
import static org.schabi.newpipe.player.VideoPlayer.DEFAULT_CONTROLS_HIDE_TIME;
|
||||
import static org.schabi.newpipe.player.helper.PlayerHelper.isUsingOldPlayer;
|
||||
import static org.schabi.newpipe.util.AnimationUtils.animateView;
|
||||
|
||||
@@ -366,6 +371,7 @@ public final class PopupVideoPlayer extends Service {
|
||||
protected class VideoPlayerImpl extends VideoPlayer implements View.OnLayoutChangeListener {
|
||||
private TextView resizingIndicator;
|
||||
private ImageButton fullScreenButton;
|
||||
private ImageView videoPlayPause;
|
||||
|
||||
private View extraOptionsView;
|
||||
|
||||
@@ -387,6 +393,8 @@ public final class PopupVideoPlayer extends Service {
|
||||
resizingIndicator = rootView.findViewById(R.id.resizing_indicator);
|
||||
fullScreenButton = rootView.findViewById(R.id.fullScreenButton);
|
||||
fullScreenButton.setOnClickListener(v -> onFullScreenButtonClicked());
|
||||
videoPlayPause = rootView.findViewById(R.id.videoPlayPause);
|
||||
videoPlayPause.setOnClickListener(this::onPlayPauseButtonPressed);
|
||||
|
||||
extraOptionsView = rootView.findViewById(R.id.extraOptionsView);
|
||||
rootView.addOnLayoutChangeListener(this);
|
||||
@@ -394,14 +402,16 @@ public final class PopupVideoPlayer extends Service {
|
||||
|
||||
@Override
|
||||
protected void setupSubtitleView(@NonNull SubtitleView view,
|
||||
@NonNull String captionSizeKey) {
|
||||
float captionRatio = SubtitleView.DEFAULT_TEXT_SIZE_FRACTION;
|
||||
if (captionSizeKey.equals(getString(R.string.smaller_caption_size_key))) {
|
||||
captionRatio *= 0.9;
|
||||
} else if (captionSizeKey.equals(getString(R.string.larger_caption_size_key))) {
|
||||
captionRatio *= 1.1;
|
||||
}
|
||||
view.setFractionalTextSize(captionRatio);
|
||||
final float captionScale,
|
||||
@NonNull final CaptionStyleCompat captionStyle) {
|
||||
float captionRatio = (captionScale - 1f) / 5f + 1f;
|
||||
view.setFractionalTextSize(SubtitleView.DEFAULT_TEXT_SIZE_FRACTION * captionRatio);
|
||||
view.setApplyEmbeddedStyles(captionStyle.equals(CaptionStyleCompat.DEFAULT));
|
||||
view.setStyle(captionStyle);
|
||||
}
|
||||
|
||||
private void onPlayPauseButtonPressed(View ib) {
|
||||
onPlayPause();
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -419,13 +429,15 @@ public final class PopupVideoPlayer extends Service {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onThumbnailReceived(Bitmap thumbnail) {
|
||||
super.onThumbnailReceived(thumbnail);
|
||||
if (thumbnail != null) {
|
||||
public void onLoadingComplete(String imageUri, View view, Bitmap loadedImage) {
|
||||
super.onLoadingComplete(imageUri, view, loadedImage);
|
||||
if (loadedImage != null) {
|
||||
// rebuild notification here since remote view does not release bitmaps, causing memory leaks
|
||||
notBuilder = createNotification();
|
||||
|
||||
if (notRemoteView != null) notRemoteView.setImageViewBitmap(R.id.notificationCover, thumbnail);
|
||||
if (notRemoteView != null) {
|
||||
notRemoteView.setImageViewBitmap(R.id.notificationCover, loadedImage);
|
||||
}
|
||||
|
||||
updateNotification(-1);
|
||||
}
|
||||
@@ -505,7 +517,7 @@ public final class PopupVideoPlayer extends Service {
|
||||
@Override
|
||||
protected int getOverrideResolutionIndex(final List<VideoStream> sortedVideos,
|
||||
final String playbackQuality) {
|
||||
return ListHelper.getPopupDefaultResolutionIndex(context, sortedVideos, playbackQuality);
|
||||
return ListHelper.getPopupResolutionIndex(context, sortedVideos, playbackQuality);
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
@@ -533,7 +545,8 @@ public final class PopupVideoPlayer extends Service {
|
||||
|
||||
private void updatePlayback() {
|
||||
if (activityListener != null && simpleExoPlayer != null && playQueue != null) {
|
||||
activityListener.onPlaybackUpdate(currentState, getRepeatMode(), playQueue.isShuffled(), simpleExoPlayer.getPlaybackParameters());
|
||||
activityListener.onPlaybackUpdate(currentState, getRepeatMode(),
|
||||
playQueue.isShuffled(), simpleExoPlayer.getPlaybackParameters());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -572,16 +585,17 @@ public final class PopupVideoPlayer extends Service {
|
||||
// Playback Listener
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
@Override
|
||||
public void sync(@NonNull PlayQueueItem item, @Nullable StreamInfo info) {
|
||||
if (currentItem == item && currentInfo == info) return;
|
||||
super.sync(item, info);
|
||||
protected void onMetadataChanged(@NonNull final PlayQueueItem item,
|
||||
@Nullable final StreamInfo info,
|
||||
final int newPlayQueueIndex,
|
||||
final boolean hasPlayQueueItemChanged) {
|
||||
super.onMetadataChanged(item, info, newPlayQueueIndex, false);
|
||||
updateMetadata();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void shutdown() {
|
||||
super.shutdown();
|
||||
public void onPlaybackShutdown() {
|
||||
super.onPlaybackShutdown();
|
||||
onClose();
|
||||
}
|
||||
|
||||
@@ -611,7 +625,7 @@ public final class PopupVideoPlayer extends Service {
|
||||
onClose();
|
||||
break;
|
||||
case ACTION_PLAY_PAUSE:
|
||||
onVideoPlayPause();
|
||||
onPlayPause();
|
||||
break;
|
||||
case ACTION_REPEAT:
|
||||
onRepeatClicked();
|
||||
@@ -645,7 +659,10 @@ public final class PopupVideoPlayer extends Service {
|
||||
public void onPlaying() {
|
||||
super.onPlaying();
|
||||
updateNotification(R.drawable.ic_pause_white);
|
||||
videoPlayPause.setBackgroundResource(R.drawable.ic_pause_white);
|
||||
lockManager.acquireWifiAndCpu();
|
||||
|
||||
hideControls(DEFAULT_CONTROLS_DURATION, DEFAULT_CONTROLS_HIDE_TIME);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -658,13 +675,14 @@ public final class PopupVideoPlayer extends Service {
|
||||
public void onPaused() {
|
||||
super.onPaused();
|
||||
updateNotification(R.drawable.ic_play_arrow_white);
|
||||
showAndAnimateControl(R.drawable.ic_play_arrow_white, false);
|
||||
videoPlayPause.setBackgroundResource(R.drawable.ic_play_arrow_white);
|
||||
lockManager.releaseWifiAndCpu();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPausedSeek() {
|
||||
super.onPausedSeek();
|
||||
videoPlayPause.setBackgroundResource(R.drawable.ic_pause_white);
|
||||
updateNotification(R.drawable.ic_play_arrow_white);
|
||||
}
|
||||
|
||||
@@ -672,10 +690,27 @@ public final class PopupVideoPlayer extends Service {
|
||||
public void onCompleted() {
|
||||
super.onCompleted();
|
||||
updateNotification(R.drawable.ic_replay_white);
|
||||
showAndAnimateControl(R.drawable.ic_replay_white, false);
|
||||
videoPlayPause.setBackgroundResource(R.drawable.ic_replay_white);
|
||||
lockManager.releaseWifiAndCpu();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void showControlsThenHide() {
|
||||
videoPlayPause.setVisibility(View.VISIBLE);
|
||||
super.showControlsThenHide();
|
||||
}
|
||||
|
||||
public void showControls(long duration) {
|
||||
videoPlayPause.setVisibility(View.VISIBLE);
|
||||
super.showControls(duration);
|
||||
}
|
||||
|
||||
public void hideControls(final long duration, long delay) {
|
||||
super.hideControlsAndButton(duration, delay, videoPlayPause);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Utils
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
@@ -707,7 +742,9 @@ public final class PopupVideoPlayer extends Service {
|
||||
public boolean onDoubleTap(MotionEvent e) {
|
||||
if (DEBUG)
|
||||
Log.d(TAG, "onDoubleTap() called with: e = [" + e + "]" + "rawXy = " + e.getRawX() + ", " + e.getRawY() + ", xy = " + e.getX() + ", " + e.getY());
|
||||
if (playerImpl == null || !playerImpl.isPlaying() || !playerImpl.isPlayerReady()) return false;
|
||||
if (playerImpl == null || !playerImpl.isPlaying()) return false;
|
||||
|
||||
playerImpl.hideControls(0, 0);
|
||||
|
||||
if (e.getX() > popupWidth / 2) {
|
||||
playerImpl.onFastForward();
|
||||
@@ -722,7 +759,12 @@ public final class PopupVideoPlayer extends Service {
|
||||
public boolean onSingleTapConfirmed(MotionEvent e) {
|
||||
if (DEBUG) Log.d(TAG, "onSingleTapConfirmed() called with: e = [" + e + "]");
|
||||
if (playerImpl == null || playerImpl.getPlayer() == null) return false;
|
||||
playerImpl.onVideoPlayPause();
|
||||
if (playerImpl.isControlsVisible()) {
|
||||
playerImpl.hideControls(100, 100);
|
||||
} else {
|
||||
playerImpl.showControlsThenHide();
|
||||
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -778,8 +820,8 @@ public final class PopupVideoPlayer extends Service {
|
||||
private void onScrollEnd() {
|
||||
if (DEBUG) Log.d(TAG, "onScrollEnd() called");
|
||||
if (playerImpl == null) return;
|
||||
if (playerImpl.isControlsVisible() && playerImpl.getCurrentState() == BasePlayer.STATE_PLAYING) {
|
||||
playerImpl.hideControls(300, VideoPlayer.DEFAULT_CONTROLS_HIDE_TIME);
|
||||
if (playerImpl.isControlsVisible() && playerImpl.getCurrentState() == STATE_PLAYING) {
|
||||
playerImpl.hideControls(DEFAULT_CONTROLS_DURATION, DEFAULT_CONTROLS_HIDE_TIME);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -29,20 +29,27 @@ import com.google.android.exoplayer2.Player;
|
||||
import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.extractor.stream.StreamInfo;
|
||||
import org.schabi.newpipe.fragments.OnScrollBelowItemsListener;
|
||||
import org.schabi.newpipe.fragments.local.dialog.PlaylistAppendDialog;
|
||||
import org.schabi.newpipe.local.dialog.PlaylistAppendDialog;
|
||||
import org.schabi.newpipe.player.event.PlayerEventListener;
|
||||
import org.schabi.newpipe.playlist.PlayQueueItem;
|
||||
import org.schabi.newpipe.playlist.PlayQueueItemBuilder;
|
||||
import org.schabi.newpipe.playlist.PlayQueueItemHolder;
|
||||
import org.schabi.newpipe.player.helper.PlaybackParameterDialog;
|
||||
import org.schabi.newpipe.player.playqueue.PlayQueueAdapter;
|
||||
import org.schabi.newpipe.player.playqueue.PlayQueueItem;
|
||||
import org.schabi.newpipe.player.playqueue.PlayQueueItemBuilder;
|
||||
import org.schabi.newpipe.player.playqueue.PlayQueueItemHolder;
|
||||
import org.schabi.newpipe.player.playqueue.PlayQueueItemTouchCallback;
|
||||
import org.schabi.newpipe.util.Localization;
|
||||
import org.schabi.newpipe.util.NavigationHelper;
|
||||
import org.schabi.newpipe.util.ThemeHelper;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
import static org.schabi.newpipe.player.helper.PlayerHelper.formatPitch;
|
||||
import static org.schabi.newpipe.player.helper.PlayerHelper.formatSpeed;
|
||||
|
||||
public abstract class ServicePlayerActivity extends AppCompatActivity
|
||||
implements PlayerEventListener, SeekBar.OnSeekBarChangeListener, View.OnClickListener {
|
||||
implements PlayerEventListener, SeekBar.OnSeekBarChangeListener,
|
||||
View.OnClickListener, PlaybackParameterDialog.Callback {
|
||||
|
||||
private boolean serviceBound;
|
||||
private ServiceConnection serviceConnection;
|
||||
@@ -56,14 +63,9 @@ public abstract class ServicePlayerActivity extends AppCompatActivity
|
||||
////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
private static final int RECYCLER_ITEM_POPUP_MENU_GROUP_ID = 47;
|
||||
private static final int PLAYBACK_SPEED_POPUP_MENU_GROUP_ID = 61;
|
||||
private static final int PLAYBACK_PITCH_POPUP_MENU_GROUP_ID = 97;
|
||||
|
||||
private static final int SMOOTH_SCROLL_MAXIMUM_DISTANCE = 80;
|
||||
|
||||
private static final int MINIMUM_INITIAL_DRAG_VELOCITY = 10;
|
||||
private static final int MAXIMUM_INITIAL_DRAG_VELOCITY = 25;
|
||||
|
||||
private View rootView;
|
||||
|
||||
private RecyclerView itemsList;
|
||||
@@ -76,6 +78,7 @@ public abstract class ServicePlayerActivity extends AppCompatActivity
|
||||
private SeekBar progressSeekBar;
|
||||
private TextView progressCurrentTime;
|
||||
private TextView progressEndTime;
|
||||
private TextView progressLiveSync;
|
||||
private TextView seekDisplay;
|
||||
|
||||
private ImageButton repeatButton;
|
||||
@@ -86,9 +89,7 @@ public abstract class ServicePlayerActivity extends AppCompatActivity
|
||||
private ProgressBar progressBar;
|
||||
|
||||
private TextView playbackSpeedButton;
|
||||
private PopupMenu playbackSpeedPopupMenu;
|
||||
private TextView playbackPitchButton;
|
||||
private PopupMenu playbackPitchPopupMenu;
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////
|
||||
// Abstracts
|
||||
@@ -154,7 +155,7 @@ public abstract class ServicePlayerActivity extends AppCompatActivity
|
||||
finish();
|
||||
return true;
|
||||
case R.id.action_append_playlist:
|
||||
appendToPlaylist();
|
||||
appendAllToPlaylist();
|
||||
return true;
|
||||
case R.id.action_settings:
|
||||
NavigationHelper.openSettings(this);
|
||||
@@ -190,13 +191,6 @@ public abstract class ServicePlayerActivity extends AppCompatActivity
|
||||
).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||
}
|
||||
|
||||
private void appendToPlaylist() {
|
||||
if (this.player != null && this.player.getPlayQueue() != null) {
|
||||
PlaylistAppendDialog.fromPlayQueueItems(this.player.getPlayQueue().getStreams())
|
||||
.show(getSupportFragmentManager(), getTag());
|
||||
}
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////
|
||||
// Service Connection
|
||||
////////////////////////////////////////////////////////////////////////////
|
||||
@@ -294,9 +288,11 @@ public abstract class ServicePlayerActivity extends AppCompatActivity
|
||||
progressCurrentTime = rootView.findViewById(R.id.current_time);
|
||||
progressSeekBar = rootView.findViewById(R.id.seek_bar);
|
||||
progressEndTime = rootView.findViewById(R.id.end_time);
|
||||
progressLiveSync = rootView.findViewById(R.id.live_sync);
|
||||
seekDisplay = rootView.findViewById(R.id.seek_display);
|
||||
|
||||
progressSeekBar.setOnSeekBarChangeListener(this);
|
||||
progressLiveSync.setOnClickListener(this);
|
||||
}
|
||||
|
||||
private void buildControls() {
|
||||
@@ -316,50 +312,12 @@ public abstract class ServicePlayerActivity extends AppCompatActivity
|
||||
shuffleButton.setOnClickListener(this);
|
||||
playbackSpeedButton.setOnClickListener(this);
|
||||
playbackPitchButton.setOnClickListener(this);
|
||||
|
||||
playbackSpeedPopupMenu = new PopupMenu(this, playbackSpeedButton);
|
||||
playbackPitchPopupMenu = new PopupMenu(this, playbackPitchButton);
|
||||
buildPlaybackSpeedMenu();
|
||||
buildPlaybackPitchMenu();
|
||||
}
|
||||
|
||||
private void buildPlaybackSpeedMenu() {
|
||||
if (playbackSpeedPopupMenu == null) return;
|
||||
|
||||
playbackSpeedPopupMenu.getMenu().removeGroup(PLAYBACK_SPEED_POPUP_MENU_GROUP_ID);
|
||||
for (int i = 0; i < BasePlayer.PLAYBACK_SPEEDS.length; i++) {
|
||||
final float playbackSpeed = BasePlayer.PLAYBACK_SPEEDS[i];
|
||||
final String formattedSpeed = formatSpeed(playbackSpeed);
|
||||
final MenuItem item = playbackSpeedPopupMenu.getMenu().add(PLAYBACK_SPEED_POPUP_MENU_GROUP_ID, i, Menu.NONE, formattedSpeed);
|
||||
item.setOnMenuItemClickListener(menuItem -> {
|
||||
if (player == null) return false;
|
||||
|
||||
player.setPlaybackSpeed(playbackSpeed);
|
||||
return true;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private void buildPlaybackPitchMenu() {
|
||||
if (playbackPitchPopupMenu == null) return;
|
||||
|
||||
playbackPitchPopupMenu.getMenu().removeGroup(PLAYBACK_PITCH_POPUP_MENU_GROUP_ID);
|
||||
for (int i = 0; i < BasePlayer.PLAYBACK_PITCHES.length; i++) {
|
||||
final float playbackPitch = BasePlayer.PLAYBACK_PITCHES[i];
|
||||
final String formattedPitch = formatPitch(playbackPitch);
|
||||
final MenuItem item = playbackPitchPopupMenu.getMenu().add(PLAYBACK_PITCH_POPUP_MENU_GROUP_ID, i, Menu.NONE, formattedPitch);
|
||||
item.setOnMenuItemClickListener(menuItem -> {
|
||||
if (player == null) return false;
|
||||
|
||||
player.setPlaybackPitch(playbackPitch);
|
||||
return true;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private void buildItemPopupMenu(final PlayQueueItem item, final View view) {
|
||||
final PopupMenu menu = new PopupMenu(this, view);
|
||||
final MenuItem remove = menu.getMenu().add(RECYCLER_ITEM_POPUP_MENU_GROUP_ID, 0, Menu.NONE, R.string.play_queue_remove);
|
||||
final MenuItem remove = menu.getMenu().add(RECYCLER_ITEM_POPUP_MENU_GROUP_ID, /*pos=*/0,
|
||||
Menu.NONE, R.string.play_queue_remove);
|
||||
remove.setOnMenuItemClickListener(menuItem -> {
|
||||
if (player == null) return false;
|
||||
|
||||
@@ -368,12 +326,20 @@ public abstract class ServicePlayerActivity extends AppCompatActivity
|
||||
return true;
|
||||
});
|
||||
|
||||
final MenuItem detail = menu.getMenu().add(RECYCLER_ITEM_POPUP_MENU_GROUP_ID, 1, Menu.NONE, R.string.play_queue_stream_detail);
|
||||
final MenuItem detail = menu.getMenu().add(RECYCLER_ITEM_POPUP_MENU_GROUP_ID, /*pos=*/1,
|
||||
Menu.NONE, R.string.play_queue_stream_detail);
|
||||
detail.setOnMenuItemClickListener(menuItem -> {
|
||||
onOpenDetail(item.getServiceId(), item.getUrl(), item.getTitle());
|
||||
return true;
|
||||
});
|
||||
|
||||
final MenuItem append = menu.getMenu().add(RECYCLER_ITEM_POPUP_MENU_GROUP_ID, /*pos=*/2,
|
||||
Menu.NONE, R.string.append_playlist);
|
||||
append.setOnMenuItemClickListener(menuItem -> {
|
||||
openPlaylistAppendDialog(Collections.singletonList(item));
|
||||
return true;
|
||||
});
|
||||
|
||||
menu.show();
|
||||
}
|
||||
|
||||
@@ -395,43 +361,11 @@ public abstract class ServicePlayerActivity extends AppCompatActivity
|
||||
}
|
||||
|
||||
private ItemTouchHelper.SimpleCallback getItemTouchCallback() {
|
||||
return new ItemTouchHelper.SimpleCallback(ItemTouchHelper.UP | ItemTouchHelper.DOWN, 0) {
|
||||
return new PlayQueueItemTouchCallback() {
|
||||
@Override
|
||||
public int interpolateOutOfBoundsScroll(RecyclerView recyclerView, int viewSize,
|
||||
int viewSizeOutOfBounds, int totalSize,
|
||||
long msSinceStartScroll) {
|
||||
final int standardSpeed = super.interpolateOutOfBoundsScroll(recyclerView, viewSize,
|
||||
viewSizeOutOfBounds, totalSize, msSinceStartScroll);
|
||||
final int clampedAbsVelocity = Math.max(MINIMUM_INITIAL_DRAG_VELOCITY,
|
||||
Math.min(Math.abs(standardSpeed), MAXIMUM_INITIAL_DRAG_VELOCITY));
|
||||
return clampedAbsVelocity * (int) Math.signum(viewSizeOutOfBounds);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onMove(RecyclerView recyclerView, RecyclerView.ViewHolder source,
|
||||
RecyclerView.ViewHolder target) {
|
||||
if (source.getItemViewType() != target.getItemViewType()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
final int sourceIndex = source.getLayoutPosition();
|
||||
final int targetIndex = target.getLayoutPosition();
|
||||
public void onMove(int sourceIndex, int targetIndex) {
|
||||
if (player != null) player.getPlayQueue().move(sourceIndex, targetIndex);
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isLongPressDragEnabled() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isItemViewSwipeEnabled() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSwiped(RecyclerView.ViewHolder viewHolder, int swipeDir) {}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -496,7 +430,7 @@ public abstract class ServicePlayerActivity extends AppCompatActivity
|
||||
player.onPlayPrevious();
|
||||
|
||||
} else if (view.getId() == playPauseButton.getId()) {
|
||||
player.onVideoPlayPause();
|
||||
player.onPlayPause();
|
||||
|
||||
} else if (view.getId() == forwardButton.getId()) {
|
||||
player.onPlayNext();
|
||||
@@ -505,17 +439,35 @@ public abstract class ServicePlayerActivity extends AppCompatActivity
|
||||
player.onShuffleClicked();
|
||||
|
||||
} else if (view.getId() == playbackSpeedButton.getId()) {
|
||||
playbackSpeedPopupMenu.show();
|
||||
openPlaybackParameterDialog();
|
||||
|
||||
} else if (view.getId() == playbackPitchButton.getId()) {
|
||||
playbackPitchPopupMenu.show();
|
||||
openPlaybackParameterDialog();
|
||||
|
||||
} else if (view.getId() == metadata.getId()) {
|
||||
scrollToSelected();
|
||||
|
||||
} else if (view.getId() == progressLiveSync.getId()) {
|
||||
player.seekToDefault();
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////
|
||||
// Playback Parameters
|
||||
////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
private void openPlaybackParameterDialog() {
|
||||
if (player == null) return;
|
||||
PlaybackParameterDialog.newInstance(player.getPlaybackSpeed(),
|
||||
player.getPlaybackPitch()).show(getSupportFragmentManager(), getTag());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPlaybackParameterChanged(float playbackTempo, float playbackPitch) {
|
||||
if (player != null) player.setPlaybackParameters(playbackTempo, playbackPitch);
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////
|
||||
// Seekbar Listener
|
||||
////////////////////////////////////////////////////////////////////////////
|
||||
@@ -537,11 +489,26 @@ public abstract class ServicePlayerActivity extends AppCompatActivity
|
||||
|
||||
@Override
|
||||
public void onStopTrackingTouch(SeekBar seekBar) {
|
||||
if (player != null) player.simpleExoPlayer.seekTo(seekBar.getProgress());
|
||||
if (player != null) player.seekTo(seekBar.getProgress());
|
||||
seekDisplay.setVisibility(View.GONE);
|
||||
seeking = false;
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////
|
||||
// Playlist append
|
||||
////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
private void appendAllToPlaylist() {
|
||||
if (player != null && player.getPlayQueue() != null) {
|
||||
openPlaylistAppendDialog(player.getPlayQueue().getStreams());
|
||||
}
|
||||
}
|
||||
|
||||
private void openPlaylistAppendDialog(final List<PlayQueueItem> playlist) {
|
||||
PlaylistAppendDialog.fromPlayQueueItems(playlist)
|
||||
.show(getSupportFragmentManager(), getTag());
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////
|
||||
// Binding Service Listener
|
||||
////////////////////////////////////////////////////////////////////////////
|
||||
@@ -551,6 +518,7 @@ public abstract class ServicePlayerActivity extends AppCompatActivity
|
||||
onStateChanged(state);
|
||||
onPlayModeChanged(repeatMode, shuffled);
|
||||
onPlaybackParameterChanged(parameters);
|
||||
onMaybePlaybackAdapterChanged();
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -567,13 +535,30 @@ public abstract class ServicePlayerActivity extends AppCompatActivity
|
||||
progressSeekBar.setProgress(currentProgress);
|
||||
progressCurrentTime.setText(Localization.getDurationString(currentProgress / 1000));
|
||||
}
|
||||
|
||||
if (player != null) {
|
||||
progressLiveSync.setClickable(!player.isLiveEdge());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onMetadataUpdate(StreamInfo info) {
|
||||
if (info != null) {
|
||||
metadataTitle.setText(info.getName());
|
||||
metadataArtist.setText(info.uploader_name);
|
||||
metadataArtist.setText(info.getUploaderName());
|
||||
|
||||
progressEndTime.setVisibility(View.GONE);
|
||||
progressLiveSync.setVisibility(View.GONE);
|
||||
switch (info.getStreamType()) {
|
||||
case LIVE_STREAM:
|
||||
case AUDIO_LIVE_STREAM:
|
||||
progressLiveSync.setVisibility(View.VISIBLE);
|
||||
break;
|
||||
default:
|
||||
progressEndTime.setVisibility(View.VISIBLE);
|
||||
break;
|
||||
}
|
||||
|
||||
scrollToSelected();
|
||||
}
|
||||
}
|
||||
@@ -646,4 +631,12 @@ public abstract class ServicePlayerActivity extends AppCompatActivity
|
||||
playbackPitchButton.setText(formatPitch(parameters.pitch));
|
||||
}
|
||||
}
|
||||
|
||||
private void onMaybePlaybackAdapterChanged() {
|
||||
if (itemsList == null || player == null) return;
|
||||
final PlayQueueAdapter maybeNewAdapter = player.getPlayQueueAdapter();
|
||||
if (maybeNewAdapter != null && itemsList.getAdapter() != maybeNewAdapter) {
|
||||
itemsList.setAdapter(maybeNewAdapter);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,7 +32,6 @@ import android.graphics.PorterDuff;
|
||||
import android.net.Uri;
|
||||
import android.os.Build;
|
||||
import android.os.Handler;
|
||||
import android.preference.PreferenceManager;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.support.v4.content.ContextCompat;
|
||||
@@ -49,25 +48,27 @@ import android.widget.TextView;
|
||||
|
||||
import com.google.android.exoplayer2.C;
|
||||
import com.google.android.exoplayer2.Format;
|
||||
import com.google.android.exoplayer2.PlaybackParameters;
|
||||
import com.google.android.exoplayer2.Player;
|
||||
import com.google.android.exoplayer2.SimpleExoPlayer;
|
||||
import com.google.android.exoplayer2.source.MediaSource;
|
||||
import com.google.android.exoplayer2.source.MergingMediaSource;
|
||||
import com.google.android.exoplayer2.source.SingleSampleMediaSource;
|
||||
import com.google.android.exoplayer2.source.TrackGroup;
|
||||
import com.google.android.exoplayer2.source.TrackGroupArray;
|
||||
import com.google.android.exoplayer2.text.CaptionStyleCompat;
|
||||
import com.google.android.exoplayer2.trackselection.TrackSelectionArray;
|
||||
import com.google.android.exoplayer2.ui.AspectRatioFrameLayout;
|
||||
import com.google.android.exoplayer2.ui.SubtitleView;
|
||||
import com.google.android.exoplayer2.video.VideoListener;
|
||||
|
||||
import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.extractor.MediaFormat;
|
||||
import org.schabi.newpipe.extractor.Subtitles;
|
||||
import org.schabi.newpipe.extractor.stream.AudioStream;
|
||||
import org.schabi.newpipe.extractor.stream.StreamInfo;
|
||||
import org.schabi.newpipe.extractor.stream.StreamType;
|
||||
import org.schabi.newpipe.extractor.stream.VideoStream;
|
||||
import org.schabi.newpipe.player.helper.PlayerHelper;
|
||||
import org.schabi.newpipe.playlist.PlayQueueItem;
|
||||
import org.schabi.newpipe.player.playqueue.PlayQueueItem;
|
||||
import org.schabi.newpipe.util.AnimationUtils;
|
||||
import org.schabi.newpipe.util.ListHelper;
|
||||
|
||||
@@ -87,7 +88,7 @@ import static org.schabi.newpipe.util.AnimationUtils.animateView;
|
||||
*/
|
||||
@SuppressWarnings({"WeakerAccess", "unused"})
|
||||
public abstract class VideoPlayer extends BasePlayer
|
||||
implements SimpleExoPlayer.VideoListener,
|
||||
implements VideoListener,
|
||||
SeekBar.OnSeekBarChangeListener,
|
||||
View.OnClickListener,
|
||||
Player.EventListener,
|
||||
@@ -101,6 +102,7 @@ public abstract class VideoPlayer extends BasePlayer
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
protected static final int RENDERER_UNAVAILABLE = -1;
|
||||
public static final int DEFAULT_CONTROLS_DURATION = 300; // 300 millis
|
||||
public static final int DEFAULT_CONTROLS_HIDE_TIME = 2000; // 2 Seconds
|
||||
|
||||
private ArrayList<VideoStream> availableStreams;
|
||||
@@ -131,6 +133,7 @@ public abstract class VideoPlayer extends BasePlayer
|
||||
private SeekBar playbackSeekBar;
|
||||
private TextView playbackCurrentTime;
|
||||
private TextView playbackEndTime;
|
||||
private TextView playbackLiveSync;
|
||||
private TextView playbackSpeedTextView;
|
||||
|
||||
private View topControlsRoot;
|
||||
@@ -159,7 +162,6 @@ public abstract class VideoPlayer extends BasePlayer
|
||||
public VideoPlayer(String debugTag, Context context) {
|
||||
super(context);
|
||||
this.TAG = debugTag;
|
||||
this.context = context;
|
||||
}
|
||||
|
||||
public void setup(View rootView) {
|
||||
@@ -180,16 +182,17 @@ public abstract class VideoPlayer extends BasePlayer
|
||||
this.playbackSeekBar = rootView.findViewById(R.id.playbackSeekBar);
|
||||
this.playbackCurrentTime = rootView.findViewById(R.id.playbackCurrentTime);
|
||||
this.playbackEndTime = rootView.findViewById(R.id.playbackEndTime);
|
||||
this.playbackLiveSync = rootView.findViewById(R.id.playbackLiveSync);
|
||||
this.playbackSpeedTextView = rootView.findViewById(R.id.playbackSpeed);
|
||||
this.bottomControlsRoot = rootView.findViewById(R.id.bottomControls);
|
||||
this.topControlsRoot = rootView.findViewById(R.id.topControls);
|
||||
this.qualityTextView = rootView.findViewById(R.id.qualityTextView);
|
||||
|
||||
this.subtitleView = rootView.findViewById(R.id.subtitleView);
|
||||
final String captionSizeKey = PreferenceManager.getDefaultSharedPreferences(context)
|
||||
.getString(context.getString(R.string.caption_size_key),
|
||||
context.getString(R.string.caption_size_default));
|
||||
setupSubtitleView(subtitleView, captionSizeKey);
|
||||
|
||||
final float captionScale = PlayerHelper.getCaptionScale(context);
|
||||
final CaptionStyleCompat captionStyle = PlayerHelper.getCaptionStyle(context);
|
||||
setupSubtitleView(subtitleView, captionScale, captionStyle);
|
||||
|
||||
this.resizeView = rootView.findViewById(R.id.resizeTextView);
|
||||
resizeView.setText(PlayerHelper.resizeTypeOf(context, aspectRatioFrameLayout.getResizeMode()));
|
||||
@@ -211,7 +214,8 @@ public abstract class VideoPlayer extends BasePlayer
|
||||
}
|
||||
|
||||
protected abstract void setupSubtitleView(@NonNull SubtitleView view,
|
||||
@NonNull String captionSizeKey);
|
||||
final float captionScale,
|
||||
@NonNull final CaptionStyleCompat captionStyle);
|
||||
|
||||
@Override
|
||||
public void initListeners() {
|
||||
@@ -221,11 +225,12 @@ public abstract class VideoPlayer extends BasePlayer
|
||||
qualityTextView.setOnClickListener(this);
|
||||
captionTextView.setOnClickListener(this);
|
||||
resizeView.setOnClickListener(this);
|
||||
playbackLiveSync.setOnClickListener(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void initPlayer() {
|
||||
super.initPlayer();
|
||||
public void initPlayer(final boolean playOnReady) {
|
||||
super.initPlayer(playOnReady);
|
||||
|
||||
// Setup video view
|
||||
simpleExoPlayer.setVideoSurfaceView(surfaceView);
|
||||
@@ -261,7 +266,8 @@ public abstract class VideoPlayer extends BasePlayer
|
||||
qualityPopupMenu.getMenu().removeGroup(qualityPopupMenuGroupId);
|
||||
for (int i = 0; i < availableStreams.size(); i++) {
|
||||
VideoStream videoStream = availableStreams.get(i);
|
||||
qualityPopupMenu.getMenu().add(qualityPopupMenuGroupId, i, Menu.NONE, MediaFormat.getNameById(videoStream.format) + " " + videoStream.resolution);
|
||||
qualityPopupMenu.getMenu().add(qualityPopupMenuGroupId, i, Menu.NONE,
|
||||
MediaFormat.getNameById(videoStream.getFormatId()) + " " + videoStream.resolution);
|
||||
}
|
||||
if (getSelectedVideoStream() != null) {
|
||||
qualityTextView.setText(getSelectedVideoStream().resolution);
|
||||
@@ -305,8 +311,7 @@ public abstract class VideoPlayer extends BasePlayer
|
||||
captionItem.setOnMenuItemClickListener(menuItem -> {
|
||||
final int textRendererIndex = getRendererIndex(C.TRACK_TYPE_TEXT);
|
||||
if (trackSelector != null && textRendererIndex != RENDERER_UNAVAILABLE) {
|
||||
trackSelector.setParameters(trackSelector.getParameters()
|
||||
.withPreferredTextLanguage(captionLanguage));
|
||||
trackSelector.setPreferredTextLanguage(captionLanguage);
|
||||
trackSelector.setRendererDisabled(textRendererIndex, false);
|
||||
}
|
||||
return true;
|
||||
@@ -322,27 +327,53 @@ public abstract class VideoPlayer extends BasePlayer
|
||||
|
||||
protected abstract int getOverrideResolutionIndex(final List<VideoStream> sortedVideos, final String playbackQuality);
|
||||
|
||||
@Override
|
||||
public void sync(@NonNull final PlayQueueItem item, @Nullable final StreamInfo info) {
|
||||
super.sync(item, info);
|
||||
protected void onMetadataChanged(@NonNull final PlayQueueItem item,
|
||||
@Nullable final StreamInfo info,
|
||||
final int newPlayQueueIndex,
|
||||
final boolean hasPlayQueueItemChanged) {
|
||||
qualityTextView.setVisibility(View.GONE);
|
||||
playbackSpeedTextView.setVisibility(View.GONE);
|
||||
|
||||
if (info != null && info.video_streams.size() + info.video_only_streams.size() > 0) {
|
||||
final List<VideoStream> videos = ListHelper.getSortedStreamVideosList(context,
|
||||
info.video_streams, info.video_only_streams, false);
|
||||
availableStreams = new ArrayList<>(videos);
|
||||
if (playbackQuality == null) {
|
||||
selectedStreamIndex = getDefaultResolutionIndex(videos);
|
||||
} else {
|
||||
selectedStreamIndex = getOverrideResolutionIndex(videos, getPlaybackQuality());
|
||||
}
|
||||
playbackEndTime.setVisibility(View.GONE);
|
||||
playbackLiveSync.setVisibility(View.GONE);
|
||||
|
||||
buildQualityMenu();
|
||||
qualityTextView.setVisibility(View.VISIBLE);
|
||||
surfaceView.setVisibility(View.VISIBLE);
|
||||
} else {
|
||||
surfaceView.setVisibility(View.GONE);
|
||||
final StreamType streamType = info == null ? StreamType.NONE : info.getStreamType();
|
||||
|
||||
switch (streamType) {
|
||||
case AUDIO_STREAM:
|
||||
surfaceView.setVisibility(View.GONE);
|
||||
playbackEndTime.setVisibility(View.VISIBLE);
|
||||
break;
|
||||
|
||||
case AUDIO_LIVE_STREAM:
|
||||
surfaceView.setVisibility(View.GONE);
|
||||
playbackLiveSync.setVisibility(View.VISIBLE);
|
||||
break;
|
||||
|
||||
case LIVE_STREAM:
|
||||
surfaceView.setVisibility(View.VISIBLE);
|
||||
playbackLiveSync.setVisibility(View.VISIBLE);
|
||||
break;
|
||||
|
||||
case VIDEO_STREAM:
|
||||
if (info.getVideoStreams().size() + info.getVideoOnlyStreams().size() == 0) break;
|
||||
|
||||
final List<VideoStream> videos = ListHelper.getSortedStreamVideosList(context,
|
||||
info.getVideoStreams(), info.getVideoOnlyStreams(), false);
|
||||
availableStreams = new ArrayList<>(videos);
|
||||
if (playbackQuality == null) {
|
||||
selectedStreamIndex = getDefaultResolutionIndex(videos);
|
||||
} else {
|
||||
selectedStreamIndex = getOverrideResolutionIndex(videos, getPlaybackQuality());
|
||||
}
|
||||
|
||||
buildQualityMenu();
|
||||
qualityTextView.setVisibility(View.VISIBLE);
|
||||
|
||||
surfaceView.setVisibility(View.VISIBLE);
|
||||
default:
|
||||
playbackEndTime.setVisibility(View.VISIBLE);
|
||||
break;
|
||||
}
|
||||
|
||||
buildPlaybackSpeedMenu();
|
||||
@@ -352,11 +383,14 @@ public abstract class VideoPlayer extends BasePlayer
|
||||
@Override
|
||||
@Nullable
|
||||
public MediaSource sourceOf(final PlayQueueItem item, final StreamInfo info) {
|
||||
final MediaSource liveSource = super.sourceOf(item, info);
|
||||
if (liveSource != null) return liveSource;
|
||||
|
||||
List<MediaSource> mediaSources = new ArrayList<>();
|
||||
|
||||
// Create video stream source
|
||||
final List<VideoStream> videos = ListHelper.getSortedStreamVideosList(context,
|
||||
info.video_streams, info.video_only_streams, false);
|
||||
info.getVideoStreams(), info.getVideoOnlyStreams(), false);
|
||||
final int index;
|
||||
if (videos.isEmpty()) {
|
||||
index = -1;
|
||||
@@ -368,6 +402,7 @@ public abstract class VideoPlayer extends BasePlayer
|
||||
final VideoStream video = index >= 0 && index < videos.size() ? videos.get(index) : null;
|
||||
if (video != null) {
|
||||
final MediaSource streamSource = buildMediaSource(video.getUrl(),
|
||||
PlayerHelper.cacheKeyOf(info, video),
|
||||
MediaFormat.getSuffixById(video.getFormatId()));
|
||||
mediaSources.add(streamSource);
|
||||
}
|
||||
@@ -380,6 +415,7 @@ public abstract class VideoPlayer extends BasePlayer
|
||||
// Merge with audio stream in case if video does not contain audio
|
||||
if (audio != null && ((video != null && video.isVideoOnly) || video == null)) {
|
||||
final MediaSource audioSource = buildMediaSource(audio.getUrl(),
|
||||
PlayerHelper.cacheKeyOf(info, audio),
|
||||
MediaFormat.getSuffixById(audio.getFormatId()));
|
||||
mediaSources.add(audioSource);
|
||||
}
|
||||
@@ -391,12 +427,12 @@ public abstract class VideoPlayer extends BasePlayer
|
||||
// Create subtitle sources
|
||||
for (final Subtitles subtitle : info.getSubtitles()) {
|
||||
final String mimeType = PlayerHelper.mimeTypesOf(subtitle.getFileType());
|
||||
if (mimeType == null || context == null) continue;
|
||||
if (mimeType == null) continue;
|
||||
|
||||
final Format textFormat = Format.createTextSampleFormat(null, mimeType,
|
||||
SELECTION_FLAG_AUTOSELECT, PlayerHelper.captionLanguageOf(context, subtitle));
|
||||
final MediaSource textSource = new SingleSampleMediaSource(
|
||||
Uri.parse(subtitle.getURL()), cacheDataSourceFactory, textFormat, TIME_UNSET);
|
||||
final MediaSource textSource = dataSource.getSampleMediaSourceFactory()
|
||||
.createMediaSource(Uri.parse(subtitle.getURL()), textFormat, TIME_UNSET);
|
||||
mediaSources.add(textSource);
|
||||
}
|
||||
|
||||
@@ -417,7 +453,7 @@ public abstract class VideoPlayer extends BasePlayer
|
||||
super.onBlocked();
|
||||
|
||||
controlsVisibilityHandler.removeCallbacksAndMessages(null);
|
||||
animateView(controlsRoot, false, 300);
|
||||
animateView(controlsRoot, false, DEFAULT_CONTROLS_DURATION);
|
||||
|
||||
playbackSeekBar.setEnabled(false);
|
||||
// Bug on lower api, disabling and enabling the seekBar resets the thumb color -.-, so sets the color again
|
||||
@@ -442,7 +478,7 @@ public abstract class VideoPlayer extends BasePlayer
|
||||
playbackSeekBar.getThumb().setColorFilter(Color.RED, PorterDuff.Mode.SRC_IN);
|
||||
|
||||
loadingPanel.setVisibility(View.GONE);
|
||||
showControlsThenHide();
|
||||
|
||||
animateView(currentDisplaySeek, AnimationUtils.Type.SCALE_AND_ALPHA, false, 200);
|
||||
animateView(endScreen, false, 0);
|
||||
}
|
||||
@@ -489,6 +525,12 @@ public abstract class VideoPlayer extends BasePlayer
|
||||
onTextTrackUpdate();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPlaybackParametersChanged(PlaybackParameters playbackParameters) {
|
||||
super.onPlaybackParametersChanged(playbackParameters);
|
||||
playbackSpeedTextView.setText(formatSpeed(playbackParameters.speed));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onVideoSizeChanged(int width, int height, int unappliedRotationDegrees, float pixelWidthHeightRatio) {
|
||||
if (DEBUG) {
|
||||
@@ -529,26 +571,15 @@ public abstract class VideoPlayer extends BasePlayer
|
||||
}
|
||||
|
||||
// Normalize mismatching language strings
|
||||
final String preferredLanguage = trackSelector.getParameters().preferredTextLanguage;
|
||||
// Because ExoPlayer normalizes the preferred language string but not the text track
|
||||
// language strings, some preferred language string will have the language name in lowercase
|
||||
String formattedPreferredLanguage = null;
|
||||
if (preferredLanguage != null) {
|
||||
for (final String language : availableLanguages) {
|
||||
if (language.compareToIgnoreCase(preferredLanguage) == 0) {
|
||||
formattedPreferredLanguage = language;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
final String preferredLanguage = trackSelector.getPreferredTextLanguage();
|
||||
|
||||
// Build UI
|
||||
buildCaptionMenu(availableLanguages);
|
||||
if (trackSelector.getRendererDisabled(textRenderer) || formattedPreferredLanguage == null ||
|
||||
!availableLanguages.contains(formattedPreferredLanguage)) {
|
||||
if (trackSelector.getRendererDisabled(textRenderer) || preferredLanguage == null ||
|
||||
!availableLanguages.contains(preferredLanguage)) {
|
||||
captionTextView.setText(R.string.caption_none);
|
||||
} else {
|
||||
captionTextView.setText(formattedPreferredLanguage);
|
||||
captionTextView.setText(preferredLanguage);
|
||||
}
|
||||
captionTextView.setVisibility(availableLanguages.isEmpty() ? View.GONE : View.VISIBLE);
|
||||
}
|
||||
@@ -576,7 +607,7 @@ public abstract class VideoPlayer extends BasePlayer
|
||||
|
||||
@Override
|
||||
public void onUpdateProgress(int currentProgress, int duration, int bufferPercent) {
|
||||
if (!isPrepared) return;
|
||||
if (!isPrepared()) return;
|
||||
|
||||
if (duration != playbackSeekBar.getMax()) {
|
||||
playbackEndTime.setText(getTimeString(duration));
|
||||
@@ -592,17 +623,16 @@ public abstract class VideoPlayer extends BasePlayer
|
||||
if (DEBUG && bufferPercent % 20 == 0) { //Limit log
|
||||
Log.d(TAG, "updateProgress() called with: isVisible = " + isControlsVisible() + ", currentProgress = [" + currentProgress + "], duration = [" + duration + "], bufferPercent = [" + bufferPercent + "]");
|
||||
}
|
||||
playbackLiveSync.setClickable(!isLiveEdge());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onThumbnailReceived(Bitmap thumbnail) {
|
||||
super.onThumbnailReceived(thumbnail);
|
||||
if (thumbnail != null) endScreen.setImageBitmap(thumbnail);
|
||||
public void onLoadingComplete(String imageUri, View view, Bitmap loadedImage) {
|
||||
super.onLoadingComplete(imageUri, view, loadedImage);
|
||||
if (loadedImage != null) endScreen.setImageBitmap(loadedImage);
|
||||
}
|
||||
|
||||
protected void onFullScreenButtonClicked() {
|
||||
if (!isPlayerReady()) return;
|
||||
|
||||
changeState(STATE_BLOCKED);
|
||||
}
|
||||
|
||||
@@ -633,6 +663,8 @@ public abstract class VideoPlayer extends BasePlayer
|
||||
onResizeClicked();
|
||||
} else if (v.getId() == captionTextView.getId()) {
|
||||
onCaptionClicked();
|
||||
} else if (v.getId() == playbackLiveSync.getId()) {
|
||||
seekToDefault();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -683,7 +715,7 @@ public abstract class VideoPlayer extends BasePlayer
|
||||
if (DEBUG) Log.d(TAG, "onQualitySelectorClicked() called");
|
||||
qualityPopupMenu.show();
|
||||
isSomePopupMenuVisible = true;
|
||||
showControls(300);
|
||||
showControls(DEFAULT_CONTROLS_DURATION);
|
||||
|
||||
final VideoStream videoStream = getSelectedVideoStream();
|
||||
if (videoStream != null) {
|
||||
@@ -695,22 +727,22 @@ public abstract class VideoPlayer extends BasePlayer
|
||||
wasPlaying = simpleExoPlayer.getPlayWhenReady();
|
||||
}
|
||||
|
||||
private void onPlaybackSpeedClicked() {
|
||||
public void onPlaybackSpeedClicked() {
|
||||
if (DEBUG) Log.d(TAG, "onPlaybackSpeedClicked() called");
|
||||
playbackSpeedPopupMenu.show();
|
||||
isSomePopupMenuVisible = true;
|
||||
showControls(300);
|
||||
showControls(DEFAULT_CONTROLS_DURATION);
|
||||
}
|
||||
|
||||
private void onCaptionClicked() {
|
||||
if (DEBUG) Log.d(TAG, "onCaptionClicked() called");
|
||||
captionPopupMenu.show();
|
||||
isSomePopupMenuVisible = true;
|
||||
showControls(300);
|
||||
showControls(DEFAULT_CONTROLS_DURATION);
|
||||
}
|
||||
|
||||
private void onResizeClicked() {
|
||||
if (getAspectRatioFrameLayout() != null && context != null) {
|
||||
if (getAspectRatioFrameLayout() != null) {
|
||||
final int currentResizeMode = getAspectRatioFrameLayout().getResizeMode();
|
||||
final int newResizeMode = nextResizeMode(currentResizeMode);
|
||||
getAspectRatioFrameLayout().setResizeMode(newResizeMode);
|
||||
@@ -739,14 +771,15 @@ public abstract class VideoPlayer extends BasePlayer
|
||||
if (isPlaying()) simpleExoPlayer.setPlayWhenReady(false);
|
||||
|
||||
showControls(0);
|
||||
animateView(currentDisplaySeek, AnimationUtils.Type.SCALE_AND_ALPHA, true, 300);
|
||||
animateView(currentDisplaySeek, AnimationUtils.Type.SCALE_AND_ALPHA, true,
|
||||
DEFAULT_CONTROLS_DURATION);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStopTrackingTouch(SeekBar seekBar) {
|
||||
if (DEBUG) Log.d(TAG, "onStopTrackingTouch() called with: seekBar = [" + seekBar + "]");
|
||||
|
||||
simpleExoPlayer.seekTo(seekBar.getProgress());
|
||||
seekTo(seekBar.getProgress());
|
||||
if (wasPlaying || simpleExoPlayer.getDuration() == seekBar.getProgress()) simpleExoPlayer.setPlayWhenReady(true);
|
||||
|
||||
playbackCurrentTime.setText(getTimeString(seekBar.getProgress()));
|
||||
@@ -795,7 +828,7 @@ public abstract class VideoPlayer extends BasePlayer
|
||||
PropertyValuesHolder.ofFloat(View.ALPHA, 1f, 0f),
|
||||
PropertyValuesHolder.ofFloat(View.SCALE_X, 1.4f, 1f),
|
||||
PropertyValuesHolder.ofFloat(View.SCALE_Y, 1.4f, 1f)
|
||||
).setDuration(300);
|
||||
).setDuration(DEFAULT_CONTROLS_DURATION);
|
||||
controlViewAnimator.addListener(new AnimatorListenerAdapter() {
|
||||
@Override
|
||||
public void onAnimationEnd(Animator animation) {
|
||||
@@ -837,12 +870,8 @@ public abstract class VideoPlayer extends BasePlayer
|
||||
|
||||
public void showControlsThenHide() {
|
||||
if (DEBUG) Log.d(TAG, "showControlsThenHide() called");
|
||||
animateView(controlsRoot, true, 300, 0, new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
hideControls(300, DEFAULT_CONTROLS_HIDE_TIME);
|
||||
}
|
||||
});
|
||||
animateView(controlsRoot, true, DEFAULT_CONTROLS_DURATION, 0,
|
||||
() -> hideControls(DEFAULT_CONTROLS_DURATION, DEFAULT_CONTROLS_HIDE_TIME));
|
||||
}
|
||||
|
||||
public void showControls(long duration) {
|
||||
@@ -854,14 +883,23 @@ public abstract class VideoPlayer extends BasePlayer
|
||||
public void hideControls(final long duration, long delay) {
|
||||
if (DEBUG) Log.d(TAG, "hideControls() called with: delay = [" + delay + "]");
|
||||
controlsVisibilityHandler.removeCallbacksAndMessages(null);
|
||||
controlsVisibilityHandler.postDelayed(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
animateView(controlsRoot, false, duration);
|
||||
}
|
||||
}, delay);
|
||||
controlsVisibilityHandler.postDelayed(
|
||||
() -> animateView(controlsRoot, false, duration), delay);
|
||||
}
|
||||
|
||||
public void hideControlsAndButton(final long duration, long delay, View button) {
|
||||
if (DEBUG) Log.d(TAG, "hideControls() called with: delay = [" + delay + "]");
|
||||
controlsVisibilityHandler.removeCallbacksAndMessages(null);
|
||||
controlsVisibilityHandler.postDelayed(hideControlsAndButtonHandler(duration, button), delay);
|
||||
}
|
||||
|
||||
private Runnable hideControlsAndButtonHandler(long duration, View videoPlayPause)
|
||||
{
|
||||
return () -> {
|
||||
videoPlayPause.setVisibility(View.INVISIBLE);
|
||||
animateView(controlsRoot, false,duration);
|
||||
};
|
||||
}
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Getters and Setters
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
@@ -17,10 +17,14 @@ import com.google.android.exoplayer2.SimpleExoPlayer;
|
||||
import com.google.android.exoplayer2.audio.AudioRendererEventListener;
|
||||
import com.google.android.exoplayer2.decoder.DecoderCounters;
|
||||
|
||||
public class AudioReactor implements AudioManager.OnAudioFocusChangeListener, AudioRendererEventListener {
|
||||
public class AudioReactor implements AudioManager.OnAudioFocusChangeListener,
|
||||
AudioRendererEventListener {
|
||||
|
||||
private static final String TAG = "AudioFocusReactor";
|
||||
|
||||
private static final boolean SHOULD_BUILD_FOCUS_REQUEST =
|
||||
Build.VERSION.SDK_INT >= Build.VERSION_CODES.O;
|
||||
|
||||
private static final int DUCK_DURATION = 1500;
|
||||
private static final float DUCK_AUDIO_TO = .2f;
|
||||
|
||||
@@ -33,13 +37,14 @@ public class AudioReactor implements AudioManager.OnAudioFocusChangeListener, Au
|
||||
|
||||
private final AudioFocusRequest request;
|
||||
|
||||
public AudioReactor(@NonNull final Context context, @NonNull final SimpleExoPlayer player) {
|
||||
public AudioReactor(@NonNull final Context context,
|
||||
@NonNull final SimpleExoPlayer player) {
|
||||
this.player = player;
|
||||
this.context = context;
|
||||
this.audioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
|
||||
player.setAudioDebugListener(this);
|
||||
player.addAudioDebugListener(this);
|
||||
|
||||
if (shouldBuildFocusRequest()) {
|
||||
if (SHOULD_BUILD_FOCUS_REQUEST) {
|
||||
request = new AudioFocusRequest.Builder(FOCUS_GAIN_TYPE)
|
||||
.setAcceptsDelayedFocusGain(true)
|
||||
.setWillPauseWhenDucked(true)
|
||||
@@ -50,12 +55,17 @@ public class AudioReactor implements AudioManager.OnAudioFocusChangeListener, Au
|
||||
}
|
||||
}
|
||||
|
||||
public void dispose() {
|
||||
abandonAudioFocus();
|
||||
player.removeAudioDebugListener(this);
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Audio Manager
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
public void requestAudioFocus() {
|
||||
if (shouldBuildFocusRequest()) {
|
||||
if (SHOULD_BUILD_FOCUS_REQUEST) {
|
||||
audioManager.requestAudioFocus(request);
|
||||
} else {
|
||||
audioManager.requestAudioFocus(this, STREAM_TYPE, FOCUS_GAIN_TYPE);
|
||||
@@ -63,7 +73,7 @@ public class AudioReactor implements AudioManager.OnAudioFocusChangeListener, Au
|
||||
}
|
||||
|
||||
public void abandonAudioFocus() {
|
||||
if (shouldBuildFocusRequest()) {
|
||||
if (SHOULD_BUILD_FOCUS_REQUEST) {
|
||||
audioManager.abandonAudioFocusRequest(request);
|
||||
} else {
|
||||
audioManager.abandonAudioFocus(this);
|
||||
@@ -82,10 +92,6 @@ public class AudioReactor implements AudioManager.OnAudioFocusChangeListener, Au
|
||||
audioManager.setStreamVolume(STREAM_TYPE, volume, 0);
|
||||
}
|
||||
|
||||
private boolean shouldBuildFocusRequest() {
|
||||
return Build.VERSION.SDK_INT >= Build.VERSION_CODES.O;
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// AudioFocus
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
@@ -148,12 +154,8 @@ public class AudioReactor implements AudioManager.OnAudioFocusChangeListener, Au
|
||||
player.setVolume(to);
|
||||
}
|
||||
});
|
||||
valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
|
||||
@Override
|
||||
public void onAnimationUpdate(ValueAnimator animation) {
|
||||
player.setVolume(((float) animation.getAnimatedValue()));
|
||||
}
|
||||
});
|
||||
valueAnimator.addUpdateListener(animation ->
|
||||
player.setVolume(((float) animation.getAnimatedValue())));
|
||||
valueAnimator.start();
|
||||
}
|
||||
|
||||
|
||||
@@ -4,11 +4,14 @@ import android.content.Context;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.util.Log;
|
||||
|
||||
import com.google.android.exoplayer2.upstream.BandwidthMeter;
|
||||
import com.google.android.exoplayer2.upstream.DataSource;
|
||||
import com.google.android.exoplayer2.upstream.DefaultBandwidthMeter;
|
||||
import com.google.android.exoplayer2.upstream.DefaultDataSource;
|
||||
import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory;
|
||||
import com.google.android.exoplayer2.upstream.DefaultHttpDataSource;
|
||||
import com.google.android.exoplayer2.upstream.FileDataSource;
|
||||
import com.google.android.exoplayer2.upstream.TransferListener;
|
||||
import com.google.android.exoplayer2.upstream.cache.CacheDataSink;
|
||||
import com.google.android.exoplayer2.upstream.cache.CacheDataSource;
|
||||
import com.google.android.exoplayer2.upstream.cache.LeastRecentlyUsedCacheEvictor;
|
||||
@@ -18,7 +21,7 @@ import org.schabi.newpipe.Downloader;
|
||||
|
||||
import java.io.File;
|
||||
|
||||
public class CacheFactory implements DataSource.Factory {
|
||||
/* package-private */ class CacheFactory implements DataSource.Factory {
|
||||
private static final String TAG = "CacheFactory";
|
||||
private static final String CACHE_FOLDER_NAME = "exoplayer";
|
||||
private static final int CACHE_FLAGS = CacheDataSource.FLAG_BLOCK_ON_CACHE | CacheDataSource.FLAG_IGNORE_CACHE_ON_ERROR;
|
||||
@@ -33,18 +36,21 @@ public class CacheFactory implements DataSource.Factory {
|
||||
// todo: make this a singleton?
|
||||
private static SimpleCache cache;
|
||||
|
||||
public CacheFactory(@NonNull final Context context) {
|
||||
this(context, PlayerHelper.getPreferredCacheSize(context), PlayerHelper.getPreferredFileSize(context));
|
||||
public CacheFactory(@NonNull final Context context,
|
||||
@NonNull final String userAgent,
|
||||
@NonNull final TransferListener<? super DataSource> transferListener) {
|
||||
this(context, userAgent, transferListener, PlayerHelper.getPreferredCacheSize(context),
|
||||
PlayerHelper.getPreferredFileSize(context));
|
||||
}
|
||||
|
||||
CacheFactory(@NonNull final Context context, final long maxCacheSize, final long maxFileSize) {
|
||||
super();
|
||||
private CacheFactory(@NonNull final Context context,
|
||||
@NonNull final String userAgent,
|
||||
@NonNull final TransferListener<? super DataSource> transferListener,
|
||||
final long maxCacheSize,
|
||||
final long maxFileSize) {
|
||||
this.maxFileSize = maxFileSize;
|
||||
|
||||
final String userAgent = Downloader.USER_AGENT;
|
||||
final DefaultBandwidthMeter bandwidthMeter = new DefaultBandwidthMeter();
|
||||
dataSourceFactory = new DefaultDataSourceFactory(context, userAgent, bandwidthMeter);
|
||||
|
||||
dataSourceFactory = new DefaultDataSourceFactory(context, userAgent, transferListener);
|
||||
cacheDir = new File(context.getExternalCacheDir(), CACHE_FOLDER_NAME);
|
||||
if (!cacheDir.exists()) {
|
||||
//noinspection ResultOfMethodCallIgnored
|
||||
|
||||
@@ -11,12 +11,14 @@ import com.google.android.exoplayer2.trackselection.TrackSelectionArray;
|
||||
import com.google.android.exoplayer2.upstream.Allocator;
|
||||
import com.google.android.exoplayer2.upstream.DefaultAllocator;
|
||||
|
||||
import static com.google.android.exoplayer2.DefaultLoadControl.DEFAULT_BUFFER_FOR_PLAYBACK_AFTER_REBUFFER_MS;
|
||||
import static com.google.android.exoplayer2.DefaultLoadControl.DEFAULT_PRIORITIZE_TIME_OVER_SIZE_THRESHOLDS;
|
||||
import static com.google.android.exoplayer2.DefaultLoadControl.DEFAULT_TARGET_BUFFER_BYTES;
|
||||
|
||||
public class LoadController implements LoadControl {
|
||||
|
||||
public static final String TAG = "LoadController";
|
||||
|
||||
private final long initialPlaybackBufferUs;
|
||||
private final LoadControl internalLoadControl;
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
@@ -24,19 +26,25 @@ public class LoadController implements LoadControl {
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
public LoadController(final Context context) {
|
||||
this(PlayerHelper.getMinBufferMs(context),
|
||||
PlayerHelper.getMaxBufferMs(context),
|
||||
PlayerHelper.getBufferForPlaybackMs(context));
|
||||
this(PlayerHelper.getPlaybackStartBufferMs(context),
|
||||
PlayerHelper.getPlaybackMinimumBufferMs(context),
|
||||
PlayerHelper.getPlaybackOptimalBufferMs(context));
|
||||
}
|
||||
|
||||
public LoadController(final int minBufferMs,
|
||||
final int maxBufferMs,
|
||||
final int bufferForPlaybackMs) {
|
||||
private LoadController(final int initialPlaybackBufferMs,
|
||||
final int minimumPlaybackbufferMs,
|
||||
final int optimalPlaybackBufferMs) {
|
||||
this.initialPlaybackBufferUs = initialPlaybackBufferMs * 1000;
|
||||
|
||||
final DefaultAllocator allocator = new DefaultAllocator(true,
|
||||
C.DEFAULT_BUFFER_SEGMENT_SIZE);
|
||||
|
||||
internalLoadControl = new DefaultLoadControl(allocator, minBufferMs, maxBufferMs,
|
||||
bufferForPlaybackMs, DEFAULT_BUFFER_FOR_PLAYBACK_AFTER_REBUFFER_MS);
|
||||
internalLoadControl = new DefaultLoadControl(allocator,
|
||||
/*minBufferMs=*/minimumPlaybackbufferMs,
|
||||
/*maxBufferMs=*/optimalPlaybackBufferMs,
|
||||
/*bufferForPlaybackMs=*/initialPlaybackBufferMs,
|
||||
/*bufferForPlaybackAfterRebufferMs=*/initialPlaybackBufferMs,
|
||||
DEFAULT_TARGET_BUFFER_BYTES, DEFAULT_PRIORITIZE_TIME_OVER_SIZE_THRESHOLDS);
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
@@ -49,7 +57,8 @@ public class LoadController implements LoadControl {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTracksSelected(Renderer[] renderers, TrackGroupArray trackGroupArray, TrackSelectionArray trackSelectionArray) {
|
||||
public void onTracksSelected(Renderer[] renderers, TrackGroupArray trackGroupArray,
|
||||
TrackSelectionArray trackSelectionArray) {
|
||||
internalLoadControl.onTracksSelected(renderers, trackGroupArray, trackSelectionArray);
|
||||
}
|
||||
|
||||
@@ -69,12 +78,27 @@ public class LoadController implements LoadControl {
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean shouldStartPlayback(long l, boolean b) {
|
||||
return internalLoadControl.shouldStartPlayback(l, b);
|
||||
public long getBackBufferDurationUs() {
|
||||
return internalLoadControl.getBackBufferDurationUs();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean shouldContinueLoading(long l) {
|
||||
return internalLoadControl.shouldContinueLoading(l);
|
||||
public boolean retainBackBufferFromKeyframe() {
|
||||
return internalLoadControl.retainBackBufferFromKeyframe();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean shouldContinueLoading(long bufferedDurationUs, float playbackSpeed) {
|
||||
return internalLoadControl.shouldContinueLoading(bufferedDurationUs, playbackSpeed);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean shouldStartPlayback(long bufferedDurationUs, float playbackSpeed,
|
||||
boolean rebuffering) {
|
||||
final boolean isInitialPlaybackBufferFilled = bufferedDurationUs >=
|
||||
this.initialPlaybackBufferUs * playbackSpeed;
|
||||
final boolean isInternalStartingPlayback = internalLoadControl.shouldStartPlayback(
|
||||
bufferedDurationUs, playbackSpeed, rebuffering);
|
||||
return isInitialPlaybackBufferFilled || isInternalStartingPlayback;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
package org.schabi.newpipe.player.helper;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.support.v4.media.session.MediaButtonReceiver;
|
||||
import android.support.v4.media.session.MediaSessionCompat;
|
||||
import android.view.KeyEvent;
|
||||
|
||||
import com.google.android.exoplayer2.Player;
|
||||
import com.google.android.exoplayer2.ext.mediasession.MediaSessionConnector;
|
||||
|
||||
import org.schabi.newpipe.player.mediasession.MediaSessionCallback;
|
||||
import org.schabi.newpipe.player.mediasession.PlayQueueNavigator;
|
||||
import org.schabi.newpipe.player.mediasession.PlayQueuePlaybackController;
|
||||
|
||||
public class MediaSessionManager {
|
||||
private static final String TAG = "MediaSessionManager";
|
||||
|
||||
@NonNull private final MediaSessionCompat mediaSession;
|
||||
@NonNull private final MediaSessionConnector sessionConnector;
|
||||
|
||||
public MediaSessionManager(@NonNull final Context context,
|
||||
@NonNull final Player player,
|
||||
@NonNull final MediaSessionCallback callback) {
|
||||
this.mediaSession = new MediaSessionCompat(context, TAG);
|
||||
this.mediaSession.setActive(true);
|
||||
|
||||
this.sessionConnector = new MediaSessionConnector(mediaSession,
|
||||
new PlayQueuePlaybackController(callback));
|
||||
this.sessionConnector.setQueueNavigator(new PlayQueueNavigator(mediaSession, callback));
|
||||
this.sessionConnector.setPlayer(player, null);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@SuppressWarnings("UnusedReturnValue")
|
||||
public KeyEvent handleMediaButtonIntent(final Intent intent) {
|
||||
return MediaButtonReceiver.handleIntent(mediaSession, intent);
|
||||
}
|
||||
|
||||
public void dispose() {
|
||||
this.sessionConnector.setPlayer(null, null);
|
||||
this.sessionConnector.setQueueNavigator(null);
|
||||
this.mediaSession.setActive(false);
|
||||
this.mediaSession.release();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,379 @@
|
||||
package org.schabi.newpipe.player.helper;
|
||||
|
||||
import android.app.Dialog;
|
||||
import android.content.Context;
|
||||
import android.os.Bundle;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.support.v4.app.DialogFragment;
|
||||
import android.support.v7.app.AlertDialog;
|
||||
import android.util.Log;
|
||||
import android.view.View;
|
||||
import android.widget.CheckBox;
|
||||
import android.widget.SeekBar;
|
||||
import android.widget.TextView;
|
||||
|
||||
import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.util.SliderStrategy;
|
||||
|
||||
import static org.schabi.newpipe.player.BasePlayer.DEBUG;
|
||||
|
||||
public class PlaybackParameterDialog extends DialogFragment {
|
||||
@NonNull private static final String TAG = "PlaybackParameterDialog";
|
||||
|
||||
public static final double MINIMUM_PLAYBACK_VALUE = 0.25f;
|
||||
public static final double MAXIMUM_PLAYBACK_VALUE = 3.00f;
|
||||
|
||||
public static final char STEP_UP_SIGN = '+';
|
||||
public static final char STEP_DOWN_SIGN = '-';
|
||||
public static final double PLAYBACK_STEP_VALUE = 0.05f;
|
||||
|
||||
public static final double NIGHTCORE_TEMPO = 1.20f;
|
||||
public static final double NIGHTCORE_PITCH_LOWER = 1.15f;
|
||||
public static final double NIGHTCORE_PITCH_UPPER = 1.25f;
|
||||
|
||||
public static final double DEFAULT_TEMPO = 1.00f;
|
||||
public static final double DEFAULT_PITCH = 1.00f;
|
||||
|
||||
@NonNull private static final String INITIAL_TEMPO_KEY = "initial_tempo_key";
|
||||
@NonNull private static final String INITIAL_PITCH_KEY = "initial_pitch_key";
|
||||
|
||||
public interface Callback {
|
||||
void onPlaybackParameterChanged(final float playbackTempo, final float playbackPitch);
|
||||
}
|
||||
|
||||
@Nullable private Callback callback;
|
||||
|
||||
@NonNull private final SliderStrategy strategy = new SliderStrategy.Quadratic(
|
||||
MINIMUM_PLAYBACK_VALUE, MAXIMUM_PLAYBACK_VALUE,
|
||||
/*centerAt=*/1.00f, /*sliderGranularity=*/10000);
|
||||
|
||||
private double initialTempo = DEFAULT_TEMPO;
|
||||
private double initialPitch = DEFAULT_PITCH;
|
||||
|
||||
@Nullable private SeekBar tempoSlider;
|
||||
@Nullable private TextView tempoMinimumText;
|
||||
@Nullable private TextView tempoMaximumText;
|
||||
@Nullable private TextView tempoCurrentText;
|
||||
@Nullable private TextView tempoStepDownText;
|
||||
@Nullable private TextView tempoStepUpText;
|
||||
|
||||
@Nullable private SeekBar pitchSlider;
|
||||
@Nullable private TextView pitchMinimumText;
|
||||
@Nullable private TextView pitchMaximumText;
|
||||
@Nullable private TextView pitchCurrentText;
|
||||
@Nullable private TextView pitchStepDownText;
|
||||
@Nullable private TextView pitchStepUpText;
|
||||
|
||||
@Nullable private CheckBox unhookingCheckbox;
|
||||
|
||||
@Nullable private TextView nightCorePresetText;
|
||||
@Nullable private TextView resetPresetText;
|
||||
|
||||
public static PlaybackParameterDialog newInstance(final double playbackTempo,
|
||||
final double playbackPitch) {
|
||||
PlaybackParameterDialog dialog = new PlaybackParameterDialog();
|
||||
dialog.initialTempo = playbackTempo;
|
||||
dialog.initialPitch = playbackPitch;
|
||||
return dialog;
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Lifecycle
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
@Override
|
||||
public void onAttach(Context context) {
|
||||
super.onAttach(context);
|
||||
if (context != null && context instanceof Callback) {
|
||||
callback = (Callback) context;
|
||||
} else {
|
||||
dismiss();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate(@Nullable Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
if (savedInstanceState != null) {
|
||||
initialTempo = savedInstanceState.getDouble(INITIAL_TEMPO_KEY, DEFAULT_TEMPO);
|
||||
initialPitch = savedInstanceState.getDouble(INITIAL_PITCH_KEY, DEFAULT_PITCH);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSaveInstanceState(Bundle outState) {
|
||||
super.onSaveInstanceState(outState);
|
||||
outState.putDouble(INITIAL_TEMPO_KEY, initialTempo);
|
||||
outState.putDouble(INITIAL_PITCH_KEY, initialPitch);
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Dialog
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public Dialog onCreateDialog(@Nullable Bundle savedInstanceState) {
|
||||
final View view = View.inflate(getContext(), R.layout.dialog_playback_parameter, null);
|
||||
setupControlViews(view);
|
||||
|
||||
final AlertDialog.Builder dialogBuilder = new AlertDialog.Builder(requireActivity())
|
||||
.setTitle(R.string.playback_speed_control)
|
||||
.setView(view)
|
||||
.setCancelable(true)
|
||||
.setNegativeButton(R.string.cancel, (dialogInterface, i) ->
|
||||
setPlaybackParameters(initialTempo, initialPitch))
|
||||
.setPositiveButton(R.string.finish, (dialogInterface, i) ->
|
||||
setCurrentPlaybackParameters());
|
||||
|
||||
return dialogBuilder.create();
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Control Views
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
private void setupControlViews(@NonNull View rootView) {
|
||||
setupHookingControl(rootView);
|
||||
setupTempoControl(rootView);
|
||||
setupPitchControl(rootView);
|
||||
setupPresetControl(rootView);
|
||||
}
|
||||
|
||||
private void setupTempoControl(@NonNull View rootView) {
|
||||
tempoSlider = rootView.findViewById(R.id.tempoSeekbar);
|
||||
tempoMinimumText = rootView.findViewById(R.id.tempoMinimumText);
|
||||
tempoMaximumText = rootView.findViewById(R.id.tempoMaximumText);
|
||||
tempoCurrentText = rootView.findViewById(R.id.tempoCurrentText);
|
||||
tempoStepUpText = rootView.findViewById(R.id.tempoStepUp);
|
||||
tempoStepDownText = rootView.findViewById(R.id.tempoStepDown);
|
||||
|
||||
if (tempoCurrentText != null)
|
||||
tempoCurrentText.setText(PlayerHelper.formatSpeed(initialTempo));
|
||||
if (tempoMaximumText != null)
|
||||
tempoMaximumText.setText(PlayerHelper.formatSpeed(MAXIMUM_PLAYBACK_VALUE));
|
||||
if (tempoMinimumText != null)
|
||||
tempoMinimumText.setText(PlayerHelper.formatSpeed(MINIMUM_PLAYBACK_VALUE));
|
||||
|
||||
if (tempoStepUpText != null) {
|
||||
tempoStepUpText.setText(getStepUpPercentString(PLAYBACK_STEP_VALUE));
|
||||
tempoStepUpText.setOnClickListener(view -> {
|
||||
onTempoSliderUpdated(getCurrentTempo() + PLAYBACK_STEP_VALUE);
|
||||
setCurrentPlaybackParameters();
|
||||
});
|
||||
}
|
||||
|
||||
if (tempoStepDownText != null) {
|
||||
tempoStepDownText.setText(getStepDownPercentString(PLAYBACK_STEP_VALUE));
|
||||
tempoStepDownText.setOnClickListener(view -> {
|
||||
onTempoSliderUpdated(getCurrentTempo() - PLAYBACK_STEP_VALUE);
|
||||
setCurrentPlaybackParameters();
|
||||
});
|
||||
}
|
||||
|
||||
if (tempoSlider != null) {
|
||||
tempoSlider.setMax(strategy.progressOf(MAXIMUM_PLAYBACK_VALUE));
|
||||
tempoSlider.setProgress(strategy.progressOf(initialTempo));
|
||||
tempoSlider.setOnSeekBarChangeListener(getOnTempoChangedListener());
|
||||
}
|
||||
}
|
||||
|
||||
private void setupPitchControl(@NonNull View rootView) {
|
||||
pitchSlider = rootView.findViewById(R.id.pitchSeekbar);
|
||||
pitchMinimumText = rootView.findViewById(R.id.pitchMinimumText);
|
||||
pitchMaximumText = rootView.findViewById(R.id.pitchMaximumText);
|
||||
pitchCurrentText = rootView.findViewById(R.id.pitchCurrentText);
|
||||
pitchStepDownText = rootView.findViewById(R.id.pitchStepDown);
|
||||
pitchStepUpText = rootView.findViewById(R.id.pitchStepUp);
|
||||
|
||||
if (pitchCurrentText != null)
|
||||
pitchCurrentText.setText(PlayerHelper.formatPitch(initialPitch));
|
||||
if (pitchMaximumText != null)
|
||||
pitchMaximumText.setText(PlayerHelper.formatPitch(MAXIMUM_PLAYBACK_VALUE));
|
||||
if (pitchMinimumText != null)
|
||||
pitchMinimumText.setText(PlayerHelper.formatPitch(MINIMUM_PLAYBACK_VALUE));
|
||||
|
||||
if (pitchStepUpText != null) {
|
||||
pitchStepUpText.setText(getStepUpPercentString(PLAYBACK_STEP_VALUE));
|
||||
pitchStepUpText.setOnClickListener(view -> {
|
||||
onPitchSliderUpdated(getCurrentPitch() + PLAYBACK_STEP_VALUE);
|
||||
setCurrentPlaybackParameters();
|
||||
});
|
||||
}
|
||||
|
||||
if (pitchStepDownText != null) {
|
||||
pitchStepDownText.setText(getStepDownPercentString(PLAYBACK_STEP_VALUE));
|
||||
pitchStepDownText.setOnClickListener(view -> {
|
||||
onPitchSliderUpdated(getCurrentPitch() - PLAYBACK_STEP_VALUE);
|
||||
setCurrentPlaybackParameters();
|
||||
});
|
||||
}
|
||||
|
||||
if (pitchSlider != null) {
|
||||
pitchSlider.setMax(strategy.progressOf(MAXIMUM_PLAYBACK_VALUE));
|
||||
pitchSlider.setProgress(strategy.progressOf(initialPitch));
|
||||
pitchSlider.setOnSeekBarChangeListener(getOnPitchChangedListener());
|
||||
}
|
||||
}
|
||||
|
||||
private void setupHookingControl(@NonNull View rootView) {
|
||||
unhookingCheckbox = rootView.findViewById(R.id.unhookCheckbox);
|
||||
if (unhookingCheckbox != null) {
|
||||
unhookingCheckbox.setChecked(initialPitch != initialTempo);
|
||||
unhookingCheckbox.setOnCheckedChangeListener((compoundButton, isChecked) -> {
|
||||
if (isChecked) return;
|
||||
// When unchecked, slide back to the minimum of current tempo or pitch
|
||||
final double minimum = Math.min(getCurrentPitch(), getCurrentTempo());
|
||||
setSliders(minimum);
|
||||
setCurrentPlaybackParameters();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private void setupPresetControl(@NonNull View rootView) {
|
||||
nightCorePresetText = rootView.findViewById(R.id.presetNightcore);
|
||||
if (nightCorePresetText != null) {
|
||||
nightCorePresetText.setOnClickListener(view -> {
|
||||
final double randomPitch = NIGHTCORE_PITCH_LOWER +
|
||||
Math.random() * (NIGHTCORE_PITCH_UPPER - NIGHTCORE_PITCH_LOWER);
|
||||
|
||||
setTempoSlider(NIGHTCORE_TEMPO);
|
||||
setPitchSlider(randomPitch);
|
||||
setCurrentPlaybackParameters();
|
||||
});
|
||||
}
|
||||
|
||||
resetPresetText = rootView.findViewById(R.id.presetReset);
|
||||
if (resetPresetText != null) {
|
||||
resetPresetText.setOnClickListener(view -> {
|
||||
setTempoSlider(DEFAULT_TEMPO);
|
||||
setPitchSlider(DEFAULT_PITCH);
|
||||
setCurrentPlaybackParameters();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Sliders
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
private SeekBar.OnSeekBarChangeListener getOnTempoChangedListener() {
|
||||
return new SeekBar.OnSeekBarChangeListener() {
|
||||
@Override
|
||||
public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
|
||||
final double currentTempo = strategy.valueOf(progress);
|
||||
if (fromUser) {
|
||||
onTempoSliderUpdated(currentTempo);
|
||||
setCurrentPlaybackParameters();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStartTrackingTouch(SeekBar seekBar) {
|
||||
// Do Nothing.
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStopTrackingTouch(SeekBar seekBar) {
|
||||
// Do Nothing.
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private SeekBar.OnSeekBarChangeListener getOnPitchChangedListener() {
|
||||
return new SeekBar.OnSeekBarChangeListener() {
|
||||
@Override
|
||||
public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
|
||||
final double currentPitch = strategy.valueOf(progress);
|
||||
if (fromUser) { // this change is first in chain
|
||||
onPitchSliderUpdated(currentPitch);
|
||||
setCurrentPlaybackParameters();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStartTrackingTouch(SeekBar seekBar) {
|
||||
// Do Nothing.
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStopTrackingTouch(SeekBar seekBar) {
|
||||
// Do Nothing.
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private void onTempoSliderUpdated(final double newTempo) {
|
||||
if (unhookingCheckbox == null) return;
|
||||
if (!unhookingCheckbox.isChecked()) {
|
||||
setSliders(newTempo);
|
||||
} else {
|
||||
setTempoSlider(newTempo);
|
||||
}
|
||||
}
|
||||
|
||||
private void onPitchSliderUpdated(final double newPitch) {
|
||||
if (unhookingCheckbox == null) return;
|
||||
if (!unhookingCheckbox.isChecked()) {
|
||||
setSliders(newPitch);
|
||||
} else {
|
||||
setPitchSlider(newPitch);
|
||||
}
|
||||
}
|
||||
|
||||
private void setSliders(final double newValue) {
|
||||
setTempoSlider(newValue);
|
||||
setPitchSlider(newValue);
|
||||
}
|
||||
|
||||
private void setTempoSlider(final double newTempo) {
|
||||
if (tempoSlider == null) return;
|
||||
tempoSlider.setProgress(strategy.progressOf(newTempo));
|
||||
}
|
||||
|
||||
private void setPitchSlider(final double newPitch) {
|
||||
if (pitchSlider == null) return;
|
||||
pitchSlider.setProgress(strategy.progressOf(newPitch));
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Helper
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
private void setCurrentPlaybackParameters() {
|
||||
setPlaybackParameters(getCurrentTempo(), getCurrentPitch());
|
||||
}
|
||||
|
||||
private void setPlaybackParameters(final double tempo, final double pitch) {
|
||||
if (callback != null && tempoCurrentText != null && pitchCurrentText != null) {
|
||||
if (DEBUG) Log.d(TAG, "Setting playback parameters to " +
|
||||
"tempo=[" + tempo + "], " +
|
||||
"pitch=[" + pitch + "]");
|
||||
|
||||
tempoCurrentText.setText(PlayerHelper.formatSpeed(tempo));
|
||||
pitchCurrentText.setText(PlayerHelper.formatPitch(pitch));
|
||||
callback.onPlaybackParameterChanged((float) tempo, (float) pitch);
|
||||
}
|
||||
}
|
||||
|
||||
private double getCurrentTempo() {
|
||||
return tempoSlider == null ? initialTempo : strategy.valueOf(
|
||||
tempoSlider.getProgress());
|
||||
}
|
||||
|
||||
private double getCurrentPitch() {
|
||||
return pitchSlider == null ? initialPitch : strategy.valueOf(
|
||||
pitchSlider.getProgress());
|
||||
}
|
||||
|
||||
@NonNull
|
||||
private static String getStepUpPercentString(final double percent) {
|
||||
return STEP_UP_SIGN + PlayerHelper.formatPitch(percent);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
private static String getStepDownPercentString(final double percent) {
|
||||
return STEP_DOWN_SIGN + PlayerHelper.formatPitch(percent);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
package org.schabi.newpipe.player.helper;
|
||||
|
||||
import android.content.Context;
|
||||
import android.support.annotation.NonNull;
|
||||
|
||||
import com.google.android.exoplayer2.source.ExtractorMediaSource;
|
||||
import com.google.android.exoplayer2.source.SingleSampleMediaSource;
|
||||
import com.google.android.exoplayer2.source.dash.DashMediaSource;
|
||||
import com.google.android.exoplayer2.source.dash.DefaultDashChunkSource;
|
||||
import com.google.android.exoplayer2.source.hls.HlsMediaSource;
|
||||
import com.google.android.exoplayer2.source.smoothstreaming.DefaultSsChunkSource;
|
||||
import com.google.android.exoplayer2.source.smoothstreaming.SsMediaSource;
|
||||
import com.google.android.exoplayer2.upstream.DataSource;
|
||||
import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory;
|
||||
import com.google.android.exoplayer2.upstream.TransferListener;
|
||||
|
||||
public class PlayerDataSource {
|
||||
private static final int MANIFEST_MINIMUM_RETRY = 5;
|
||||
private static final int EXTRACTOR_MINIMUM_RETRY = Integer.MAX_VALUE;
|
||||
private static final int LIVE_STREAM_EDGE_GAP_MILLIS = 10000;
|
||||
|
||||
private final DataSource.Factory cacheDataSourceFactory;
|
||||
private final DataSource.Factory cachelessDataSourceFactory;
|
||||
|
||||
public PlayerDataSource(@NonNull final Context context,
|
||||
@NonNull final String userAgent,
|
||||
@NonNull final TransferListener<? super DataSource> transferListener) {
|
||||
cacheDataSourceFactory = new CacheFactory(context, userAgent, transferListener);
|
||||
cachelessDataSourceFactory = new DefaultDataSourceFactory(context, userAgent, transferListener);
|
||||
}
|
||||
|
||||
public SsMediaSource.Factory getLiveSsMediaSourceFactory() {
|
||||
return new SsMediaSource.Factory(new DefaultSsChunkSource.Factory(
|
||||
cachelessDataSourceFactory), cachelessDataSourceFactory)
|
||||
.setMinLoadableRetryCount(MANIFEST_MINIMUM_RETRY)
|
||||
.setLivePresentationDelayMs(LIVE_STREAM_EDGE_GAP_MILLIS);
|
||||
}
|
||||
|
||||
public HlsMediaSource.Factory getLiveHlsMediaSourceFactory() {
|
||||
return new HlsMediaSource.Factory(cachelessDataSourceFactory)
|
||||
.setAllowChunklessPreparation(true)
|
||||
.setMinLoadableRetryCount(MANIFEST_MINIMUM_RETRY);
|
||||
}
|
||||
|
||||
public DashMediaSource.Factory getLiveDashMediaSourceFactory() {
|
||||
return new DashMediaSource.Factory(new DefaultDashChunkSource.Factory(
|
||||
cachelessDataSourceFactory), cachelessDataSourceFactory)
|
||||
.setMinLoadableRetryCount(MANIFEST_MINIMUM_RETRY)
|
||||
.setLivePresentationDelayMs(LIVE_STREAM_EDGE_GAP_MILLIS);
|
||||
}
|
||||
|
||||
public SsMediaSource.Factory getSsMediaSourceFactory() {
|
||||
return new SsMediaSource.Factory(new DefaultSsChunkSource.Factory(
|
||||
cacheDataSourceFactory), cacheDataSourceFactory);
|
||||
}
|
||||
|
||||
public HlsMediaSource.Factory getHlsMediaSourceFactory() {
|
||||
return new HlsMediaSource.Factory(cacheDataSourceFactory);
|
||||
}
|
||||
|
||||
public DashMediaSource.Factory getDashMediaSourceFactory() {
|
||||
return new DashMediaSource.Factory(new DefaultDashChunkSource.Factory(
|
||||
cacheDataSourceFactory), cacheDataSourceFactory);
|
||||
}
|
||||
|
||||
public ExtractorMediaSource.Factory getExtractorMediaSourceFactory() {
|
||||
return new ExtractorMediaSource.Factory(cacheDataSourceFactory)
|
||||
.setMinLoadableRetryCount(EXTRACTOR_MINIMUM_RETRY);
|
||||
}
|
||||
|
||||
public ExtractorMediaSource.Factory getExtractorMediaSourceFactory(@NonNull final String key) {
|
||||
return getExtractorMediaSourceFactory().setCustomCacheKey(key);
|
||||
}
|
||||
|
||||
public SingleSampleMediaSource.Factory getSampleMediaSourceFactory() {
|
||||
return new SingleSampleMediaSource.Factory(cacheDataSourceFactory);
|
||||
}
|
||||
}
|
||||
@@ -2,27 +2,45 @@ package org.schabi.newpipe.player.helper;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.SharedPreferences;
|
||||
import android.os.Build;
|
||||
import android.preference.PreferenceManager;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.view.accessibility.CaptioningManager;
|
||||
|
||||
import com.google.android.exoplayer2.SeekParameters;
|
||||
import com.google.android.exoplayer2.text.CaptionStyleCompat;
|
||||
import com.google.android.exoplayer2.trackselection.AdaptiveTrackSelection;
|
||||
import com.google.android.exoplayer2.trackselection.TrackSelection;
|
||||
import com.google.android.exoplayer2.ui.AspectRatioFrameLayout;
|
||||
import com.google.android.exoplayer2.upstream.BandwidthMeter;
|
||||
import com.google.android.exoplayer2.util.MimeTypes;
|
||||
|
||||
import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.extractor.InfoItem;
|
||||
import org.schabi.newpipe.extractor.Subtitles;
|
||||
import org.schabi.newpipe.extractor.stream.AudioStream;
|
||||
import org.schabi.newpipe.extractor.stream.StreamInfo;
|
||||
import org.schabi.newpipe.extractor.stream.StreamInfoItem;
|
||||
import org.schabi.newpipe.extractor.stream.SubtitlesFormat;
|
||||
import org.schabi.newpipe.extractor.stream.VideoStream;
|
||||
import org.schabi.newpipe.player.playqueue.PlayQueue;
|
||||
import org.schabi.newpipe.player.playqueue.PlayQueueItem;
|
||||
import org.schabi.newpipe.player.playqueue.SinglePlayQueue;
|
||||
|
||||
import java.text.DecimalFormat;
|
||||
import java.text.NumberFormat;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.Formatter;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
|
||||
import javax.annotation.Nonnull;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import static com.google.android.exoplayer2.ui.AspectRatioFrameLayout.RESIZE_MODE_FILL;
|
||||
import static com.google.android.exoplayer2.ui.AspectRatioFrameLayout.RESIZE_MODE_FIT;
|
||||
import static com.google.android.exoplayer2.ui.AspectRatioFrameLayout.RESIZE_MODE_FIXED_HEIGHT;
|
||||
import static com.google.android.exoplayer2.ui.AspectRatioFrameLayout.RESIZE_MODE_FIXED_WIDTH;
|
||||
import static com.google.android.exoplayer2.ui.AspectRatioFrameLayout.RESIZE_MODE_ZOOM;
|
||||
|
||||
public class PlayerHelper {
|
||||
@@ -49,11 +67,11 @@ public class PlayerHelper {
|
||||
: stringFormatter.format("%02d:%02d", minutes, seconds).toString();
|
||||
}
|
||||
|
||||
public static String formatSpeed(float speed) {
|
||||
public static String formatSpeed(double speed) {
|
||||
return speedFormatter.format(speed);
|
||||
}
|
||||
|
||||
public static String formatPitch(float pitch) {
|
||||
public static String formatPitch(double pitch) {
|
||||
return pitchFormatter.format(pitch);
|
||||
}
|
||||
|
||||
@@ -69,10 +87,10 @@ public class PlayerHelper {
|
||||
public static String captionLanguageOf(@NonNull final Context context,
|
||||
@NonNull final Subtitles subtitles) {
|
||||
final String displayName = subtitles.getLocale().getDisplayName(subtitles.getLocale());
|
||||
return displayName + (subtitles.isAutoGenerated() ?
|
||||
" (" + context.getString(R.string.caption_auto_generated)+ ")" : "");
|
||||
return displayName + (subtitles.isAutoGenerated() ? " (" + context.getString(R.string.caption_auto_generated)+ ")" : "");
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public static String resizeTypeOf(@NonNull final Context context,
|
||||
@AspectRatioFrameLayout.ResizeMode final int resizeMode) {
|
||||
switch (resizeMode) {
|
||||
@@ -83,6 +101,58 @@ public class PlayerHelper {
|
||||
}
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public static String cacheKeyOf(@NonNull final StreamInfo info, @NonNull VideoStream video) {
|
||||
return info.getUrl() + video.getResolution() + video.getFormat().getName();
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public static String cacheKeyOf(@NonNull final StreamInfo info, @NonNull AudioStream audio) {
|
||||
return info.getUrl() + audio.getAverageBitrate() + audio.getFormat().getName();
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a {@link StreamInfo} and the existing queue items, provide the
|
||||
* {@link SinglePlayQueue} consisting of the next video for auto queuing.
|
||||
* <br><br>
|
||||
* This method detects and prevents cycle by naively checking if a
|
||||
* candidate next video's url already exists in the existing items.
|
||||
* <br><br>
|
||||
* To select the next video, {@link StreamInfo#getNextVideo()} is first
|
||||
* checked. If it is nonnull and is not part of the existing items, then
|
||||
* it will be used as the next video. Otherwise, an random item with
|
||||
* non-repeating url will be selected from the {@link StreamInfo#getRelatedStreams()}.
|
||||
* */
|
||||
@Nullable
|
||||
public static PlayQueue autoQueueOf(@NonNull final StreamInfo info,
|
||||
@NonNull final List<PlayQueueItem> existingItems) {
|
||||
Set<String> urls = new HashSet<>(existingItems.size());
|
||||
for (final PlayQueueItem item : existingItems) {
|
||||
urls.add(item.getUrl());
|
||||
}
|
||||
|
||||
final StreamInfoItem nextVideo = info.getNextVideo();
|
||||
if (nextVideo != null && !urls.contains(nextVideo.getUrl())) {
|
||||
return new SinglePlayQueue(nextVideo);
|
||||
}
|
||||
|
||||
final List<InfoItem> relatedItems = info.getRelatedStreams();
|
||||
if (relatedItems == null) return null;
|
||||
|
||||
List<StreamInfoItem> autoQueueItems = new ArrayList<>();
|
||||
for (final InfoItem item : info.getRelatedStreams()) {
|
||||
if (item instanceof StreamInfoItem && !urls.contains(item.getUrl())) {
|
||||
autoQueueItems.add((StreamInfoItem) item);
|
||||
}
|
||||
}
|
||||
Collections.shuffle(autoQueueItems);
|
||||
return autoQueueItems.isEmpty() ? null : new SinglePlayQueue(autoQueueItems.get(0));
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////
|
||||
// Settings Resolution
|
||||
////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
public static boolean isResumeAfterAudioFocusGain(@NonNull final Context context) {
|
||||
return isResumeAfterAudioFocusGain(context, false);
|
||||
}
|
||||
@@ -95,10 +165,20 @@ public class PlayerHelper {
|
||||
return isUsingOldPlayer(context, false);
|
||||
}
|
||||
|
||||
public static boolean isRememberingPopupDimensions(@Nonnull final Context context) {
|
||||
public static boolean isRememberingPopupDimensions(@NonNull final Context context) {
|
||||
return isRememberingPopupDimensions(context, true);
|
||||
}
|
||||
|
||||
public static boolean isAutoQueueEnabled(@NonNull final Context context) {
|
||||
return isAutoQueueEnabled(context, false);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public static SeekParameters getSeekParameters(@NonNull final Context context) {
|
||||
return isUsingInexactSeek(context, false) ?
|
||||
SeekParameters.CLOSEST_SYNC : SeekParameters.EXACT;
|
||||
}
|
||||
|
||||
public static long getPreferredCacheSize(@NonNull final Context context) {
|
||||
return 64 * 1024 * 1024L;
|
||||
}
|
||||
@@ -107,30 +187,90 @@ public class PlayerHelper {
|
||||
return 512 * 1024L;
|
||||
}
|
||||
|
||||
public static int getMinBufferMs(@NonNull final Context context) {
|
||||
return 15000;
|
||||
/**
|
||||
* Returns the number of milliseconds the player buffers for before starting playback.
|
||||
* */
|
||||
public static int getPlaybackStartBufferMs(@NonNull final Context context) {
|
||||
return 500;
|
||||
}
|
||||
|
||||
public static int getMaxBufferMs(@NonNull final Context context) {
|
||||
return 30000;
|
||||
/**
|
||||
* Returns the minimum number of milliseconds the player always buffers to after starting
|
||||
* playback.
|
||||
* */
|
||||
public static int getPlaybackMinimumBufferMs(@NonNull final Context context) {
|
||||
return 25000;
|
||||
}
|
||||
|
||||
public static int getBufferForPlaybackMs(@NonNull final Context context) {
|
||||
return 2500;
|
||||
/**
|
||||
* Returns the maximum/optimal number of milliseconds the player will buffer to once the buffer
|
||||
* hits the point of {@link #getPlaybackMinimumBufferMs(Context)}.
|
||||
* */
|
||||
public static int getPlaybackOptimalBufferMs(@NonNull final Context context) {
|
||||
return 60000;
|
||||
}
|
||||
|
||||
public static TrackSelection.Factory getQualitySelector(@NonNull final Context context,
|
||||
@NonNull final BandwidthMeter meter) {
|
||||
return new AdaptiveTrackSelection.Factory(meter,
|
||||
AdaptiveTrackSelection.DEFAULT_MAX_INITIAL_BITRATE,
|
||||
/*bufferDurationRequiredForQualityIncrease=*/1000,
|
||||
AdaptiveTrackSelection.DEFAULT_MAX_DURATION_FOR_QUALITY_DECREASE_MS,
|
||||
AdaptiveTrackSelection.DEFAULT_MIN_DURATION_TO_RETAIN_AFTER_DISCARD_MS,
|
||||
AdaptiveTrackSelection.DEFAULT_BANDWIDTH_FRACTION);
|
||||
}
|
||||
|
||||
public static boolean isUsingDSP(@NonNull final Context context) {
|
||||
return true;
|
||||
}
|
||||
|
||||
public static int getShutdownFlingVelocity(@Nonnull final Context context) {
|
||||
public static int getShutdownFlingVelocity(@NonNull final Context context) {
|
||||
return 10000;
|
||||
}
|
||||
|
||||
public static int getTossFlingVelocity(@Nonnull final Context context) {
|
||||
public static int getTossFlingVelocity(@NonNull final Context context) {
|
||||
return 2500;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public static CaptionStyleCompat getCaptionStyle(@NonNull final Context context) {
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT) return CaptionStyleCompat.DEFAULT;
|
||||
|
||||
final CaptioningManager captioningManager = (CaptioningManager)
|
||||
context.getSystemService(Context.CAPTIONING_SERVICE);
|
||||
if (captioningManager == null || !captioningManager.isEnabled()) {
|
||||
return CaptionStyleCompat.DEFAULT;
|
||||
}
|
||||
|
||||
return CaptionStyleCompat.createFromCaptionStyle(captioningManager.getUserStyle());
|
||||
}
|
||||
|
||||
/**
|
||||
* System font scaling:
|
||||
* Very small - 0.25f, Small - 0.5f, Normal - 1.0f, Large - 1.5f, Very Large - 2.0f
|
||||
* */
|
||||
@NonNull
|
||||
public static float getCaptionScale(@NonNull final Context context) {
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT) return 1f;
|
||||
|
||||
final CaptioningManager captioningManager = (CaptioningManager)
|
||||
context.getSystemService(Context.CAPTIONING_SERVICE);
|
||||
if (captioningManager == null || !captioningManager.isEnabled()) {
|
||||
return 1f;
|
||||
}
|
||||
|
||||
return captioningManager.getFontScale();
|
||||
}
|
||||
|
||||
public static float getScreenBrightness(@NonNull final Context context) {
|
||||
//a value of less than 0, the default, means to use the preferred screen brightness
|
||||
return getScreenBrightness(context, -1);
|
||||
}
|
||||
|
||||
public static void setScreenBrightness(@NonNull final Context context, final float setScreenBrightness) {
|
||||
setScreenBrightness(context, setScreenBrightness, System.currentTimeMillis());
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////
|
||||
// Private helpers
|
||||
////////////////////////////////////////////////////////////////////////////
|
||||
@@ -152,7 +292,34 @@ public class PlayerHelper {
|
||||
return getPreferences(context).getBoolean(context.getString(R.string.use_old_player_key), b);
|
||||
}
|
||||
|
||||
private static boolean isRememberingPopupDimensions(@Nonnull final Context context, final boolean b) {
|
||||
private static boolean isRememberingPopupDimensions(@NonNull final Context context, final boolean b) {
|
||||
return getPreferences(context).getBoolean(context.getString(R.string.popup_remember_size_pos_key), b);
|
||||
}
|
||||
|
||||
private static boolean isUsingInexactSeek(@NonNull final Context context, final boolean b) {
|
||||
return getPreferences(context).getBoolean(context.getString(R.string.use_inexact_seek_key), b);
|
||||
}
|
||||
|
||||
private static boolean isAutoQueueEnabled(@NonNull final Context context, final boolean b) {
|
||||
return getPreferences(context).getBoolean(context.getString(R.string.auto_queue_key), b);
|
||||
}
|
||||
|
||||
private static void setScreenBrightness(@NonNull final Context context, final float screenBrightness, final long timestamp) {
|
||||
SharedPreferences.Editor editor = getPreferences(context).edit();
|
||||
editor.putFloat(context.getString(R.string.screen_brightness_key), screenBrightness);
|
||||
editor.putLong(context.getString(R.string.screen_brightness_timestamp_key), timestamp);
|
||||
editor.apply();
|
||||
}
|
||||
|
||||
private static float getScreenBrightness(@NonNull final Context context, final float screenBrightness) {
|
||||
SharedPreferences sp = getPreferences(context);
|
||||
long timestamp = sp.getLong(context.getString(R.string.screen_brightness_timestamp_key), 0);
|
||||
// hypothesis: 4h covers a viewing block, eg evening. External lightning conditions will change in the next
|
||||
// viewing block so we fall back to the default brightness
|
||||
if ((System.currentTimeMillis() - timestamp) > TimeUnit.HOURS.toMillis(4)) {
|
||||
return screenBrightness;
|
||||
} else {
|
||||
return sp.getFloat(context.getString(R.string.screen_brightness_key), screenBrightness);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
package org.schabi.newpipe.player.mediasession;
|
||||
|
||||
import android.support.v4.media.MediaDescriptionCompat;
|
||||
|
||||
public interface MediaSessionCallback {
|
||||
void onSkipToPrevious();
|
||||
void onSkipToNext();
|
||||
void onSkipToIndex(final int index);
|
||||
|
||||
int getCurrentPlayingIndex();
|
||||
int getQueueSize();
|
||||
MediaDescriptionCompat getQueueMetadata(final int index);
|
||||
|
||||
void onPlay();
|
||||
void onPause();
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user