mirror of
https://github.com/TeamNewPipe/NewPipe
synced 2026-01-13 18:22:41 +00:00
Compare commits
842 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
97ad3c1e6d | ||
|
|
612228bb73 | ||
|
|
6e75d41956 | ||
|
|
9883a38698 | ||
|
|
07256e2e34 | ||
|
|
43674ae80a | ||
|
|
c066ebd76f | ||
|
|
6e382c64a4 | ||
|
|
81e76f260c | ||
|
|
93571961ee | ||
|
|
cb24347b23 | ||
|
|
146b7be825 | ||
|
|
50203c5f87 | ||
|
|
b188073fb0 | ||
|
|
91c60df0e9 | ||
|
|
ad065e9281 | ||
|
|
b1429366da | ||
|
|
8bf7af2e74 | ||
|
|
2003f51d49 | ||
|
|
ce83fd9a10 | ||
|
|
eacbaa3680 | ||
|
|
98c65fb9b7 | ||
|
|
44a71d8565 | ||
|
|
badd4d3207 | ||
|
|
0f517b803b | ||
|
|
c2d11e786f | ||
|
|
b0efe49e29 | ||
|
|
2d029b9f76 | ||
|
|
8ad917cff0 | ||
|
|
4e478c65d3 | ||
|
|
a469086915 | ||
|
|
bf05ff6048 | ||
|
|
a817d8cbf9 | ||
|
|
4a19c78fa5 | ||
|
|
e8bb7da906 | ||
|
|
523477fc2b | ||
|
|
c730426be0 | ||
|
|
57d6c97203 | ||
|
|
fce17aa1d4 | ||
|
|
01abc244b1 | ||
|
|
7bedacf5ad | ||
|
|
552a1d0464 | ||
|
|
8dde25532a | ||
|
|
f29fa939ab | ||
|
|
614bdb33b4 | ||
|
|
71761675cf | ||
|
|
d9194aa859 | ||
|
|
f15081a474 | ||
|
|
2f99ff4a0c | ||
|
|
3a7d26aa46 | ||
|
|
3f35bc593c | ||
|
|
e5e708d781 | ||
|
|
d694561980 | ||
|
|
8d6d18e875 | ||
|
|
072e27ed27 | ||
|
|
6d64215614 | ||
|
|
33f5ed5b14 | ||
|
|
27f509c8e0 | ||
|
|
890b3e13c9 | ||
|
|
b730cb099f | ||
|
|
fc94f184d2 | ||
|
|
cbf6540889 | ||
|
|
40804a7fb3 | ||
|
|
d4101c4f43 | ||
|
|
409bebd5bc | ||
|
|
8e3ad69adb | ||
|
|
c8e46d9e21 | ||
|
|
c56241ffc1 | ||
|
|
be62a2bfc5 | ||
|
|
5cb7771484 | ||
|
|
6675d3e2cd | ||
|
|
8ecbe4c8ad | ||
|
|
edb75c4bab | ||
|
|
54b21c716a | ||
|
|
4704274b87 | ||
|
|
78547aa119 | ||
|
|
3887231c73 | ||
|
|
8a29cfbb7e | ||
|
|
a01d6eaf72 | ||
|
|
69fc571b56 | ||
|
|
4cfd9c322b | ||
|
|
d2dce8801b | ||
|
|
5b8bb9f678 | ||
|
|
2076f146cf | ||
|
|
5d4f2b7862 | ||
|
|
e3815e40d2 | ||
|
|
6dccfb4774 | ||
|
|
1ccc1f4c1a | ||
|
|
042809620a | ||
|
|
cb4c8abd94 | ||
|
|
e86302f5b9 | ||
|
|
de080d5811 | ||
|
|
21b7045f93 | ||
|
|
627301f83d | ||
|
|
607ca84fcc | ||
|
|
a7f36248d0 | ||
|
|
d008d15167 | ||
|
|
607dc436bd | ||
|
|
4384948f6c | ||
|
|
5e05e9ec93 | ||
|
|
ac1fe66cf9 | ||
|
|
86732b6ae4 | ||
|
|
0713f55e9c | ||
|
|
5c32d73409 | ||
|
|
5e13a1735d | ||
|
|
6a1fbb00d9 | ||
|
|
20c3badfac | ||
|
|
7817cfe0c1 | ||
|
|
9ed823b5a5 | ||
|
|
c6a5dedf0a | ||
|
|
01c9ab36b7 | ||
|
|
27f5bdeef1 | ||
|
|
723898f87d | ||
|
|
bc05cc1445 | ||
|
|
dcf4e43e28 | ||
|
|
3868c53908 | ||
|
|
a71c693ca3 | ||
|
|
691f93f01c | ||
|
|
4cff749186 | ||
|
|
e1ac1547fd | ||
|
|
e53bd505fb | ||
|
|
cfa926542e | ||
|
|
79097eca47 | ||
|
|
d1741e40e3 | ||
|
|
5d0528d195 | ||
|
|
a9ea06f753 | ||
|
|
62e121c12c | ||
|
|
02ef05160f | ||
|
|
6e66c013c0 | ||
|
|
dcb11f01e1 | ||
|
|
8a0e4b577c | ||
|
|
b57f420261 | ||
|
|
7d1790abe3 | ||
|
|
1fc494571b | ||
|
|
e6d97bc773 | ||
|
|
0e53323fb7 | ||
|
|
eb4764d2b2 | ||
|
|
298a91adbf | ||
|
|
2cb9912039 | ||
|
|
e52bfe4335 | ||
|
|
761a249e05 | ||
|
|
c42df3a0c2 | ||
|
|
3d359b7a98 | ||
|
|
deef6417ad | ||
|
|
f55a8deb97 | ||
|
|
333506e00b | ||
|
|
bbc1642b90 | ||
|
|
8209eda27a | ||
|
|
b13f7a599b | ||
|
|
cb0f700be1 | ||
|
|
7b6d6b466a | ||
|
|
76f97e5c2e | ||
|
|
4669a1ab57 | ||
|
|
b1ad0edbe1 | ||
|
|
51a695d047 | ||
|
|
396e2d14f3 | ||
|
|
245479c339 | ||
|
|
092215f47a | ||
|
|
fcb46db718 | ||
|
|
60c58c8b9c | ||
|
|
124a2839b5 | ||
|
|
ededfe10ab | ||
|
|
81895c20d6 | ||
|
|
46fabe065c | ||
|
|
7ac338756a | ||
|
|
640b8edd78 | ||
|
|
c5d98752fa | ||
|
|
a6a5bef447 | ||
|
|
042885c155 | ||
|
|
cbb9dcf7d0 | ||
|
|
9b080800e1 | ||
|
|
6effbf50a8 | ||
|
|
398f9aa19a | ||
|
|
935d89747f | ||
|
|
21c2fbfd39 | ||
|
|
bd337f3aac | ||
|
|
4a673eee81 | ||
|
|
cebf349f9a | ||
|
|
3683deb51c | ||
|
|
99ee076db9 | ||
|
|
04f759041f | ||
|
|
75f89059e7 | ||
|
|
a81f31156d | ||
|
|
f706452e67 | ||
|
|
b0126afbcf | ||
|
|
5b8393ff89 | ||
|
|
d2235da06a | ||
|
|
a87f6a0791 | ||
|
|
7e7cfb79a4 | ||
|
|
fb43a5265c | ||
|
|
439a814133 | ||
|
|
d9dfcc04bf | ||
|
|
0cfac137b7 | ||
|
|
4575ee805a | ||
|
|
67f70ce2cc | ||
|
|
2f641ffb13 | ||
|
|
50f92269c2 | ||
|
|
f220f397ae | ||
|
|
cdb4096124 | ||
|
|
01938af65b | ||
|
|
47a1fca32f | ||
|
|
321342cf6d | ||
|
|
086e9beb59 | ||
|
|
3519d4b219 | ||
|
|
9034b9a9ae | ||
|
|
90ba8440a0 | ||
|
|
4988b37d6f | ||
|
|
ad4799ee60 | ||
|
|
35229f8ae5 | ||
|
|
7a011d9e75 | ||
|
|
3d86835979 | ||
|
|
4073306538 | ||
|
|
b86aa28d6a | ||
|
|
d43cee29f2 | ||
|
|
acad468b4a | ||
|
|
4cbe842cfa | ||
|
|
d93e227190 | ||
|
|
a9cf424998 | ||
|
|
6d8fdf46d5 | ||
|
|
781f98ef91 | ||
|
|
1313f685da | ||
|
|
4779a993d3 | ||
|
|
342b2ae5dc | ||
|
|
ce8ae40206 | ||
|
|
d0704f621f | ||
|
|
c6da4043ed | ||
|
|
1aa3761d1a | ||
|
|
ff769caf82 | ||
|
|
be6bc68b56 | ||
|
|
feb3d11f63 | ||
|
|
d2f9b063b2 | ||
|
|
315089c361 | ||
|
|
e95df0dbd5 | ||
|
|
8fed029ee3 | ||
|
|
522daf5aff | ||
|
|
e948eebe84 | ||
|
|
783d4e7e8a | ||
|
|
1ce2198621 | ||
|
|
f521def4a5 | ||
|
|
75202921a1 | ||
|
|
4ef8b93344 | ||
|
|
881b191b8d | ||
|
|
b39e071d1e | ||
|
|
84cb3a1060 | ||
|
|
f4ea3980c2 | ||
|
|
ef73720a5e | ||
|
|
d8cdc57702 | ||
|
|
b52dee37f4 | ||
|
|
21bd9f09da | ||
|
|
44f24e58f6 | ||
|
|
860c4d045a | ||
|
|
3b0c96f654 | ||
|
|
65b744472b | ||
|
|
26489b0f00 | ||
|
|
414abad05f | ||
|
|
735d9a5391 | ||
|
|
50571449cb | ||
|
|
a31aacd115 | ||
|
|
f5b57cc0da | ||
|
|
b7006a8f2c | ||
|
|
82bb467a2a | ||
|
|
3a44e92432 | ||
|
|
e60db5f928 | ||
|
|
78485287a4 | ||
|
|
48b6f01b13 | ||
|
|
39e04de208 | ||
|
|
68d5b59693 | ||
|
|
4ef01ef745 | ||
|
|
573fa8870c | ||
|
|
88d354b08b | ||
|
|
0ff65b5496 | ||
|
|
14e0dcb085 | ||
|
|
e008fd21a4 | ||
|
|
4638149ad0 | ||
|
|
e386bdd6b3 | ||
|
|
decb167ba9 | ||
|
|
dd9557c13e | ||
|
|
708d7162fb | ||
|
|
1ee1b522f1 | ||
|
|
d5a500c037 | ||
|
|
3e02c65bc0 | ||
|
|
d10f9a5f25 | ||
|
|
17e7214d25 | ||
|
|
22774592db | ||
|
|
7c2aa6e69d | ||
|
|
d1dbcda88e | ||
|
|
3e05508cf9 | ||
|
|
1acdefd358 | ||
|
|
23143797f9 | ||
|
|
3364bf268b | ||
|
|
2025d6305e | ||
|
|
639ad7698d | ||
|
|
4eaff51ba2 | ||
|
|
1fb30bc3d9 | ||
|
|
6b66f40bcb | ||
|
|
a3cd531cc8 | ||
|
|
55c84192be | ||
|
|
aa9018b97b | ||
|
|
1e79c146a7 | ||
|
|
cb8545e33d | ||
|
|
4a484c535b | ||
|
|
ee417e41ea | ||
|
|
3cdc1fcaee | ||
|
|
0a023a61f9 | ||
|
|
73f81c5b52 | ||
|
|
bc4acbb7e1 | ||
|
|
26d07ea2c6 | ||
|
|
5380c8352d | ||
|
|
e85e91183e | ||
|
|
a208a22bc2 | ||
|
|
644766b5a6 | ||
|
|
ca679f5932 | ||
|
|
7f7145e8de | ||
|
|
aa1878c15a | ||
|
|
e7d23176b7 | ||
|
|
31218c2a8c | ||
|
|
06374c82fd | ||
|
|
8efcc8f80f | ||
|
|
2d6317bd24 | ||
|
|
157b064214 | ||
|
|
0ece4851d2 | ||
|
|
f1f5996975 | ||
|
|
0a2dbc4688 | ||
|
|
13587d7ab3 | ||
|
|
0fcef064fb | ||
|
|
19b8796cbc | ||
|
|
15fb60a845 | ||
|
|
5c202f04e7 | ||
|
|
bc6fdf81d2 | ||
|
|
3194a2bf2c | ||
|
|
72d1e5131f | ||
|
|
7721098551 | ||
|
|
f563bc4210 | ||
|
|
43e7be9b86 | ||
|
|
27131d15dd | ||
|
|
fb1a290bd9 | ||
|
|
ef16145695 | ||
|
|
4fbd1182c2 | ||
|
|
2d39e65b5c | ||
|
|
8e96b675fa | ||
|
|
adb6943420 | ||
|
|
eae7babf93 | ||
|
|
7d5e18c05b | ||
|
|
cbe001efd6 | ||
|
|
86b783fb0f | ||
|
|
ccc27b48df | ||
|
|
a32391f560 | ||
|
|
aed0348802 | ||
|
|
1b66446c0d | ||
|
|
2bc0c8a483 | ||
|
|
3f7e02e305 | ||
|
|
dd0d666003 | ||
|
|
81859a37de | ||
|
|
180bb581a3 | ||
|
|
2f31779af4 | ||
|
|
ee6d512165 | ||
|
|
1470fdc057 | ||
|
|
93bbaf187e | ||
|
|
f1e43007f1 | ||
|
|
155436b85d | ||
|
|
f3e029c3f6 | ||
|
|
90d6416f55 | ||
|
|
af2a2e45af | ||
|
|
e79eda9c5c | ||
|
|
b338d9dbcf | ||
|
|
7fb9345344 | ||
|
|
77b488568b | ||
|
|
fcf650c6eb | ||
|
|
0c21023ad8 | ||
|
|
8f35a56ec8 | ||
|
|
95ba1873e4 | ||
|
|
8b8652d44c | ||
|
|
2515b8167f | ||
|
|
09dd044f3d | ||
|
|
ace0ed9667 | ||
|
|
cf03708da2 | ||
|
|
d4670bf6fa | ||
|
|
feea448a24 | ||
|
|
71ad54652b | ||
|
|
8e1deda7b0 | ||
|
|
752f985e13 | ||
|
|
89e3219e06 | ||
|
|
429dddc6c9 | ||
|
|
981174a490 | ||
|
|
201f7e9848 | ||
|
|
bb0d8ad58a | ||
|
|
29e64f7c1a | ||
|
|
4819ebd56e | ||
|
|
3b603b0637 | ||
|
|
baa63249d1 | ||
|
|
ee347e3081 | ||
|
|
f96bc95053 | ||
|
|
e1df4757e4 | ||
|
|
4fc37a7321 | ||
|
|
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 | ||
|
|
2837e44bab |
4
.github/CONTRIBUTING.md
vendored
4
.github/CONTRIBUTING.md
vendored
@@ -15,6 +15,8 @@ Do not report crashes in the GitHub issue tracker. NewPipe has an automated cras
|
||||
* If you are an Android/Java developer, you are always welcome to fix/implement an issue/a feature yourself. PRs welcome!
|
||||
* We use English for development. Issues in other languages will be closed and ignored.
|
||||
* Please only add *one* issue at a time. Do not put multiple issues into one thread.
|
||||
* When reporting a bug please give us a context, and a description how to reproduce it.
|
||||
* Issues that only contain a generated bug report, but no describtion might be closed.
|
||||
|
||||
## Bug Fixing
|
||||
* If you want to help NewPipe to become free of bugs (this is our utopic goal for NewPipe), you can send us an email to tnp@newpipe.schabi.org to let me know that you intend to help. We'll send you further instructions. You may, on request, register at our [Sentry](https://sentry.schabi.org) instance (see section "Crash reporting" for more information.
|
||||
@@ -28,7 +30,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.
|
||||
|
||||
1
.github/ISSUE_TEMPLATE.md
vendored
1
.github/ISSUE_TEMPLATE.md
vendored
@@ -1,2 +1,3 @@
|
||||
- [ ] I carefully read the [contribution guidelines](https://github.com/TeamNewPipe/NewPipe/blob/HEAD/.github/CONTRIBUTING.md) and agree to them.
|
||||
- [ ] I checked if the issue/feature exists in the latest version.
|
||||
- [ ] I did use the [incredible bugreport to markdown converter](https://teamnewpipe.github.io/CrashReportToMarkdown/) to paste bug reports.
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,8 +8,8 @@ android {
|
||||
applicationId "org.schabi.newpipe"
|
||||
minSdkVersion 15
|
||||
targetSdkVersion 27
|
||||
versionCode 60
|
||||
versionName "0.13.1"
|
||||
versionCode 67
|
||||
versionName "0.14.0"
|
||||
|
||||
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
|
||||
vectorDrawables.useSupportLibrary = true
|
||||
@@ -41,24 +41,24 @@ android {
|
||||
}
|
||||
|
||||
ext {
|
||||
supportLibVersion = '27.1.0'
|
||||
exoPlayerLibVersion = '2.7.1'
|
||||
roomDbLibVersion = '1.0.0'
|
||||
supportLibVersion = '27.1.1'
|
||||
exoPlayerLibVersion = '2.8.2'
|
||||
roomDbLibVersion = '1.1.1'
|
||||
leakCanaryLibVersion = '1.5.4'
|
||||
okHttpLibVersion = '1.5.0'
|
||||
okHttpLibVersion = '3.10.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:a6b6235644474'
|
||||
|
||||
implementation 'com.github.TeamNewPipe:NewPipeExtractor:850670917fce'
|
||||
|
||||
testImplementation 'junit:junit:4.12'
|
||||
testImplementation 'org.mockito:mockito-core:1.10.19'
|
||||
testImplementation 'org.mockito:mockito-core:2.8.9'
|
||||
|
||||
implementation "com.android.support:appcompat-v7:$supportLibVersion"
|
||||
implementation "com.android.support:support-v4:$supportLibVersion"
|
||||
@@ -66,13 +66,13 @@ 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:4.2.1'
|
||||
|
||||
implementation "com.google.android.exoplayer:exoplayer:$exoPlayerLibVersion"
|
||||
implementation "com.google.android.exoplayer:extension-mediasession:$exoPlayerLibVersion"
|
||||
|
||||
@@ -80,7 +80,7 @@ dependencies {
|
||||
debugImplementation "com.facebook.stetho:stetho-urlconnection:$stethoLibVersion"
|
||||
debugImplementation 'com.android.support:multidex:1.0.3'
|
||||
|
||||
implementation 'io.reactivex.rxjava2:rxjava:2.1.10'
|
||||
implementation 'io.reactivex.rxjava2:rxjava:2.1.14'
|
||||
implementation 'io.reactivex.rxjava2:rxandroid:2.0.2'
|
||||
implementation 'com.jakewharton.rxbinding2:rxbinding:2.1.1'
|
||||
|
||||
@@ -94,6 +94,9 @@ dependencies {
|
||||
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:$okHttpLibVersion"
|
||||
|
||||
implementation "com.squareup.okhttp3:okhttp:$okHttpLibVersion"
|
||||
debugImplementation "com.facebook.stetho:stetho-okhttp3:$stethoLibVersion"
|
||||
implementation 'com.android.support.constraint:constraint-layout:1.1.2'
|
||||
implementation 'com.android.support:cardview-v7:27.1.1'
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -42,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"
|
||||
@@ -72,12 +76,8 @@
|
||||
android:name=".about.AboutActivity"
|
||||
android:label="@string/title_activity_about"/>
|
||||
|
||||
<activity
|
||||
android:name=".history.HistoryActivity"
|
||||
android:label="@string/title_activity_history"/>
|
||||
|
||||
<service android:name=".subscription.services.SubscriptionsImportService"/>
|
||||
<service android:name=".subscription.services.SubscriptionsExportService"/>
|
||||
<service android:name=".local.subscription.services.SubscriptionsImportService"/>
|
||||
<service android:name=".local.subscription.services.SubscriptionsExportService"/>
|
||||
|
||||
<activity
|
||||
android:name=".PanicResponderActivity"
|
||||
@@ -118,6 +118,7 @@
|
||||
<activity
|
||||
android:name=".ReCaptchaActivity"
|
||||
android:label="@string/reCaptchaActivity"/>
|
||||
<activity android:name=".download.ExtSDDownloadFailedActivity" />
|
||||
|
||||
<provider
|
||||
android:name="android.support.v4.content.FileProvider"
|
||||
@@ -132,7 +133,7 @@
|
||||
<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">
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ import android.content.Context;
|
||||
import android.os.Bundle;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.v4.app.Fragment;
|
||||
import android.support.v4.app.FragmentManager;
|
||||
import android.support.v7.app.AppCompatActivity;
|
||||
import android.util.Log;
|
||||
import android.view.View;
|
||||
@@ -11,7 +12,10 @@ import android.view.View;
|
||||
import com.nostra13.universalimageloader.core.ImageLoader;
|
||||
import com.squareup.leakcanary.RefWatcher;
|
||||
|
||||
import org.schabi.newpipe.report.UserAction;
|
||||
|
||||
import icepick.Icepick;
|
||||
import icepick.State;
|
||||
|
||||
public abstract class BaseFragment extends Fragment {
|
||||
protected final String TAG = getClass().getSimpleName() + "@" + Integer.toHexString(hashCode());
|
||||
@@ -20,6 +24,15 @@ public abstract class BaseFragment extends Fragment {
|
||||
protected AppCompatActivity activity;
|
||||
public static final ImageLoader imageLoader = ImageLoader.getInstance();
|
||||
|
||||
//These values are used for controlling framgents when they are part of the frontpage
|
||||
@State
|
||||
protected boolean useAsFrontPage = false;
|
||||
protected boolean mIsVisibleToUser = false;
|
||||
|
||||
public void useAsFrontPage(boolean value) {
|
||||
useAsFrontPage = value;
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Fragment's Lifecycle
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
@@ -72,6 +85,12 @@ public abstract class BaseFragment extends Fragment {
|
||||
if (refWatcher != null) refWatcher.watch(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setUserVisibleHint(boolean isVisibleToUser) {
|
||||
super.setUserVisibleHint(isVisibleToUser);
|
||||
mIsVisibleToUser = isVisibleToUser;
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Init
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
@@ -88,8 +107,15 @@ public abstract class BaseFragment extends Fragment {
|
||||
|
||||
public void setTitle(String title) {
|
||||
if (DEBUG) Log.d(TAG, "setTitle() called with: title = [" + title + "]");
|
||||
if (activity != null && activity.getSupportActionBar() != null) {
|
||||
if((!useAsFrontPage || mIsVisibleToUser)
|
||||
&& (activity != null && activity.getSupportActionBar() != null)) {
|
||||
activity.getSupportActionBar().setTitle(title);
|
||||
}
|
||||
}
|
||||
|
||||
protected FragmentManager getFM() {
|
||||
return getParentFragment() == null
|
||||
? getFragmentManager()
|
||||
: getParentFragment().getFragmentManager();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -73,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.
|
||||
|
||||
@@ -22,11 +22,14 @@ package org.schabi.newpipe;
|
||||
|
||||
import android.content.Intent;
|
||||
import android.content.SharedPreferences;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.net.Uri;
|
||||
import android.os.Build;
|
||||
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;
|
||||
@@ -41,23 +44,30 @@ import android.view.Menu;
|
||||
import android.view.MenuInflater;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
import android.view.Window;
|
||||
import android.view.WindowManager;
|
||||
import android.widget.Button;
|
||||
import android.widget.ImageButton;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.TextView;
|
||||
|
||||
import org.schabi.newpipe.extractor.NewPipe;
|
||||
import org.schabi.newpipe.extractor.StreamingService;
|
||||
import org.schabi.newpipe.extractor.exceptions.ExtractionException;
|
||||
import org.schabi.newpipe.fragments.BackPressable;
|
||||
import org.schabi.newpipe.fragments.MainFragment;
|
||||
import org.schabi.newpipe.fragments.detail.VideoDetailFragment;
|
||||
import org.schabi.newpipe.fragments.list.search.SearchFragment;
|
||||
import org.schabi.newpipe.report.ErrorActivity;
|
||||
import org.schabi.newpipe.util.Constants;
|
||||
import org.schabi.newpipe.util.KioskTranslator;
|
||||
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;
|
||||
|
||||
import static org.schabi.newpipe.extractor.InfoItem.InfoType.PLAYLIST;
|
||||
|
||||
public class MainActivity extends AppCompatActivity {
|
||||
private static final String TAG = "MainActivity";
|
||||
public static final boolean DEBUG = !BuildConfig.BUILD_TYPE.equals("release");
|
||||
@@ -67,6 +77,19 @@ public class MainActivity extends AppCompatActivity {
|
||||
private NavigationView drawerItems = null;
|
||||
private TextView headerServiceView = null;
|
||||
|
||||
private boolean servicesShown = false;
|
||||
private ImageView serviceArrow;
|
||||
|
||||
private static final int ITEM_ID_SUBSCRIPTIONS = - 1;
|
||||
private static final int ITEM_ID_FEED = - 2;
|
||||
private static final int ITEM_ID_BOOKMARKS = - 3;
|
||||
private static final int ITEM_ID_DOWNLOADS = - 4;
|
||||
private static final int ITEM_ID_HISTORY = - 5;
|
||||
private static final int ITEM_ID_SETTINGS = 0;
|
||||
private static final int ITEM_ID_ABOUT = 1;
|
||||
|
||||
private static final int ORDER = 0;
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Activity's LifeCycle
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
@@ -80,29 +103,64 @@ public class MainActivity extends AppCompatActivity {
|
||||
super.onCreate(savedInstanceState);
|
||||
setContentView(R.layout.activity_main);
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
||||
Window w = getWindow();
|
||||
w.setFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS, WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS);
|
||||
}
|
||||
|
||||
if (getSupportFragmentManager() != null && getSupportFragmentManager().getBackStackEntryCount() == 0) {
|
||||
initFragments();
|
||||
}
|
||||
|
||||
setSupportActionBar(findViewById(R.id.toolbar));
|
||||
setupDrawer();
|
||||
try {
|
||||
setupDrawer();
|
||||
} catch (Exception e) {
|
||||
ErrorActivity.reportUiError(this, e);
|
||||
}
|
||||
}
|
||||
|
||||
private void setupDrawer() {
|
||||
private void setupDrawer() throws Exception {
|
||||
final Toolbar toolbar = findViewById(R.id.toolbar);
|
||||
drawer = findViewById(R.id.drawer_layout);
|
||||
drawerItems = findViewById(R.id.navigation);
|
||||
|
||||
for(StreamingService s : NewPipe.getServices()) {
|
||||
String title =
|
||||
s.getServiceInfo().getName() +
|
||||
(ServiceHelper.isBeta(s) ? " (beta)" : "");
|
||||
MenuItem item = drawerItems.getMenu()
|
||||
.add(R.id.menu_services_group, s.getServiceId(), 0, title);
|
||||
item.setIcon(ServiceHelper.getIcon(s.getServiceId()));
|
||||
//Tabs
|
||||
int currentServiceId = ServiceHelper.getSelectedServiceId(this);
|
||||
StreamingService service = NewPipe.getService(currentServiceId);
|
||||
|
||||
int kioskId = 0;
|
||||
|
||||
for (final String ks : service.getKioskList().getAvailableKiosks()) {
|
||||
drawerItems.getMenu()
|
||||
.add(R.id.menu_tabs_group, kioskId, 0, KioskTranslator.getTranslatedKioskName(ks, this))
|
||||
.setIcon(KioskTranslator.getKioskIcons(ks, this));
|
||||
kioskId ++;
|
||||
}
|
||||
|
||||
drawerItems.getMenu().getItem(ServiceHelper.getSelectedServiceId(this)).setChecked(true);
|
||||
drawerItems.getMenu()
|
||||
.add(R.id.menu_tabs_group, ITEM_ID_SUBSCRIPTIONS, ORDER, R.string.tab_subscriptions)
|
||||
.setIcon(ThemeHelper.resolveResourceIdFromAttr(this, R.attr.ic_channel));
|
||||
drawerItems.getMenu()
|
||||
.add(R.id.menu_tabs_group, ITEM_ID_FEED, ORDER, R.string.fragment_whats_new)
|
||||
.setIcon(ThemeHelper.resolveResourceIdFromAttr(this, R.attr.rss));
|
||||
drawerItems.getMenu()
|
||||
.add(R.id.menu_tabs_group, ITEM_ID_BOOKMARKS, ORDER, R.string.tab_bookmarks)
|
||||
.setIcon(ThemeHelper.resolveResourceIdFromAttr(this, R.attr.ic_bookmark));
|
||||
drawerItems.getMenu()
|
||||
.add(R.id.menu_tabs_group, ITEM_ID_DOWNLOADS, ORDER, R.string.downloads)
|
||||
.setIcon(ThemeHelper.resolveResourceIdFromAttr(this, R.attr.download));
|
||||
drawerItems.getMenu()
|
||||
.add(R.id.menu_tabs_group, ITEM_ID_HISTORY, ORDER, R.string.action_history)
|
||||
.setIcon(ThemeHelper.resolveResourceIdFromAttr(this, R.attr.history));
|
||||
|
||||
//Settings and About
|
||||
drawerItems.getMenu()
|
||||
.add(R.id.menu_options_about_group, ITEM_ID_SETTINGS, ORDER, R.string.settings)
|
||||
.setIcon(ThemeHelper.resolveResourceIdFromAttr(this, R.attr.settings));
|
||||
drawerItems.getMenu()
|
||||
.add(R.id.menu_options_about_group, ITEM_ID_ABOUT, ORDER, R.string.tab_about)
|
||||
.setIcon(ThemeHelper.resolveResourceIdFromAttr(this, R.attr.info));
|
||||
|
||||
toggle = new ActionBarDrawerToggle(this, drawer, toolbar, R.string.drawer_open, R.string.drawer_close);
|
||||
toggle.syncState();
|
||||
@@ -117,52 +175,179 @@ public class MainActivity extends AppCompatActivity {
|
||||
|
||||
@Override
|
||||
public void onDrawerClosed(View drawerView) {
|
||||
if(servicesShown) {
|
||||
toggleServices();
|
||||
}
|
||||
if (lastService != ServiceHelper.getSelectedServiceId(MainActivity.this)) {
|
||||
new Handler(Looper.getMainLooper()).post(MainActivity.this::recreate);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
drawerItems.setNavigationItemSelectedListener(this::changeService);
|
||||
|
||||
setupDrawerFooter();
|
||||
drawerItems.setNavigationItemSelectedListener(this::drawerItemSelected);
|
||||
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.getItemId());
|
||||
drawerItems.getMenu().getItem(ServiceHelper.getSelectedServiceId(this)).setChecked(true);
|
||||
} else {
|
||||
return false;
|
||||
private boolean drawerItemSelected(MenuItem item) {
|
||||
switch (item.getGroupId()) {
|
||||
case R.id.menu_services_group:
|
||||
changeService(item);
|
||||
break;
|
||||
case R.id.menu_tabs_group:
|
||||
try {
|
||||
tabSelected(item);
|
||||
} catch (Exception e) {
|
||||
ErrorActivity.reportUiError(this, e);
|
||||
}
|
||||
break;
|
||||
case R.id.menu_options_about_group:
|
||||
optionsAboutSelected(item);
|
||||
break;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
|
||||
drawer.closeDrawers();
|
||||
return true;
|
||||
}
|
||||
|
||||
private void setupDrawerFooter() {
|
||||
ImageButton settings = findViewById(R.id.drawer_settings);
|
||||
ImageButton downloads = findViewById(R.id.drawer_downloads);
|
||||
ImageButton history = findViewById(R.id.drawer_history);
|
||||
private void changeService(MenuItem item) {
|
||||
drawerItems.getMenu().getItem(ServiceHelper.getSelectedServiceId(this)).setChecked(false);
|
||||
ServiceHelper.setSelectedServiceId(this, item.getItemId());
|
||||
drawerItems.getMenu().getItem(ServiceHelper.getSelectedServiceId(this)).setChecked(true);
|
||||
}
|
||||
|
||||
settings.setOnClickListener(view -> NavigationHelper.openSettings(this));
|
||||
downloads.setOnClickListener(view ->NavigationHelper.openDownloads(this));
|
||||
history.setOnClickListener(view -> NavigationHelper.openHistory(this));
|
||||
private void tabSelected(MenuItem item) throws ExtractionException {
|
||||
switch(item.getItemId()) {
|
||||
case ITEM_ID_SUBSCRIPTIONS:
|
||||
NavigationHelper.openSubscriptionFragment(getSupportFragmentManager());
|
||||
break;
|
||||
case ITEM_ID_FEED:
|
||||
NavigationHelper.openWhatsNewFragment(getSupportFragmentManager());
|
||||
break;
|
||||
case ITEM_ID_BOOKMARKS:
|
||||
NavigationHelper.openBookmarksFragment(getSupportFragmentManager());
|
||||
break;
|
||||
case ITEM_ID_DOWNLOADS:
|
||||
NavigationHelper.openDownloads(this);
|
||||
break;
|
||||
case ITEM_ID_HISTORY:
|
||||
NavigationHelper.openStatisticFragment(getSupportFragmentManager());
|
||||
break;
|
||||
default:
|
||||
int currentServiceId = ServiceHelper.getSelectedServiceId(this);
|
||||
StreamingService service = NewPipe.getService(currentServiceId);
|
||||
String serviceName = "";
|
||||
|
||||
int kioskId = 0;
|
||||
for (final String ks : service.getKioskList().getAvailableKiosks()) {
|
||||
if(kioskId == item.getItemId()) {
|
||||
serviceName = ks;
|
||||
}
|
||||
kioskId ++;
|
||||
}
|
||||
|
||||
NavigationHelper.openKioskFragment(getSupportFragmentManager(), currentServiceId, serviceName);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private void optionsAboutSelected(MenuItem item) {
|
||||
switch(item.getItemId()) {
|
||||
case ITEM_ID_SETTINGS:
|
||||
NavigationHelper.openSettings(this);
|
||||
break;
|
||||
case ITEM_ID_ABOUT:
|
||||
NavigationHelper.openAbout(this);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private void setupDrawerHeader() {
|
||||
headerServiceView = findViewById(R.id.drawer_header_service_view);
|
||||
Button action = findViewById(R.id.drawer_header_action_button);
|
||||
NavigationView navigationView = findViewById(R.id.navigation);
|
||||
View hView = navigationView.getHeaderView(0);
|
||||
|
||||
serviceArrow = hView.findViewById(R.id.drawer_arrow);
|
||||
headerServiceView = hView.findViewById(R.id.drawer_header_service_view);
|
||||
Button action = hView.findViewById(R.id.drawer_header_action_button);
|
||||
action.setOnClickListener(view -> {
|
||||
Intent intent = new Intent(Intent.ACTION_VIEW);
|
||||
intent.setData(Uri.parse("https://newpipe.schabi.org/blog/"));
|
||||
startActivity(intent);
|
||||
drawer.closeDrawers();
|
||||
toggleServices();
|
||||
});
|
||||
}
|
||||
|
||||
private void toggleServices() {
|
||||
servicesShown = !servicesShown;
|
||||
|
||||
drawerItems.getMenu().removeGroup(R.id.menu_services_group);
|
||||
drawerItems.getMenu().removeGroup(R.id.menu_tabs_group);
|
||||
drawerItems.getMenu().removeGroup(R.id.menu_options_about_group);
|
||||
|
||||
if(servicesShown) {
|
||||
showServices();
|
||||
} else {
|
||||
try {
|
||||
showTabs();
|
||||
} catch (Exception e) {
|
||||
ErrorActivity.reportUiError(this, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void showServices() {
|
||||
serviceArrow.setImageResource(R.drawable.ic_arrow_up_white);
|
||||
|
||||
for(StreamingService s : NewPipe.getServices()) {
|
||||
final String title = s.getServiceInfo().getName() +
|
||||
(ServiceHelper.isBeta(s) ? " (beta)" : "");
|
||||
|
||||
drawerItems.getMenu()
|
||||
.add(R.id.menu_services_group, s.getServiceId(), ORDER, title)
|
||||
.setIcon(ServiceHelper.getIcon(s.getServiceId()));
|
||||
}
|
||||
drawerItems.getMenu().getItem(ServiceHelper.getSelectedServiceId(this)).setChecked(true);
|
||||
}
|
||||
|
||||
private void showTabs() throws ExtractionException {
|
||||
serviceArrow.setImageResource(R.drawable.ic_arrow_down_white);
|
||||
|
||||
//Tabs
|
||||
int currentServiceId = ServiceHelper.getSelectedServiceId(this);
|
||||
StreamingService service = NewPipe.getService(currentServiceId);
|
||||
|
||||
int kioskId = 0;
|
||||
|
||||
for (final String ks : service.getKioskList().getAvailableKiosks()) {
|
||||
drawerItems.getMenu()
|
||||
.add(R.id.menu_tabs_group, kioskId, ORDER, KioskTranslator.getTranslatedKioskName(ks, this))
|
||||
.setIcon(KioskTranslator.getKioskIcons(ks, this));
|
||||
kioskId ++;
|
||||
}
|
||||
|
||||
drawerItems.getMenu()
|
||||
.add(R.id.menu_tabs_group, ITEM_ID_SUBSCRIPTIONS, ORDER, R.string.tab_subscriptions)
|
||||
.setIcon(ThemeHelper.resolveResourceIdFromAttr(this, R.attr.ic_channel));
|
||||
drawerItems.getMenu()
|
||||
.add(R.id.menu_tabs_group, ITEM_ID_FEED, ORDER, R.string.fragment_whats_new)
|
||||
.setIcon(ThemeHelper.resolveResourceIdFromAttr(this, R.attr.rss));
|
||||
drawerItems.getMenu()
|
||||
.add(R.id.menu_tabs_group, ITEM_ID_BOOKMARKS, ORDER, R.string.tab_bookmarks)
|
||||
.setIcon(ThemeHelper.resolveResourceIdFromAttr(this, R.attr.ic_bookmark));
|
||||
drawerItems.getMenu()
|
||||
.add(R.id.menu_tabs_group, ITEM_ID_DOWNLOADS, ORDER, R.string.downloads)
|
||||
.setIcon(ThemeHelper.resolveResourceIdFromAttr(this, R.attr.download));
|
||||
drawerItems.getMenu()
|
||||
.add(R.id.menu_tabs_group, ITEM_ID_HISTORY, ORDER, R.string.action_history)
|
||||
.setIcon(ThemeHelper.resolveResourceIdFromAttr(this, R.attr.history));
|
||||
|
||||
//Settings and About
|
||||
drawerItems.getMenu()
|
||||
.add(R.id.menu_options_about_group, ITEM_ID_SETTINGS, ORDER, R.string.settings)
|
||||
.setIcon(ThemeHelper.resolveResourceIdFromAttr(this, R.attr.settings));
|
||||
drawerItems.getMenu()
|
||||
.add(R.id.menu_options_about_group, ITEM_ID_ABOUT, ORDER, R.string.tab_about)
|
||||
.setIcon(ThemeHelper.resolveResourceIdFromAttr(this, R.attr.info));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDestroy() {
|
||||
super.onDestroy();
|
||||
@@ -233,6 +418,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>
|
||||
@@ -305,14 +510,14 @@ 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_about:
|
||||
NavigationHelper.openAbout(this);
|
||||
return true;
|
||||
return NavigationHelper.openDownloads(this);
|
||||
case R.id.action_history:
|
||||
NavigationHelper.openStatisticFragment(getSupportFragmentManager());
|
||||
return true;
|
||||
case R.id.action_settings:
|
||||
NavigationHelper.openSettings(this);
|
||||
return true;
|
||||
default:
|
||||
return super.onOptionsItemSelected(item);
|
||||
}
|
||||
@@ -356,31 +561,45 @@ public class MainActivity extends AppCompatActivity {
|
||||
}
|
||||
|
||||
private void handleIntent(Intent intent) {
|
||||
if (DEBUG) Log.d(TAG, "handleIntent() called with: intent = [" + intent + "]");
|
||||
try {
|
||||
if (DEBUG) Log.d(TAG, "handleIntent() called with: intent = [" + intent + "]");
|
||||
|
||||
if (intent.hasExtra(Constants.KEY_LINK_TYPE)) {
|
||||
String url = intent.getStringExtra(Constants.KEY_URL);
|
||||
int serviceId = intent.getIntExtra(Constants.KEY_SERVICE_ID, 0);
|
||||
String title = intent.getStringExtra(Constants.KEY_TITLE);
|
||||
switch (((StreamingService.LinkType) intent.getSerializableExtra(Constants.KEY_LINK_TYPE))) {
|
||||
case STREAM:
|
||||
boolean autoPlay = intent.getBooleanExtra(VideoDetailFragment.AUTO_PLAY, false);
|
||||
NavigationHelper.openVideoDetailFragment(getSupportFragmentManager(), serviceId, url, title, autoPlay);
|
||||
break;
|
||||
case CHANNEL:
|
||||
NavigationHelper.openChannelFragment(getSupportFragmentManager(), serviceId, url, title);
|
||||
break;
|
||||
case PLAYLIST:
|
||||
NavigationHelper.openPlaylistFragment(getSupportFragmentManager(), serviceId, url, title);
|
||||
break;
|
||||
if (intent.hasExtra(Constants.KEY_LINK_TYPE)) {
|
||||
String url = intent.getStringExtra(Constants.KEY_URL);
|
||||
int serviceId = intent.getIntExtra(Constants.KEY_SERVICE_ID, 0);
|
||||
String title = intent.getStringExtra(Constants.KEY_TITLE);
|
||||
switch (((StreamingService.LinkType) intent.getSerializableExtra(Constants.KEY_LINK_TYPE))) {
|
||||
case STREAM:
|
||||
boolean autoPlay = intent.getBooleanExtra(VideoDetailFragment.AUTO_PLAY, false);
|
||||
NavigationHelper.openVideoDetailFragment(getSupportFragmentManager(), serviceId, url, title, autoPlay);
|
||||
break;
|
||||
case CHANNEL:
|
||||
NavigationHelper.openChannelFragment(getSupportFragmentManager(),
|
||||
serviceId,
|
||||
url,
|
||||
title);
|
||||
break;
|
||||
case PLAYLIST:
|
||||
NavigationHelper.openPlaylistFragment(getSupportFragmentManager(),
|
||||
serviceId,
|
||||
url,
|
||||
title);
|
||||
break;
|
||||
}
|
||||
} else if (intent.hasExtra(Constants.KEY_OPEN_SEARCH)) {
|
||||
String searchString = intent.getStringExtra(Constants.KEY_SEARCH_STRING);
|
||||
if (searchString == null) searchString = "";
|
||||
int serviceId = intent.getIntExtra(Constants.KEY_SERVICE_ID, 0);
|
||||
NavigationHelper.openSearchFragment(
|
||||
getSupportFragmentManager(),
|
||||
serviceId,
|
||||
searchString);
|
||||
|
||||
} else {
|
||||
NavigationHelper.gotoMainFragment(getSupportFragmentManager());
|
||||
}
|
||||
} else if (intent.hasExtra(Constants.KEY_OPEN_SEARCH)) {
|
||||
String searchQuery = intent.getStringExtra(Constants.KEY_QUERY);
|
||||
if (searchQuery == null) searchQuery = "";
|
||||
int serviceId = intent.getIntExtra(Constants.KEY_SERVICE_ID, 0);
|
||||
NavigationHelper.openSearchFragment(getSupportFragmentManager(), serviceId, searchQuery);
|
||||
} else {
|
||||
NavigationHelper.gotoMainFragment(getSupportFragmentManager());
|
||||
} catch (Exception e) {
|
||||
ErrorActivity.reportUiError(this, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,13 +1,19 @@
|
||||
package org.schabi.newpipe;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.app.FragmentManager;
|
||||
import android.app.IntentService;
|
||||
import android.content.Context;
|
||||
import android.content.DialogInterface;
|
||||
import android.content.Intent;
|
||||
import android.content.SharedPreferences;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.os.Bundle;
|
||||
import android.preference.PreferenceManager;
|
||||
import android.support.annotation.DrawableRes;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.support.v4.app.Fragment;
|
||||
import android.support.v4.app.NotificationCompat;
|
||||
import android.support.v7.app.AlertDialog;
|
||||
import android.support.v7.app.AppCompatActivity;
|
||||
@@ -22,30 +28,37 @@ import android.widget.RadioButton;
|
||||
import android.widget.RadioGroup;
|
||||
import android.widget.Toast;
|
||||
|
||||
import org.schabi.newpipe.download.DownloadDialog;
|
||||
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;
|
||||
import org.schabi.newpipe.extractor.exceptions.ExtractionException;
|
||||
import org.schabi.newpipe.extractor.playlist.PlaylistInfo;
|
||||
import org.schabi.newpipe.extractor.stream.StreamInfo;
|
||||
import org.schabi.newpipe.extractor.stream.VideoStream;
|
||||
import org.schabi.newpipe.fragments.detail.VideoDetailFragment;
|
||||
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.ListHelper;
|
||||
import org.schabi.newpipe.util.NavigationHelper;
|
||||
import org.schabi.newpipe.util.PermissionHelper;
|
||||
import org.schabi.newpipe.util.ThemeHelper;
|
||||
|
||||
import java.io.IOException;
|
||||
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 java.util.Observer;
|
||||
|
||||
import icepick.Icepick;
|
||||
import icepick.State;
|
||||
@@ -57,6 +70,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,18 +79,17 @@ 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;
|
||||
protected CompositeDisposable disposables = new CompositeDisposable();
|
||||
|
||||
private boolean selectionIsDownload = false;
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
@@ -91,8 +105,7 @@ public class RouterActivity extends AppCompatActivity {
|
||||
}
|
||||
|
||||
setTheme(ThemeHelper.isLightThemeSelected(this)
|
||||
? R.style.RouterActivityThemeLight
|
||||
: R.style.RouterActivityThemeDark);
|
||||
? R.style.RouterActivityThemeLight : R.style.RouterActivityThemeDark);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -104,7 +117,7 @@ public class RouterActivity extends AppCompatActivity {
|
||||
@Override
|
||||
protected void onStart() {
|
||||
super.onStart();
|
||||
|
||||
|
||||
handleUrl(currentUrl);
|
||||
}
|
||||
|
||||
@@ -122,7 +135,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 +172,72 @@ 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 downloadKey = getString(R.string.download_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 if (selectedChoiceKey.equals(downloadKey)) {
|
||||
handleChoice(downloadKey);
|
||||
} 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,15 +246,18 @@ 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)
|
||||
.setPositiveButton(R.string.always, dialogButtonsClickListener)
|
||||
.setOnDismissListener((dialog) -> finish())
|
||||
.setOnDismissListener((dialog) -> {
|
||||
if(!selectionIsDownload) finish();
|
||||
})
|
||||
.create();
|
||||
|
||||
//noinspection CodeBlock2Expr
|
||||
alertDialog.setOnShowListener(dialog -> {
|
||||
setDialogButtonsState(alertDialog, radioGroup.getCheckedRadioButtonId() != -1);
|
||||
});
|
||||
@@ -240,7 +271,7 @@ public class RouterActivity extends AppCompatActivity {
|
||||
selectedRadioPosition = indexOfChild;
|
||||
|
||||
if (selectedPreviously == selectedRadioPosition) {
|
||||
handleChoice(choices[selectedRadioPosition].key);
|
||||
handleChoice(choices.get(selectedRadioPosition).key);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -259,8 +290,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 +300,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 +309,42 @@ 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)));
|
||||
}
|
||||
|
||||
returnList.add(new AdapterChoiceItem(getString(R.string.download_key), getString(R.string.download),
|
||||
resolveResourceIdFromAttr(context, R.attr.download)));
|
||||
|
||||
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,25 +354,31 @@ 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;
|
||||
}
|
||||
|
||||
if (selectedChoiceKey.equals(getString(R.string.download_key))) {
|
||||
if (PermissionHelper.checkStoragePermissions(this, PermissionHelper.DOWNLOAD_DIALOG_REQUEST_CODE)) {
|
||||
selectionIsDownload = true;
|
||||
openDownloadDialog();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// 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,20 +395,57 @@ 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();
|
||||
}
|
||||
|
||||
@SuppressLint("CheckResult")
|
||||
private void openDownloadDialog() {
|
||||
ExtractorHelper.getStreamInfo(currentServiceId, currentUrl, true)
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe((@NonNull StreamInfo result) -> {
|
||||
List<VideoStream> sortedVideoStreams = ListHelper.getSortedStreamVideosList(this,
|
||||
result.getVideoStreams(),
|
||||
result.getVideoOnlyStreams(),
|
||||
false);
|
||||
int selectedVideoStreamIndex = ListHelper.getDefaultResolutionIndex(this,
|
||||
sortedVideoStreams);
|
||||
|
||||
android.support.v4.app.FragmentManager fm = getSupportFragmentManager();
|
||||
DownloadDialog downloadDialog = DownloadDialog.newInstance(result);
|
||||
downloadDialog.setVideoStreams(sortedVideoStreams);
|
||||
downloadDialog.setAudioStreams(result.getAudioStreams());
|
||||
downloadDialog.setSelectedVideoStream(selectedVideoStreamIndex);
|
||||
downloadDialog.show(fm, "downloadDialog");
|
||||
fm.executePendingTransactions();
|
||||
downloadDialog.getDialog().setOnDismissListener(dialog -> {
|
||||
finish();
|
||||
});
|
||||
}, (@NonNull Throwable throwable) -> {
|
||||
onError();
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
|
||||
for (int i: grantResults){
|
||||
if (i == PackageManager.PERMISSION_DENIED){
|
||||
finish();
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (requestCode == PermissionHelper.DOWNLOAD_DIALOG_REQUEST_CODE) {
|
||||
openDownloadDialog();
|
||||
}
|
||||
}
|
||||
|
||||
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 +664,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);
|
||||
|
||||
@@ -71,6 +71,14 @@ public class PlaylistRemoteEntity implements PlaylistLocalItem {
|
||||
info.getUploaderName(), info.getStreamCount());
|
||||
}
|
||||
|
||||
@Ignore
|
||||
public boolean isIdenticalTo(final PlaylistInfo info) {
|
||||
return getServiceId() == info.getServiceId() && getName().equals(info.getName()) &&
|
||||
getStreamCount() == info.getStreamCount() && getUrl().equals(info.getUrl()) &&
|
||||
getThumbnailUrl().equals(info.getThumbnailUrl()) &&
|
||||
getUploader().equals(info.getUploaderName());
|
||||
}
|
||||
|
||||
public long getUid() {
|
||||
return uid;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -0,0 +1,158 @@
|
||||
package org.schabi.newpipe.download;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.os.Bundle;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.support.design.widget.BaseTransientBottomBar;
|
||||
import android.support.design.widget.Snackbar;
|
||||
import android.view.View;
|
||||
|
||||
import org.schabi.newpipe.R;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import io.reactivex.Completable;
|
||||
import io.reactivex.Observable;
|
||||
import io.reactivex.android.schedulers.AndroidSchedulers;
|
||||
import io.reactivex.disposables.Disposable;
|
||||
import io.reactivex.schedulers.Schedulers;
|
||||
import io.reactivex.subjects.PublishSubject;
|
||||
import us.shandian.giga.get.DownloadManager;
|
||||
import us.shandian.giga.get.DownloadMission;
|
||||
|
||||
public class DeleteDownloadManager {
|
||||
|
||||
private static final String KEY_STATE = "delete_manager_state";
|
||||
|
||||
private View mView;
|
||||
private HashSet<String> mPendingMap;
|
||||
private List<Disposable> mDisposableList;
|
||||
private DownloadManager mDownloadManager;
|
||||
private PublishSubject<DownloadMission> publishSubject = PublishSubject.create();
|
||||
|
||||
DeleteDownloadManager(Activity activity) {
|
||||
mPendingMap = new HashSet<>();
|
||||
mDisposableList = new ArrayList<>();
|
||||
mView = activity.findViewById(android.R.id.content);
|
||||
}
|
||||
|
||||
public Observable<DownloadMission> getUndoObservable() {
|
||||
return publishSubject;
|
||||
}
|
||||
|
||||
public boolean contains(@NonNull DownloadMission mission) {
|
||||
return mPendingMap.contains(mission.url);
|
||||
}
|
||||
|
||||
public void add(@NonNull DownloadMission mission) {
|
||||
mPendingMap.add(mission.url);
|
||||
|
||||
if (mPendingMap.size() == 1) {
|
||||
showUndoDeleteSnackbar(mission);
|
||||
}
|
||||
}
|
||||
|
||||
public void setDownloadManager(@NonNull DownloadManager downloadManager) {
|
||||
mDownloadManager = downloadManager;
|
||||
|
||||
if (mPendingMap.size() < 1) return;
|
||||
|
||||
showUndoDeleteSnackbar();
|
||||
}
|
||||
|
||||
public void restoreState(@Nullable Bundle savedInstanceState) {
|
||||
if (savedInstanceState == null) return;
|
||||
|
||||
List<String> list = savedInstanceState.getStringArrayList(KEY_STATE);
|
||||
if (list != null) {
|
||||
mPendingMap.addAll(list);
|
||||
}
|
||||
}
|
||||
|
||||
public void saveState(@Nullable Bundle outState) {
|
||||
if (outState == null) return;
|
||||
|
||||
for (Disposable disposable : mDisposableList) {
|
||||
disposable.dispose();
|
||||
}
|
||||
|
||||
outState.putStringArrayList(KEY_STATE, new ArrayList<>(mPendingMap));
|
||||
}
|
||||
|
||||
private void showUndoDeleteSnackbar() {
|
||||
if (mPendingMap.size() < 1) return;
|
||||
|
||||
String url = mPendingMap.iterator().next();
|
||||
|
||||
for (int i = 0; i < mDownloadManager.getCount(); i++) {
|
||||
DownloadMission mission = mDownloadManager.getMission(i);
|
||||
if (url.equals(mission.url)) {
|
||||
showUndoDeleteSnackbar(mission);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void showUndoDeleteSnackbar(@NonNull DownloadMission mission) {
|
||||
final Snackbar snackbar = Snackbar.make(mView, mission.name, Snackbar.LENGTH_INDEFINITE);
|
||||
final Disposable disposable = Observable.timer(3, TimeUnit.SECONDS)
|
||||
.subscribeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(l -> snackbar.dismiss());
|
||||
|
||||
mDisposableList.add(disposable);
|
||||
|
||||
snackbar.setAction(R.string.undo, v -> {
|
||||
mPendingMap.remove(mission.url);
|
||||
publishSubject.onNext(mission);
|
||||
disposable.dispose();
|
||||
snackbar.dismiss();
|
||||
});
|
||||
|
||||
snackbar.addCallback(new BaseTransientBottomBar.BaseCallback<Snackbar>() {
|
||||
@Override
|
||||
public void onDismissed(Snackbar transientBottomBar, int event) {
|
||||
if (!disposable.isDisposed()) {
|
||||
Completable.fromAction(() -> deletePending(mission))
|
||||
.subscribeOn(Schedulers.io())
|
||||
.subscribe();
|
||||
}
|
||||
mPendingMap.remove(mission.url);
|
||||
snackbar.removeCallback(this);
|
||||
mDisposableList.remove(disposable);
|
||||
showUndoDeleteSnackbar();
|
||||
}
|
||||
});
|
||||
|
||||
snackbar.show();
|
||||
}
|
||||
|
||||
public void deletePending() {
|
||||
if (mPendingMap.size() < 1) return;
|
||||
|
||||
HashSet<Integer> idSet = new HashSet<>();
|
||||
for (int i = 0; i < mDownloadManager.getCount(); i++) {
|
||||
if (contains(mDownloadManager.getMission(i))) {
|
||||
idSet.add(i);
|
||||
}
|
||||
}
|
||||
|
||||
for (Integer id : idSet) {
|
||||
mDownloadManager.deleteMission(id);
|
||||
}
|
||||
|
||||
mPendingMap.clear();
|
||||
}
|
||||
|
||||
private void deletePending(@NonNull DownloadMission mission) {
|
||||
for (int i = 0; i < mDownloadManager.getCount(); i++) {
|
||||
if (mission.url.equals(mDownloadManager.getMission(i).url)) {
|
||||
mDownloadManager.deleteMission(i);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -15,12 +15,17 @@ import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.settings.SettingsActivity;
|
||||
import org.schabi.newpipe.util.ThemeHelper;
|
||||
|
||||
import io.reactivex.Completable;
|
||||
import io.reactivex.schedulers.Schedulers;
|
||||
import us.shandian.giga.service.DownloadManagerService;
|
||||
import us.shandian.giga.ui.fragment.AllMissionsFragment;
|
||||
import us.shandian.giga.ui.fragment.MissionsFragment;
|
||||
|
||||
public class DownloadActivity extends AppCompatActivity {
|
||||
|
||||
private static final String MISSIONS_FRAGMENT_TAG = "fragment_tag";
|
||||
private DeleteDownloadManager mDeleteDownloadManager;
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
// Service
|
||||
@@ -42,21 +47,35 @@ public class DownloadActivity extends AppCompatActivity {
|
||||
actionBar.setDisplayShowTitleEnabled(true);
|
||||
}
|
||||
|
||||
// Fragment
|
||||
getWindow().getDecorView().getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
|
||||
@Override
|
||||
public void onGlobalLayout() {
|
||||
updateFragments();
|
||||
getWindow().getDecorView().getViewTreeObserver().removeGlobalOnLayoutListener(this);
|
||||
}
|
||||
});
|
||||
mDeleteDownloadManager = new DeleteDownloadManager(this);
|
||||
mDeleteDownloadManager.restoreState(savedInstanceState);
|
||||
|
||||
MissionsFragment fragment = (MissionsFragment) getFragmentManager().findFragmentByTag(MISSIONS_FRAGMENT_TAG);
|
||||
if (fragment != null) {
|
||||
fragment.setDeleteManager(mDeleteDownloadManager);
|
||||
} else {
|
||||
getWindow().getDecorView().getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
|
||||
@Override
|
||||
public void onGlobalLayout() {
|
||||
updateFragments();
|
||||
getWindow().getDecorView().getViewTreeObserver().removeGlobalOnLayoutListener(this);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onSaveInstanceState(Bundle outState) {
|
||||
mDeleteDownloadManager.saveState(outState);
|
||||
super.onSaveInstanceState(outState);
|
||||
}
|
||||
|
||||
private void updateFragments() {
|
||||
|
||||
MissionsFragment fragment = new AllMissionsFragment();
|
||||
fragment.setDeleteManager(mDeleteDownloadManager);
|
||||
|
||||
getFragmentManager().beginTransaction()
|
||||
.replace(R.id.frame, fragment)
|
||||
.replace(R.id.frame, fragment, MISSIONS_FRAGMENT_TAG)
|
||||
.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_FADE)
|
||||
.commit();
|
||||
}
|
||||
@@ -80,6 +99,7 @@ public class DownloadActivity extends AppCompatActivity {
|
||||
case R.id.action_settings: {
|
||||
Intent intent = new Intent(this, SettingsActivity.class);
|
||||
startActivity(intent);
|
||||
deletePending();
|
||||
return true;
|
||||
}
|
||||
default:
|
||||
@@ -87,4 +107,15 @@ public class DownloadActivity extends AppCompatActivity {
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBackPressed() {
|
||||
super.onBackPressed();
|
||||
deletePending();
|
||||
}
|
||||
|
||||
private void deletePending() {
|
||||
Completable.fromAction(mDeleteDownloadManager::deletePending)
|
||||
.subscribeOn(Schedulers.io())
|
||||
.subscribe();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.getAudioStreams(), 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();
|
||||
}
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
package org.schabi.newpipe.download;
|
||||
|
||||
import android.app.AlertDialog;
|
||||
import android.content.DialogInterface;
|
||||
import android.os.Bundle;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.support.v7.app.AppCompatActivity;
|
||||
|
||||
import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.settings.NewPipeSettings;
|
||||
import org.schabi.newpipe.util.ServiceHelper;
|
||||
import org.schabi.newpipe.util.ThemeHelper;
|
||||
|
||||
public class ExtSDDownloadFailedActivity extends AppCompatActivity {
|
||||
@Override
|
||||
protected void onCreate(@Nullable Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
ThemeHelper.setTheme(this, ServiceHelper.getSelectedServiceId(this));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onStart() {
|
||||
super.onStart();
|
||||
new AlertDialog.Builder(this)
|
||||
.setTitle(R.string.download_to_sdcard_error_title)
|
||||
.setMessage(R.string.download_to_sdcard_error_message)
|
||||
.setPositiveButton(R.string.yes, (DialogInterface dialogInterface, int i) -> {
|
||||
NewPipeSettings.resetDownloadFolders(this);
|
||||
finish();
|
||||
})
|
||||
.setNegativeButton(R.string.cancel, (DialogInterface dialogInterface, int i) -> {
|
||||
dialogInterface.dismiss();
|
||||
finish();
|
||||
})
|
||||
.create()
|
||||
.show();
|
||||
}
|
||||
}
|
||||
@@ -51,9 +51,6 @@ public abstract class BaseStateFragment<I> extends BaseFragment implements ViewC
|
||||
protected Button errorButtonRetry;
|
||||
protected TextView errorTextView;
|
||||
|
||||
@State
|
||||
protected boolean useAsFrontPage = false;
|
||||
|
||||
@Override
|
||||
public void onViewCreated(View rootView, Bundle savedInstanceState) {
|
||||
super.onViewCreated(rootView, savedInstanceState);
|
||||
@@ -66,9 +63,6 @@ public abstract class BaseStateFragment<I> extends BaseFragment implements ViewC
|
||||
wasLoading.set(isLoading.get());
|
||||
}
|
||||
|
||||
public void useAsFrontPage(boolean value) {
|
||||
useAsFrontPage = value;
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Init
|
||||
@@ -93,12 +87,7 @@ public abstract class BaseStateFragment<I> extends BaseFragment implements ViewC
|
||||
RxView.clicks(errorButtonRetry)
|
||||
.debounce(300, TimeUnit.MILLISECONDS)
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(new Consumer<Object>() {
|
||||
@Override
|
||||
public void accept(Object o) throws Exception {
|
||||
onRetryButtonClicked();
|
||||
}
|
||||
});
|
||||
.subscribe(o -> onRetryButtonClicked());
|
||||
}
|
||||
|
||||
protected void onRetryButtonClicked() {
|
||||
@@ -239,7 +228,8 @@ 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));
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
|
||||
@@ -14,24 +14,16 @@ public class BlankFragment extends BaseFragment {
|
||||
@Nullable
|
||||
@Override
|
||||
public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, Bundle savedInstanceState) {
|
||||
if(activity != null && activity.getSupportActionBar() != null) {
|
||||
activity.getSupportActionBar()
|
||||
.setTitle("NewPipe");
|
||||
}
|
||||
setTitle("NewPipe");
|
||||
return inflater.inflate(R.layout.fragment_blank, container, false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setUserVisibleHint(boolean isVisibleToUser) {
|
||||
super.setUserVisibleHint(isVisibleToUser);
|
||||
if(isVisibleToUser) {
|
||||
if(activity != null && activity.getSupportActionBar() != null) {
|
||||
activity.getSupportActionBar()
|
||||
.setTitle("NewPipe");
|
||||
}
|
||||
// leave this inline. Will make it harder for copy cats.
|
||||
// If you are a Copy cat FUCK YOU.
|
||||
// I WILL FIND YOU, AND I WILL ...
|
||||
}
|
||||
setTitle("NewPipe");
|
||||
// leave this inline. Will make it harder for copy cats.
|
||||
// If you are a Copy cat FUCK YOU.
|
||||
// I WILL FIND YOU, AND I WILL ...
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
package org.schabi.newpipe.fragments;
|
||||
|
||||
import android.content.SharedPreferences;
|
||||
import android.os.Bundle;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.annotation.Nullable;
|
||||
@@ -10,48 +9,37 @@ import android.support.v4.app.FragmentManager;
|
||||
import android.support.v4.app.FragmentPagerAdapter;
|
||||
import android.support.v4.view.ViewPager;
|
||||
import android.support.v7.app.ActionBar;
|
||||
import android.support.v7.preference.PreferenceManager;
|
||||
import android.support.v7.app.AppCompatActivity;
|
||||
import android.util.Log;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuInflater;
|
||||
import android.view.MenuItem;
|
||||
import android.view.SubMenu;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
|
||||
import org.schabi.newpipe.BaseFragment;
|
||||
import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.extractor.NewPipe;
|
||||
import org.schabi.newpipe.extractor.ServiceList;
|
||||
import org.schabi.newpipe.extractor.StreamingService;
|
||||
import org.schabi.newpipe.extractor.kiosk.KioskList;
|
||||
import org.schabi.newpipe.fragments.list.channel.ChannelFragment;
|
||||
import org.schabi.newpipe.fragments.list.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.extractor.exceptions.ExtractionException;
|
||||
import org.schabi.newpipe.report.ErrorActivity;
|
||||
import org.schabi.newpipe.report.UserAction;
|
||||
import org.schabi.newpipe.util.KioskTranslator;
|
||||
import org.schabi.newpipe.settings.tabs.Tab;
|
||||
import org.schabi.newpipe.settings.tabs.TabsManager;
|
||||
import org.schabi.newpipe.util.NavigationHelper;
|
||||
import org.schabi.newpipe.util.ServiceHelper;
|
||||
import org.schabi.newpipe.util.ThemeHelper;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
public class MainFragment extends BaseFragment implements TabLayout.OnTabSelectedListener {
|
||||
|
||||
public int currentServiceId = -1;
|
||||
private ViewPager viewPager;
|
||||
private SelectedTabsPagerAdapter pagerAdapter;
|
||||
private TabLayout tabLayout;
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Constants
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
private List<Tab> tabsList = new ArrayList<>();
|
||||
private TabsManager tabsManager;
|
||||
|
||||
private static final int FALLBACK_SERVICE_ID = ServiceList.YouTube.getServiceId();
|
||||
private static final String FALLBACK_CHANNEL_URL = "https://www.youtube.com/channel/UC-9-kyTW8ZkZNDHQJ6FgpwQ";
|
||||
private static final String FALLBACK_CHANNEL_NAME = "Music";
|
||||
private static final String FALLBACK_KIOSK_ID = "Trending";
|
||||
private static final int KIOSK_MENU_OFFSET = 2000;
|
||||
private boolean hasTabsChanged = false;
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Fragment's LifeCycle
|
||||
@@ -61,11 +49,22 @@ public class MainFragment extends BaseFragment implements TabLayout.OnTabSelecte
|
||||
public void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
setHasOptionsMenu(true);
|
||||
|
||||
tabsManager = TabsManager.getManager(activity);
|
||||
tabsManager.setSavedTabsListener(() -> {
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "TabsManager.SavedTabsChangeListener: onTabsChanged called, isResumed = " + isResumed());
|
||||
}
|
||||
if (isResumed()) {
|
||||
updateTabs();
|
||||
} else {
|
||||
hasTabsChanged = true;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
|
||||
currentServiceId = ServiceHelper.getSelectedServiceId(activity);
|
||||
return inflater.inflate(R.layout.fragment_main, container, false);
|
||||
}
|
||||
|
||||
@@ -73,30 +72,34 @@ public class MainFragment extends BaseFragment implements TabLayout.OnTabSelecte
|
||||
protected void initViews(View rootView, Bundle savedInstanceState) {
|
||||
super.initViews(rootView, savedInstanceState);
|
||||
|
||||
TabLayout tabLayout = rootView.findViewById(R.id.main_tab_layout);
|
||||
tabLayout = rootView.findViewById(R.id.main_tab_layout);
|
||||
viewPager = rootView.findViewById(R.id.pager);
|
||||
|
||||
/* Nested fragment, use child fragment here to maintain backstack in view pager. */
|
||||
PagerAdapter adapter = new PagerAdapter(getChildFragmentManager());
|
||||
viewPager.setAdapter(adapter);
|
||||
viewPager.setOffscreenPageLimit(adapter.getCount());
|
||||
pagerAdapter = new SelectedTabsPagerAdapter(getChildFragmentManager());
|
||||
viewPager.setAdapter(pagerAdapter);
|
||||
|
||||
tabLayout.setupWithViewPager(viewPager);
|
||||
tabLayout.addOnTabSelectedListener(this);
|
||||
updateTabs();
|
||||
}
|
||||
|
||||
int channelIcon = ThemeHelper.resolveResourceIdFromAttr(activity, R.attr.ic_channel);
|
||||
int whatsHotIcon = ThemeHelper.resolveResourceIdFromAttr(activity, R.attr.ic_hot);
|
||||
int bookmarkIcon = ThemeHelper.resolveResourceIdFromAttr(activity, R.attr.ic_bookmark);
|
||||
@Override
|
||||
public void onResume() {
|
||||
super.onResume();
|
||||
|
||||
if (isSubscriptionsPageOnlySelected()) {
|
||||
tabLayout.getTabAt(0).setIcon(channelIcon);
|
||||
tabLayout.getTabAt(1).setIcon(bookmarkIcon);
|
||||
} else {
|
||||
tabLayout.getTabAt(0).setIcon(whatsHotIcon);
|
||||
tabLayout.getTabAt(1).setIcon(channelIcon);
|
||||
tabLayout.getTabAt(2).setIcon(bookmarkIcon);
|
||||
if (hasTabsChanged) {
|
||||
hasTabsChanged = false;
|
||||
updateTabs();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroy() {
|
||||
super.onDestroy();
|
||||
tabsManager.unsetSavedTabsListener();
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Menu
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
@@ -106,16 +109,6 @@ 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(Menu.NONE, Menu.NONE, 200, getString(R.string.kiosk));
|
||||
try {
|
||||
createKioskMenu(kioskMenu, inflater);
|
||||
} catch (Exception e) {
|
||||
ErrorActivity.reportError(activity, e,
|
||||
activity.getClass(),
|
||||
null,
|
||||
ErrorActivity.ErrorInfo.make(UserAction.UI_ERROR,
|
||||
"none", "", R.string.app_ui_crash));
|
||||
}
|
||||
|
||||
ActionBar supportActionBar = activity.getSupportActionBar();
|
||||
if (supportActionBar != null) {
|
||||
@@ -127,7 +120,14 @@ public class MainFragment extends BaseFragment implements TabLayout.OnTabSelecte
|
||||
public boolean onOptionsItemSelected(MenuItem item) {
|
||||
switch (item.getItemId()) {
|
||||
case R.id.action_search:
|
||||
NavigationHelper.openSearchFragment(getFragmentManager(), ServiceHelper.getSelectedServiceId(activity), "");
|
||||
try {
|
||||
NavigationHelper.openSearchFragment(
|
||||
getFragmentManager(),
|
||||
ServiceHelper.getSelectedServiceId(activity),
|
||||
"");
|
||||
} catch (Exception e) {
|
||||
ErrorActivity.reportUiError((AppCompatActivity) getActivity(), e);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return super.onOptionsItemSelected(item);
|
||||
@@ -137,9 +137,33 @@ public class MainFragment extends BaseFragment implements TabLayout.OnTabSelecte
|
||||
// Tabs
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
public void updateTabs() {
|
||||
tabsList.clear();
|
||||
tabsList.addAll(tabsManager.getTabs());
|
||||
pagerAdapter.notifyDataSetChanged();
|
||||
|
||||
viewPager.setOffscreenPageLimit(pagerAdapter.getCount());
|
||||
updateTabsIcon();
|
||||
updateCurrentTitle();
|
||||
}
|
||||
|
||||
private void updateTabsIcon() {
|
||||
for (int i = 0; i < tabsList.size(); i++) {
|
||||
final TabLayout.Tab tabToSet = tabLayout.getTabAt(i);
|
||||
if (tabToSet != null) {
|
||||
tabToSet.setIcon(tabsList.get(i).getTabIconRes(activity));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void updateCurrentTitle() {
|
||||
setTitle(tabsList.get(viewPager.getCurrentItem()).getTabName(requireContext()));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTabSelected(TabLayout.Tab tab) {
|
||||
viewPager.setCurrentItem(tab.getPosition());
|
||||
public void onTabSelected(TabLayout.Tab selectedTab) {
|
||||
if (DEBUG) Log.d(TAG, "onTabSelected() called with: selectedTab = [" + selectedTab + "]");
|
||||
updateCurrentTitle();
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -148,129 +172,58 @@ public class MainFragment extends BaseFragment implements TabLayout.OnTabSelecte
|
||||
|
||||
@Override
|
||||
public void onTabReselected(TabLayout.Tab tab) {
|
||||
if (DEBUG) Log.d(TAG, "onTabReselected() called with: tab = [" + tab + "]");
|
||||
updateCurrentTitle();
|
||||
}
|
||||
|
||||
private class PagerAdapter extends FragmentPagerAdapter {
|
||||
PagerAdapter(FragmentManager fm) {
|
||||
super(fm);
|
||||
private class SelectedTabsPagerAdapter extends FragmentPagerAdapter {
|
||||
private SelectedTabsPagerAdapter(FragmentManager fragmentManager) {
|
||||
super(fragmentManager);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Fragment getItem(int position) {
|
||||
switch (position) {
|
||||
case 0:
|
||||
return isSubscriptionsPageOnlySelected() ? new SubscriptionFragment() : getMainPageFragment();
|
||||
case 1:
|
||||
if(PreferenceManager.getDefaultSharedPreferences(getActivity())
|
||||
.getString(getString(R.string.main_page_content_key), getString(R.string.blank_page_key))
|
||||
.equals(getString(R.string.subscription_page_key))) {
|
||||
return new BookmarkFragment();
|
||||
} else {
|
||||
return new SubscriptionFragment();
|
||||
}
|
||||
case 2:
|
||||
return new BookmarkFragment();
|
||||
default:
|
||||
return new BlankFragment();
|
||||
final Tab tab = tabsList.get(position);
|
||||
|
||||
Throwable throwable = null;
|
||||
Fragment fragment = null;
|
||||
try {
|
||||
fragment = tab.getFragment();
|
||||
} catch (ExtractionException e) {
|
||||
throwable = e;
|
||||
}
|
||||
|
||||
if (throwable != null) {
|
||||
ErrorActivity.reportError(activity, throwable, activity.getClass(), null,
|
||||
ErrorActivity.ErrorInfo.make(UserAction.UI_ERROR, "none", "", R.string.app_ui_crash));
|
||||
return new BlankFragment();
|
||||
}
|
||||
|
||||
if (fragment instanceof BaseFragment) {
|
||||
((BaseFragment) fragment).useAsFrontPage(true);
|
||||
}
|
||||
|
||||
return fragment;
|
||||
}
|
||||
|
||||
@Override
|
||||
public CharSequence getPageTitle(int position) {
|
||||
//return getString(this.tabTitles[position]);
|
||||
return "";
|
||||
public int getItemPosition(Object object) {
|
||||
// Causes adapter to reload all Fragments when
|
||||
// notifyDataSetChanged is called
|
||||
return POSITION_NONE;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getCount() {
|
||||
return isSubscriptionsPageOnlySelected() ? 2 : 3;
|
||||
return tabsList.size();
|
||||
}
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Main page content
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
private boolean isSubscriptionsPageOnlySelected() {
|
||||
return PreferenceManager.getDefaultSharedPreferences(activity)
|
||||
.getString(getString(R.string.main_page_content_key), getString(R.string.blank_page_key))
|
||||
.equals(getString(R.string.subscription_page_key));
|
||||
}
|
||||
|
||||
private Fragment getMainPageFragment() {
|
||||
if (getActivity() == null) return new BlankFragment();
|
||||
|
||||
try {
|
||||
SharedPreferences preferences =
|
||||
PreferenceManager.getDefaultSharedPreferences(getActivity());
|
||||
final String setMainPage = preferences.getString(getString(R.string.main_page_content_key),
|
||||
getString(R.string.main_page_selectd_kiosk_id));
|
||||
if (setMainPage.equals(getString(R.string.blank_page_key))) {
|
||||
return new BlankFragment();
|
||||
} else if (setMainPage.equals(getString(R.string.kiosk_page_key))) {
|
||||
int serviceId = preferences.getInt(getString(R.string.main_page_selected_service),
|
||||
FALLBACK_SERVICE_ID);
|
||||
String kioskId = preferences.getString(getString(R.string.main_page_selectd_kiosk_id),
|
||||
FALLBACK_KIOSK_ID);
|
||||
KioskFragment fragment = KioskFragment.getInstance(serviceId, kioskId);
|
||||
fragment.useAsFrontPage(true);
|
||||
return fragment;
|
||||
} else if (setMainPage.equals(getString(R.string.feed_page_key))) {
|
||||
FeedFragment fragment = new FeedFragment();
|
||||
fragment.useAsFrontPage(true);
|
||||
return fragment;
|
||||
} else if (setMainPage.equals(getString(R.string.channel_page_key))) {
|
||||
int serviceId = preferences.getInt(getString(R.string.main_page_selected_service),
|
||||
FALLBACK_SERVICE_ID);
|
||||
String url = preferences.getString(getString(R.string.main_page_selected_channel_url),
|
||||
FALLBACK_CHANNEL_URL);
|
||||
String name = preferences.getString(getString(R.string.main_page_selected_channel_name),
|
||||
FALLBACK_CHANNEL_NAME);
|
||||
ChannelFragment fragment = ChannelFragment.getInstance(serviceId, url, name);
|
||||
fragment.useAsFrontPage(true);
|
||||
return fragment;
|
||||
} else {
|
||||
return new BlankFragment();
|
||||
}
|
||||
|
||||
} catch (Exception e) {
|
||||
ErrorActivity.reportError(activity, e,
|
||||
activity.getClass(),
|
||||
null,
|
||||
ErrorActivity.ErrorInfo.make(UserAction.UI_ERROR,
|
||||
"none", "", R.string.app_ui_crash));
|
||||
return new BlankFragment();
|
||||
}
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Select Kiosk
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
private void createKioskMenu(Menu menu, MenuInflater menuInflater)
|
||||
throws Exception {
|
||||
StreamingService service = NewPipe.getService(currentServiceId);
|
||||
KioskList kl = service.getKioskList();
|
||||
int i = 0;
|
||||
for (final String ks : kl.getAvailableKiosks()) {
|
||||
menu.add(0, KIOSK_MENU_OFFSET + i, Menu.NONE,
|
||||
KioskTranslator.getTranslatedKioskName(ks, getContext()))
|
||||
.setOnMenuItemClickListener(new MenuItem.OnMenuItemClickListener() {
|
||||
@Override
|
||||
public boolean onMenuItemClick(MenuItem menuItem) {
|
||||
try {
|
||||
NavigationHelper.openKioskFragment(getFragmentManager(), currentServiceId, ks);
|
||||
} catch (Exception e) {
|
||||
ErrorActivity.reportError(activity, e,
|
||||
activity.getClass(),
|
||||
null,
|
||||
ErrorActivity.ErrorInfo.make(UserAction.UI_ERROR,
|
||||
"none", "", R.string.app_ui_crash));
|
||||
}
|
||||
return true;
|
||||
}
|
||||
});
|
||||
i++;
|
||||
@Override
|
||||
public void destroyItem(ViewGroup container, int position, Object object) {
|
||||
getChildFragmentManager()
|
||||
.beginTransaction()
|
||||
.remove((Fragment) object)
|
||||
.commitNowAllowingStateLoss();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,10 +12,12 @@ 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;
|
||||
import android.support.v7.app.AlertDialog;
|
||||
import android.support.v7.app.AppCompatActivity;
|
||||
import android.text.Html;
|
||||
import android.text.Spanned;
|
||||
import android.text.TextUtils;
|
||||
@@ -53,24 +55,28 @@ import org.schabi.newpipe.extractor.InfoItem;
|
||||
import org.schabi.newpipe.extractor.NewPipe;
|
||||
import org.schabi.newpipe.extractor.exceptions.ContentNotAvailableException;
|
||||
import org.schabi.newpipe.extractor.exceptions.ParsingException;
|
||||
import org.schabi.newpipe.extractor.services.youtube.YoutubeStreamExtractor;
|
||||
import org.schabi.newpipe.extractor.services.youtube.extractors.YoutubeStreamExtractor;
|
||||
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.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.local.history.HistoryRecordManager;
|
||||
import org.schabi.newpipe.report.ErrorActivity;
|
||||
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;
|
||||
@@ -84,9 +90,10 @@ 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;
|
||||
@@ -108,8 +115,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;
|
||||
@@ -121,18 +126,16 @@ 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();
|
||||
@NonNull private CompositeDisposable disposables = new CompositeDisposable();
|
||||
|
||||
private int selectedVideoStream = -1;
|
||||
private List<VideoStream> sortedVideoStreams;
|
||||
private int selectedVideoStreamIndex = -1;
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Views
|
||||
@@ -205,7 +208,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);
|
||||
}
|
||||
|
||||
@@ -356,32 +359,23 @@ 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:
|
||||
if (TextUtils.isEmpty(currentInfo.getUploaderUrl())) {
|
||||
Log.w(TAG, "Can't open channel because we got no channel URL");
|
||||
} else {
|
||||
NavigationHelper.openChannelFragment(
|
||||
getFragmentManager(),
|
||||
currentInfo.getServiceId(),
|
||||
currentInfo.getUploaderUrl(),
|
||||
currentInfo.getUploaderName());
|
||||
try {
|
||||
NavigationHelper.openChannelFragment(
|
||||
getFragmentManager(),
|
||||
currentInfo.getServiceId(),
|
||||
currentInfo.getUploaderUrl(),
|
||||
currentInfo.getUploaderName());
|
||||
} catch (Exception e) {
|
||||
ErrorActivity.reportUiError((AppCompatActivity) getActivity(), e);
|
||||
}
|
||||
}
|
||||
break;
|
||||
case R.id.detail_thumbnail_root_layout:
|
||||
@@ -412,6 +406,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;
|
||||
@@ -533,6 +530,7 @@ public class VideoDetailFragment
|
||||
detailControlsPopup.setOnClickListener(this);
|
||||
detailControlsAddToPlaylist.setOnClickListener(this);
|
||||
detailControlsDownload.setOnClickListener(this);
|
||||
detailControlsDownload.setOnLongClickListener(this);
|
||||
relatedStreamExpandButton.setOnClickListener(this);
|
||||
|
||||
detailControlsBackground.setLongClickable(true);
|
||||
@@ -549,7 +547,9 @@ 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),
|
||||
context.getResources().getString(R.string.share)
|
||||
};
|
||||
|
||||
final DialogInterface.OnClickListener actions = (DialogInterface dialogInterface, int i) -> {
|
||||
@@ -560,6 +560,15 @@ 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;
|
||||
case 3:
|
||||
shareUrl(item.getName(), item.getUrl());
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
@@ -681,15 +690,15 @@ public class VideoDetailFragment
|
||||
int id = item.getItemId();
|
||||
switch (id) {
|
||||
case R.id.menu_item_share: {
|
||||
if(currentInfo != null) {
|
||||
shareUrl(currentInfo.getName(), 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 +731,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 +825,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();
|
||||
|
||||
@@ -877,10 +884,7 @@ public class VideoDetailFragment
|
||||
if (!useExternalAudioPlayer && android.os.Build.VERSION.SDK_INT >= 16) {
|
||||
openNormalBackgroundPlayer(append);
|
||||
} else {
|
||||
NavigationHelper.playOnExternalPlayer(activity,
|
||||
currentInfo.getName(),
|
||||
currentInfo.getUploaderName(),
|
||||
audioStream);
|
||||
startOnExternalPlayer(activity, currentInfo, audioStream);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -907,10 +911,7 @@ public class VideoDetailFragment
|
||||
|
||||
if (PreferenceManager.getDefaultSharedPreferences(activity)
|
||||
.getBoolean(this.getString(R.string.use_external_video_player_key), false)) {
|
||||
NavigationHelper.playOnExternalPlayer(activity,
|
||||
currentInfo.getName(),
|
||||
currentInfo.getUploaderName(),
|
||||
selectedVideoStream);
|
||||
startOnExternalPlayer(activity, currentInfo, selectedVideoStream);
|
||||
} else {
|
||||
openNormalPlayer(selectedVideoStream);
|
||||
}
|
||||
@@ -954,8 +955,23 @@ public class VideoDetailFragment
|
||||
this.autoPlayEnabled = autoplay;
|
||||
}
|
||||
|
||||
private void startOnExternalPlayer(@NonNull final Context context,
|
||||
@NonNull final StreamInfo info,
|
||||
@NonNull final Stream selectedStream) {
|
||||
NavigationHelper.playOnExternalPlayer(context, currentInfo.getName(),
|
||||
currentInfo.getUploaderName(), selectedStream);
|
||||
|
||||
final HistoryRecordManager recordManager = new HistoryRecordManager(requireContext());
|
||||
disposables.add(recordManager.onViewed(info).onErrorComplete()
|
||||
.subscribe(
|
||||
ignored -> {/* successful */},
|
||||
error -> Log.e(TAG, "Register view failure: ", error)
|
||||
));
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private VideoStream getSelectedVideoStream() {
|
||||
return sortedStreamVideosList.get(selectedVideoStream);
|
||||
return sortedVideoStreams != null ? sortedVideoStreams.get(selectedVideoStreamIndex) : null;
|
||||
}
|
||||
|
||||
private void prepareDescription(final String descriptionHtml) {
|
||||
@@ -1112,7 +1128,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);
|
||||
@@ -1192,7 +1208,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(),
|
||||
@@ -1209,10 +1227,10 @@ public class VideoDetailFragment
|
||||
spinnerToolbar.setVisibility(View.GONE);
|
||||
break;
|
||||
default:
|
||||
if(info.getAudioStreams().isEmpty()) detailControlsBackground.setVisibility(View.GONE);
|
||||
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);
|
||||
@@ -1226,6 +1244,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
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
@@ -6,6 +6,7 @@ import android.content.DialogInterface;
|
||||
import android.os.Bundle;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.v7.app.ActionBar;
|
||||
import android.support.v7.app.AppCompatActivity;
|
||||
import android.support.v7.widget.LinearLayoutManager;
|
||||
import android.support.v7.widget.RecyclerView;
|
||||
import android.util.Log;
|
||||
@@ -20,10 +21,11 @@ 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.report.ErrorActivity;
|
||||
import org.schabi.newpipe.util.NavigationHelper;
|
||||
import org.schabi.newpipe.util.OnClickGesture;
|
||||
import org.schabi.newpipe.util.StateSaver;
|
||||
@@ -140,9 +142,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
|
||||
@@ -154,18 +154,30 @@ public abstract class BaseListFragment<I, N> extends BaseStateFragment<I> implem
|
||||
infoListAdapter.setOnChannelSelectedListener(new OnClickGesture<ChannelInfoItem>() {
|
||||
@Override
|
||||
public void selected(ChannelInfoItem selectedItem) {
|
||||
onItemSelected(selectedItem);
|
||||
NavigationHelper.openChannelFragment(useAsFrontPage ? getParentFragment().getFragmentManager() : getFragmentManager(),
|
||||
selectedItem.getServiceId(), selectedItem.getUrl(), selectedItem.getName());
|
||||
try {
|
||||
onItemSelected(selectedItem);
|
||||
NavigationHelper.openChannelFragment(getFM(),
|
||||
selectedItem.getServiceId(),
|
||||
selectedItem.getUrl(),
|
||||
selectedItem.getName());
|
||||
} catch (Exception e) {
|
||||
ErrorActivity.reportUiError((AppCompatActivity) getActivity(), e);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
infoListAdapter.setOnPlaylistSelectedListener(new OnClickGesture<PlaylistInfoItem>() {
|
||||
@Override
|
||||
public void selected(PlaylistInfoItem selectedItem) {
|
||||
onItemSelected(selectedItem);
|
||||
NavigationHelper.openPlaylistFragment(useAsFrontPage ? getParentFragment().getFragmentManager() : getFragmentManager(),
|
||||
selectedItem.getServiceId(), selectedItem.getUrl(), selectedItem.getName());
|
||||
try {
|
||||
onItemSelected(selectedItem);
|
||||
NavigationHelper.openPlaylistFragment(getFM(),
|
||||
selectedItem.getServiceId(),
|
||||
selectedItem.getUrl(),
|
||||
selectedItem.getName());
|
||||
} catch (Exception e) {
|
||||
ErrorActivity.reportUiError((AppCompatActivity) getActivity(), e);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -178,6 +190,12 @@ public abstract class BaseListFragment<I, N> extends BaseStateFragment<I> implem
|
||||
});
|
||||
}
|
||||
|
||||
private void onStreamSelected(StreamInfoItem selectedItem) {
|
||||
onItemSelected(selectedItem);
|
||||
NavigationHelper.openVideoDetailFragment(getFM(),
|
||||
selectedItem.getServiceId(), selectedItem.getUrl(), selectedItem.getName());
|
||||
}
|
||||
|
||||
protected void onScrollToBottom() {
|
||||
if (hasMoreItems() && !isLoading.get()) {
|
||||
loadMoreItems();
|
||||
@@ -192,7 +210,8 @@ public abstract class BaseListFragment<I, N> extends BaseStateFragment<I> implem
|
||||
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.append_playlist)
|
||||
context.getResources().getString(R.string.append_playlist),
|
||||
context.getResources().getString(R.string.share)
|
||||
};
|
||||
|
||||
final DialogInterface.OnClickListener actions = (dialogInterface, i) -> {
|
||||
@@ -209,6 +228,9 @@ public abstract class BaseListFragment<I, N> extends BaseStateFragment<I> implem
|
||||
.show(getFragmentManager(), TAG);
|
||||
}
|
||||
break;
|
||||
case 3:
|
||||
shareUrl(item.getName(), item.getUrl());
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
@@ -216,6 +238,7 @@ public abstract class BaseListFragment<I, N> extends BaseStateFragment<I> implem
|
||||
|
||||
new InfoItemDialog(getActivity(), item, commands, actions).show();
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Menu
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
@@ -8,6 +8,9 @@ import android.view.View;
|
||||
|
||||
import org.schabi.newpipe.extractor.ListExtractor;
|
||||
import org.schabi.newpipe.extractor.ListInfo;
|
||||
import org.schabi.newpipe.extractor.NewPipe;
|
||||
import org.schabi.newpipe.extractor.linkhandler.ListLinkHandler;
|
||||
import org.schabi.newpipe.extractor.linkhandler.LinkHandler;
|
||||
import org.schabi.newpipe.util.Constants;
|
||||
|
||||
import java.util.Queue;
|
||||
@@ -166,7 +169,6 @@ public abstract class BaseListInfoFragment<I extends ListInfo>
|
||||
public void handleResult(@NonNull I result) {
|
||||
super.handleResult(result);
|
||||
|
||||
url = result.getUrl();
|
||||
name = result.getName();
|
||||
setTitle(name);
|
||||
|
||||
|
||||
@@ -32,16 +32,15 @@ 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.Stream;
|
||||
import org.schabi.newpipe.extractor.stream.StreamInfo;
|
||||
import org.schabi.newpipe.extractor.stream.StreamInfoItem;
|
||||
import org.schabi.newpipe.fragments.list.BaseListInfoFragment;
|
||||
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.local.subscription.SubscriptionService;
|
||||
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.subscription.SubscriptionService;
|
||||
import org.schabi.newpipe.util.AnimationUtils;
|
||||
import org.schabi.newpipe.util.ExtractorHelper;
|
||||
import org.schabi.newpipe.util.ImageDisplayConstants;
|
||||
@@ -49,6 +48,7 @@ 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;
|
||||
|
||||
@@ -90,6 +90,8 @@ public class ChannelFragment extends BaseListInfoFragment<ChannelInfo> {
|
||||
|
||||
private MenuItem menuRssButton;
|
||||
|
||||
private boolean mIsVisibleToUser = false;
|
||||
|
||||
public static ChannelFragment getInstance(int serviceId, String url, String name) {
|
||||
ChannelFragment instance = new ChannelFragment();
|
||||
instance.setInitialData(serviceId, url, name);
|
||||
@@ -103,6 +105,7 @@ public class ChannelFragment extends BaseListInfoFragment<ChannelInfo> {
|
||||
@Override
|
||||
public void setUserVisibleHint(boolean isVisibleToUser) {
|
||||
super.setUserVisibleHint(isVisibleToUser);
|
||||
mIsVisibleToUser = isVisibleToUser;
|
||||
if(activity != null
|
||||
&& useAsFrontPage
|
||||
&& isVisibleToUser) {
|
||||
@@ -161,31 +164,39 @@ 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),
|
||||
context.getResources().getString(R.string.share)
|
||||
};
|
||||
|
||||
final DialogInterface.OnClickListener actions = new DialogInterface.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(DialogInterface dialogInterface, int i) {
|
||||
final int index = Math.max(infoListAdapter.getItemsList().indexOf(item), 0);
|
||||
switch (i) {
|
||||
case 0:
|
||||
NavigationHelper.enqueueOnBackgroundPlayer(context, new SinglePlayQueue(item));
|
||||
break;
|
||||
case 1:
|
||||
NavigationHelper.enqueueOnPopupPlayer(activity, new SinglePlayQueue(item));
|
||||
break;
|
||||
case 2:
|
||||
NavigationHelper.playOnMainPlayer(context, getPlayQueue(index));
|
||||
break;
|
||||
case 3:
|
||||
NavigationHelper.playOnBackgroundPlayer(context, getPlayQueue(index));
|
||||
break;
|
||||
case 4:
|
||||
NavigationHelper.playOnPopupPlayer(activity, getPlayQueue(index));
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
final DialogInterface.OnClickListener actions = (DialogInterface dialogInterface, int i) -> {
|
||||
final int index = Math.max(infoListAdapter.getItemsList().indexOf(item), 0);
|
||||
switch (i) {
|
||||
case 0:
|
||||
NavigationHelper.enqueueOnBackgroundPlayer(context, new SinglePlayQueue(item));
|
||||
break;
|
||||
case 1:
|
||||
NavigationHelper.enqueueOnPopupPlayer(activity, new SinglePlayQueue(item));
|
||||
break;
|
||||
case 2:
|
||||
NavigationHelper.playOnMainPlayer(context, getPlayQueue(index));
|
||||
break;
|
||||
case 3:
|
||||
NavigationHelper.playOnBackgroundPlayer(context, getPlayQueue(index));
|
||||
break;
|
||||
case 4:
|
||||
NavigationHelper.playOnPopupPlayer(activity, getPlayQueue(index));
|
||||
break;
|
||||
case 5:
|
||||
if (getFragmentManager() != null) {
|
||||
PlaylistAppendDialog.fromStreamInfoItems(Collections.singletonList(item))
|
||||
.show(getFragmentManager(), TAG);
|
||||
}
|
||||
break;
|
||||
case 6:
|
||||
shareUrl(item.getName(), item.getUrl());
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -243,12 +254,12 @@ public class ChannelFragment extends BaseListInfoFragment<ChannelInfo> {
|
||||
private static final int BUTTON_DEBOUNCE_INTERVAL = 100;
|
||||
|
||||
private void monitorSubscription(final ChannelInfo info) {
|
||||
final Consumer<Throwable> onError = new Consumer<Throwable>() {
|
||||
@Override
|
||||
public void accept(Throwable throwable) throws Exception {
|
||||
final Consumer<Throwable> onError = (Throwable throwable) -> {
|
||||
animateView(headerSubscribeButton, false, 100);
|
||||
showSnackBarError(throwable, UserAction.SUBSCRIPTION, NewPipe.getNameOfService(currentInfo.getServiceId()), "Get subscription status", 0);
|
||||
}
|
||||
showSnackBarError(throwable, UserAction.SUBSCRIPTION,
|
||||
NewPipe.getNameOfService(currentInfo.getServiceId()),
|
||||
"Get subscription status",
|
||||
0);
|
||||
};
|
||||
|
||||
final Observable<List<SubscriptionEntity>> observable = subscriptionService.subscriptionTable()
|
||||
@@ -264,50 +275,38 @@ public class ChannelFragment extends BaseListInfoFragment<ChannelInfo> {
|
||||
// so only update the UI for the latest emission ("sync" the subscribe button's state)
|
||||
.debounce(100, TimeUnit.MILLISECONDS)
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(new Consumer<List<SubscriptionEntity>>() {
|
||||
@Override
|
||||
public void accept(List<SubscriptionEntity> subscriptionEntities) throws Exception {
|
||||
updateSubscribeButton(!subscriptionEntities.isEmpty());
|
||||
}
|
||||
}, onError));
|
||||
.subscribe((List<SubscriptionEntity> subscriptionEntities) ->
|
||||
updateSubscribeButton(!subscriptionEntities.isEmpty())
|
||||
, onError));
|
||||
|
||||
}
|
||||
|
||||
private Function<Object, Object> mapOnSubscribe(final SubscriptionEntity subscription) {
|
||||
return new Function<Object, Object>() {
|
||||
@Override
|
||||
public Object apply(@NonNull Object o) throws Exception {
|
||||
subscriptionService.subscriptionTable().insert(subscription);
|
||||
return o;
|
||||
}
|
||||
return (@NonNull Object o) -> {
|
||||
subscriptionService.subscriptionTable().insert(subscription);
|
||||
return o;
|
||||
};
|
||||
}
|
||||
|
||||
private Function<Object, Object> mapOnUnsubscribe(final SubscriptionEntity subscription) {
|
||||
return new Function<Object, Object>() {
|
||||
@Override
|
||||
public Object apply(@NonNull Object o) throws Exception {
|
||||
subscriptionService.subscriptionTable().delete(subscription);
|
||||
return o;
|
||||
}
|
||||
return (@NonNull Object o) -> {
|
||||
subscriptionService.subscriptionTable().delete(subscription);
|
||||
return o;
|
||||
};
|
||||
}
|
||||
|
||||
private void updateSubscription(final ChannelInfo info) {
|
||||
if (DEBUG) Log.d(TAG, "updateSubscription() called with: info = [" + info + "]");
|
||||
final Action onComplete = new Action() {
|
||||
@Override
|
||||
public void run() throws Exception {
|
||||
final Action onComplete = () -> {
|
||||
if (DEBUG) Log.d(TAG, "Updated subscription: " + info.getUrl());
|
||||
}
|
||||
};
|
||||
|
||||
final Consumer<Throwable> onError = new Consumer<Throwable>() {
|
||||
@Override
|
||||
public void accept(@NonNull Throwable throwable) throws Exception {
|
||||
onUnrecoverableError(throwable, UserAction.SUBSCRIPTION, NewPipe.getNameOfService(info.getServiceId()), "Updating Subscription for " + info.getUrl(), R.string.subscription_update_failed);
|
||||
}
|
||||
};
|
||||
final Consumer<Throwable> onError = (@NonNull Throwable throwable) ->
|
||||
onUnrecoverableError(throwable,
|
||||
UserAction.SUBSCRIPTION,
|
||||
NewPipe.getNameOfService(info.getServiceId()),
|
||||
"Updating Subscription for " + info.getUrl(),
|
||||
R.string.subscription_update_failed);
|
||||
|
||||
disposables.add(subscriptionService.updateChannelInfo(info)
|
||||
.subscribeOn(Schedulers.io())
|
||||
@@ -316,19 +315,16 @@ public class ChannelFragment extends BaseListInfoFragment<ChannelInfo> {
|
||||
}
|
||||
|
||||
private Disposable monitorSubscribeButton(final Button subscribeButton, final Function<Object, Object> action) {
|
||||
final Consumer<Object> onNext = new Consumer<Object>() {
|
||||
@Override
|
||||
public void accept(@NonNull Object o) throws Exception {
|
||||
final Consumer<Object> onNext = (@NonNull Object o) -> {
|
||||
if (DEBUG) Log.d(TAG, "Changed subscription status to this channel!");
|
||||
}
|
||||
};
|
||||
|
||||
final Consumer<Throwable> onError = new Consumer<Throwable>() {
|
||||
@Override
|
||||
public void accept(@NonNull Throwable throwable) throws Exception {
|
||||
onUnrecoverableError(throwable, UserAction.SUBSCRIPTION, NewPipe.getNameOfService(currentInfo.getServiceId()), "Subscription Change", R.string.subscription_change_failed);
|
||||
}
|
||||
};
|
||||
final Consumer<Throwable> onError = (@NonNull Throwable throwable) ->
|
||||
onUnrecoverableError(throwable,
|
||||
UserAction.SUBSCRIPTION,
|
||||
NewPipe.getNameOfService(currentInfo.getServiceId()),
|
||||
"Subscription Change",
|
||||
R.string.subscription_change_failed);
|
||||
|
||||
/* Emit clicks from main thread unto io thread */
|
||||
return RxView.clicks(subscribeButton)
|
||||
@@ -340,25 +336,25 @@ public class ChannelFragment extends BaseListInfoFragment<ChannelInfo> {
|
||||
}
|
||||
|
||||
private Consumer<List<SubscriptionEntity>> getSubscribeUpdateMonitor(final ChannelInfo info) {
|
||||
return new Consumer<List<SubscriptionEntity>>() {
|
||||
@Override
|
||||
public void accept(List<SubscriptionEntity> subscriptionEntities) throws Exception {
|
||||
if (DEBUG)
|
||||
Log.d(TAG, "subscriptionService.subscriptionTable.doOnNext() called with: subscriptionEntities = [" + subscriptionEntities + "]");
|
||||
if (subscribeButtonMonitor != null) subscribeButtonMonitor.dispose();
|
||||
return (List<SubscriptionEntity> subscriptionEntities) -> {
|
||||
if (DEBUG)
|
||||
Log.d(TAG, "subscriptionService.subscriptionTable.doOnNext() called with: subscriptionEntities = [" + subscriptionEntities + "]");
|
||||
if (subscribeButtonMonitor != null) subscribeButtonMonitor.dispose();
|
||||
|
||||
if (subscriptionEntities.isEmpty()) {
|
||||
if (DEBUG) Log.d(TAG, "No subscription to this channel!");
|
||||
SubscriptionEntity channel = new SubscriptionEntity();
|
||||
channel.setServiceId(info.getServiceId());
|
||||
channel.setUrl(info.getUrl());
|
||||
channel.setData(info.getName(), info.getAvatarUrl(), info.getDescription(), info.getSubscriberCount());
|
||||
subscribeButtonMonitor = monitorSubscribeButton(headerSubscribeButton, mapOnSubscribe(channel));
|
||||
} else {
|
||||
if (DEBUG) Log.d(TAG, "Found subscription to this channel!");
|
||||
final SubscriptionEntity subscription = subscriptionEntities.get(0);
|
||||
subscribeButtonMonitor = monitorSubscribeButton(headerSubscribeButton, mapOnUnsubscribe(subscription));
|
||||
}
|
||||
if (subscriptionEntities.isEmpty()) {
|
||||
if (DEBUG) Log.d(TAG, "No subscription to this channel!");
|
||||
SubscriptionEntity channel = new SubscriptionEntity();
|
||||
channel.setServiceId(info.getServiceId());
|
||||
channel.setUrl(info.getUrl());
|
||||
channel.setData(info.getName(),
|
||||
info.getAvatarUrl(),
|
||||
info.getDescription(),
|
||||
info.getSubscriberCount());
|
||||
subscribeButtonMonitor = monitorSubscribeButton(headerSubscribeButton, mapOnSubscribe(channel));
|
||||
} else {
|
||||
if (DEBUG) Log.d(TAG, "Found subscription to this channel!");
|
||||
final SubscriptionEntity subscription = subscriptionEntities.get(0);
|
||||
subscribeButtonMonitor = monitorSubscribeButton(headerSubscribeButton, mapOnUnsubscribe(subscription));
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -425,10 +421,12 @@ public class ChannelFragment extends BaseListInfoFragment<ChannelInfo> {
|
||||
imageLoader.displayImage(result.getAvatarUrl(), headerAvatarView,
|
||||
ImageDisplayConstants.DISPLAY_AVATAR_OPTIONS);
|
||||
|
||||
if (result.getSubscriberCount() != -1) {
|
||||
headerSubscribersTextView.setVisibility(View.VISIBLE);
|
||||
if (result.getSubscriberCount() >= 0) {
|
||||
headerSubscribersTextView.setText(Localization.localizeSubscribersCount(activity, result.getSubscriberCount()));
|
||||
headerSubscribersTextView.setVisibility(View.VISIBLE);
|
||||
} else headerSubscribersTextView.setVisibility(View.GONE);
|
||||
} else {
|
||||
headerSubscribersTextView.setText(R.string.subscribers_count_not_available);
|
||||
}
|
||||
|
||||
if (menuRssButton != null) menuRssButton.setVisible(!TextUtils.isEmpty(result.getFeedUrl()));
|
||||
|
||||
@@ -476,8 +474,11 @@ public class ChannelFragment extends BaseListInfoFragment<ChannelInfo> {
|
||||
super.handleNextItems(result);
|
||||
|
||||
if (!result.getErrors().isEmpty()) {
|
||||
showSnackBarError(result.getErrors(), UserAction.REQUESTED_CHANNEL, NewPipe.getNameOfService(serviceId),
|
||||
"Get next page of: " + url, R.string.general_error);
|
||||
showSnackBarError(result.getErrors(),
|
||||
UserAction.REQUESTED_CHANNEL,
|
||||
NewPipe.getNameOfService(serviceId),
|
||||
"Get next page of: " + url,
|
||||
R.string.general_error);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -490,7 +491,11 @@ public class ChannelFragment extends BaseListInfoFragment<ChannelInfo> {
|
||||
if (super.onError(exception)) return true;
|
||||
|
||||
int errorId = exception instanceof ExtractionException ? R.string.parsing_error : R.string.general_error;
|
||||
onUnrecoverableError(exception, UserAction.REQUESTED_CHANNEL, NewPipe.getNameOfService(serviceId), url, errorId);
|
||||
onUnrecoverableError(exception,
|
||||
UserAction.REQUESTED_CHANNEL,
|
||||
NewPipe.getNameOfService(serviceId),
|
||||
url,
|
||||
errorId);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -501,6 +506,6 @@ public class ChannelFragment extends BaseListInfoFragment<ChannelInfo> {
|
||||
@Override
|
||||
public void setTitle(String title) {
|
||||
super.setTitle(title);
|
||||
headerTitleView.setText(title);
|
||||
if (!useAsFrontPage) headerTitleView.setText(title);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,22 +11,20 @@ import android.view.Menu;
|
||||
import android.view.MenuInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.TextView;
|
||||
|
||||
import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.extractor.ListExtractor;
|
||||
import org.schabi.newpipe.extractor.NewPipe;
|
||||
import org.schabi.newpipe.extractor.StreamingService;
|
||||
import org.schabi.newpipe.extractor.UrlIdHandler;
|
||||
import org.schabi.newpipe.extractor.exceptions.ExtractionException;
|
||||
import org.schabi.newpipe.extractor.kiosk.KioskInfo;
|
||||
import org.schabi.newpipe.extractor.stream.StreamInfoItem;
|
||||
import org.schabi.newpipe.extractor.linkhandler.ListLinkHandlerFactory;
|
||||
import org.schabi.newpipe.extractor.linkhandler.ListLinkHandler;
|
||||
import org.schabi.newpipe.extractor.linkhandler.LinkHandlerFactory;
|
||||
import org.schabi.newpipe.fragments.list.BaseListInfoFragment;
|
||||
import org.schabi.newpipe.info_list.InfoItemBuilder;
|
||||
import org.schabi.newpipe.report.UserAction;
|
||||
import org.schabi.newpipe.util.ExtractorHelper;
|
||||
import org.schabi.newpipe.util.KioskTranslator;
|
||||
import org.schabi.newpipe.util.NavigationHelper;
|
||||
|
||||
import icepick.State;
|
||||
import io.reactivex.Single;
|
||||
@@ -59,6 +57,7 @@ public class KioskFragment extends BaseListInfoFragment<KioskInfo> {
|
||||
protected String kioskId = "";
|
||||
protected String kioskTranslatedName;
|
||||
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Views
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
@@ -74,11 +73,10 @@ public class KioskFragment extends BaseListInfoFragment<KioskInfo> {
|
||||
throws ExtractionException {
|
||||
KioskFragment instance = new KioskFragment();
|
||||
StreamingService service = NewPipe.getService(serviceId);
|
||||
UrlIdHandler kioskTypeUrlIdHandler = service.getKioskList()
|
||||
.getUrlIdHandlerByType(kioskId);
|
||||
ListLinkHandlerFactory kioskLinkHandlerFactory = service.getKioskList()
|
||||
.getListLinkHandlerFactoryByType(kioskId);
|
||||
instance.setInitialData(serviceId,
|
||||
kioskTypeUrlIdHandler.getUrl(kioskId),
|
||||
kioskId);
|
||||
kioskLinkHandlerFactory.fromId(kioskId).getUrl(), kioskId);
|
||||
instance.kioskId = kioskId;
|
||||
return instance;
|
||||
}
|
||||
@@ -137,7 +135,10 @@ public class KioskFragment extends BaseListInfoFragment<KioskInfo> {
|
||||
.getDefaultSharedPreferences(activity)
|
||||
.getString(getString(R.string.content_country_key),
|
||||
getString(R.string.default_country_value));
|
||||
return ExtractorHelper.getKioskInfo(serviceId, url, contentCountry, forceReload);
|
||||
return ExtractorHelper.getKioskInfo(serviceId,
|
||||
url,
|
||||
contentCountry,
|
||||
forceReload);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -146,7 +147,10 @@ public class KioskFragment extends BaseListInfoFragment<KioskInfo> {
|
||||
.getDefaultSharedPreferences(activity)
|
||||
.getString(getString(R.string.content_country_key),
|
||||
getString(R.string.default_country_value));
|
||||
return ExtractorHelper.getMoreKioskItems(serviceId, url, currentNextPageUrl, contentCountry);
|
||||
return ExtractorHelper.getMoreKioskItems(serviceId,
|
||||
url,
|
||||
currentNextPageUrl,
|
||||
contentCountry);
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
@@ -164,7 +168,9 @@ public class KioskFragment extends BaseListInfoFragment<KioskInfo> {
|
||||
super.handleResult(result);
|
||||
|
||||
name = kioskTranslatedName;
|
||||
setTitle(kioskTranslatedName);
|
||||
if(!useAsFrontPage) {
|
||||
setTitle(kioskTranslatedName);
|
||||
}
|
||||
|
||||
if (!result.getErrors().isEmpty()) {
|
||||
showSnackBarError(result.getErrors(),
|
||||
|
||||
@@ -6,6 +6,7 @@ import android.content.DialogInterface;
|
||||
import android.os.Bundle;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.support.v7.app.AppCompatActivity;
|
||||
import android.text.TextUtils;
|
||||
import android.util.Log;
|
||||
import android.view.LayoutInflater;
|
||||
@@ -19,6 +20,7 @@ import android.widget.TextView;
|
||||
|
||||
import org.reactivestreams.Subscriber;
|
||||
import org.reactivestreams.Subscription;
|
||||
import org.schabi.newpipe.App;
|
||||
import org.schabi.newpipe.NewPipeDatabase;
|
||||
import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.database.playlist.model.PlaylistRemoteEntity;
|
||||
@@ -27,14 +29,15 @@ 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.StreamInfo;
|
||||
import org.schabi.newpipe.extractor.stream.StreamInfoItem;
|
||||
import org.schabi.newpipe.extractor.linkhandler.ListLinkHandler;
|
||||
import org.schabi.newpipe.fragments.list.BaseListInfoFragment;
|
||||
import org.schabi.newpipe.fragments.local.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.local.playlist.RemotePlaylistManager;
|
||||
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.ErrorActivity;
|
||||
import org.schabi.newpipe.report.UserAction;
|
||||
import org.schabi.newpipe.util.ExtractorHelper;
|
||||
import org.schabi.newpipe.util.ImageDisplayConstants;
|
||||
@@ -45,6 +48,7 @@ import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
|
||||
import io.reactivex.Flowable;
|
||||
import io.reactivex.Single;
|
||||
import io.reactivex.android.schedulers.AndroidSchedulers;
|
||||
import io.reactivex.disposables.CompositeDisposable;
|
||||
@@ -94,7 +98,8 @@ public class PlaylistFragment extends BaseListInfoFragment<PlaylistInfo> {
|
||||
super.onCreate(savedInstanceState);
|
||||
disposables = new CompositeDisposable();
|
||||
isBookmarkButtonReady = new AtomicBoolean(false);
|
||||
remotePlaylistManager = new RemotePlaylistManager(NewPipeDatabase.getInstance(getContext()));
|
||||
remotePlaylistManager = new RemotePlaylistManager(NewPipeDatabase.getInstance(
|
||||
requireContext()));
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -143,6 +148,7 @@ public class PlaylistFragment extends BaseListInfoFragment<PlaylistInfo> {
|
||||
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.share)
|
||||
};
|
||||
|
||||
final DialogInterface.OnClickListener actions = (dialogInterface, i) -> {
|
||||
@@ -163,6 +169,9 @@ public class PlaylistFragment extends BaseListInfoFragment<PlaylistInfo> {
|
||||
case 4:
|
||||
NavigationHelper.playOnPopupPlayer(activity, getPlayQueue(index));
|
||||
break;
|
||||
case 5:
|
||||
shareUrl(item.getName(), item.getUrl());
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
@@ -262,11 +271,16 @@ public class PlaylistFragment extends BaseListInfoFragment<PlaylistInfo> {
|
||||
if (!TextUtils.isEmpty(result.getUploaderName())) {
|
||||
headerUploaderName.setText(result.getUploaderName());
|
||||
if (!TextUtils.isEmpty(result.getUploaderUrl())) {
|
||||
headerUploaderLayout.setOnClickListener(v ->
|
||||
headerUploaderLayout.setOnClickListener(v -> {
|
||||
try {
|
||||
NavigationHelper.openChannelFragment(getFragmentManager(),
|
||||
result.getServiceId(), result.getUploaderUrl(),
|
||||
result.getUploaderName())
|
||||
);
|
||||
result.getServiceId(),
|
||||
result.getUploaderUrl(),
|
||||
result.getUploaderName());
|
||||
} catch (Exception e) {
|
||||
ErrorActivity.reportUiError((AppCompatActivity) getActivity(), e);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -282,14 +296,11 @@ public class PlaylistFragment extends BaseListInfoFragment<PlaylistInfo> {
|
||||
}
|
||||
|
||||
remotePlaylistManager.getPlaylist(result)
|
||||
.flatMap(lists -> getUpdateProcessor(lists, result), (lists, id) -> lists)
|
||||
.onBackpressureLatest()
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(getPlaylistBookmarkSubscriber());
|
||||
|
||||
remotePlaylistManager.onUpdate(result)
|
||||
.subscribeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(integer -> {/* Do nothing*/}, this::onError);
|
||||
|
||||
headerPlayAllButton.setOnClickListener(view ->
|
||||
NavigationHelper.playOnMainPlayer(activity, getPlayQueue()));
|
||||
headerPopupButton.setOnClickListener(view ->
|
||||
@@ -337,7 +348,11 @@ public class PlaylistFragment extends BaseListInfoFragment<PlaylistInfo> {
|
||||
if (super.onError(exception)) return true;
|
||||
|
||||
int errorId = exception instanceof ExtractionException ? R.string.parsing_error : R.string.general_error;
|
||||
onUnrecoverableError(exception, UserAction.REQUESTED_PLAYLIST, NewPipe.getNameOfService(serviceId), url, errorId);
|
||||
onUnrecoverableError(exception,
|
||||
UserAction.REQUESTED_PLAYLIST,
|
||||
NewPipe.getNameOfService(serviceId),
|
||||
url,
|
||||
errorId);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -345,6 +360,17 @@ public class PlaylistFragment extends BaseListInfoFragment<PlaylistInfo> {
|
||||
// Utils
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
private Flowable<Integer> getUpdateProcessor(@NonNull List<PlaylistRemoteEntity> playlists,
|
||||
@NonNull PlaylistInfo result) {
|
||||
final Flowable<Integer> noItemToUpdate = Flowable.just(/*noItemToUpdate=*/-1);
|
||||
if (playlists.isEmpty()) return noItemToUpdate;
|
||||
|
||||
final PlaylistRemoteEntity playlistEntity = playlists.get(0);
|
||||
if (playlistEntity.isIdenticalTo(result)) return noItemToUpdate;
|
||||
|
||||
return remotePlaylistManager.onUpdate(playlists.get(0).getUid(), result).toFlowable();
|
||||
}
|
||||
|
||||
private Subscriber<List<PlaylistRemoteEntity>> getPlaylistBookmarkSubscriber() {
|
||||
return new Subscriber<List<PlaylistRemoteEntity>>() {
|
||||
@Override
|
||||
@@ -417,4 +443,4 @@ public class PlaylistFragment extends BaseListInfoFragment<PlaylistInfo> {
|
||||
playlistBookmarkButton.setIcon(ThemeHelper.resolveResourceIdFromAttr(activity, iconAttr));
|
||||
playlistBookmarkButton.setTitle(titleRes);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -37,26 +37,30 @@ import org.schabi.newpipe.extractor.ListExtractor;
|
||||
import org.schabi.newpipe.extractor.NewPipe;
|
||||
import org.schabi.newpipe.extractor.StreamingService;
|
||||
import org.schabi.newpipe.extractor.exceptions.ParsingException;
|
||||
import org.schabi.newpipe.extractor.search.SearchEngine;
|
||||
import org.schabi.newpipe.extractor.search.SearchResult;
|
||||
import org.schabi.newpipe.extractor.search.SearchExtractor;
|
||||
import org.schabi.newpipe.extractor.search.SearchInfo;
|
||||
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.ErrorActivity;
|
||||
import org.schabi.newpipe.report.UserAction;
|
||||
import org.schabi.newpipe.util.Constants;
|
||||
import org.schabi.newpipe.util.AnimationUtils;
|
||||
import org.schabi.newpipe.util.ExtractorHelper;
|
||||
import org.schabi.newpipe.util.LayoutManagerSmoothScroller;
|
||||
import org.schabi.newpipe.util.NavigationHelper;
|
||||
import org.schabi.newpipe.util.ServiceHelper;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InterruptedIOException;
|
||||
import java.net.SocketException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashMap;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Queue;
|
||||
import java.util.concurrent.Callable;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import icepick.State;
|
||||
@@ -65,14 +69,15 @@ import io.reactivex.Observable;
|
||||
import io.reactivex.android.schedulers.AndroidSchedulers;
|
||||
import io.reactivex.disposables.CompositeDisposable;
|
||||
import io.reactivex.disposables.Disposable;
|
||||
import io.reactivex.functions.Consumer;
|
||||
import io.reactivex.schedulers.Schedulers;
|
||||
import io.reactivex.subjects.PublishSubject;
|
||||
|
||||
import static java.util.Arrays.asList;
|
||||
|
||||
import static org.schabi.newpipe.util.AnimationUtils.animateView;
|
||||
|
||||
public class SearchFragment
|
||||
extends BaseListFragment<SearchResult, ListExtractor.InfoItemsPage>
|
||||
extends BaseListFragment<SearchInfo, ListExtractor.InfoItemsPage>
|
||||
implements BackPressable {
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
@@ -92,19 +97,29 @@ public class SearchFragment
|
||||
|
||||
@State
|
||||
protected int filterItemCheckedId = -1;
|
||||
private SearchEngine.Filter filter = SearchEngine.Filter.ANY;
|
||||
|
||||
@State
|
||||
protected int serviceId = Constants.NO_SERVICE_ID;
|
||||
|
||||
// this three represet the current search query
|
||||
@State
|
||||
protected String searchQuery;
|
||||
protected String searchString;
|
||||
@State
|
||||
protected String lastSearchedQuery;
|
||||
protected String[] contentFilter;
|
||||
@State
|
||||
protected String sortFilter;
|
||||
|
||||
// these represtent the last search
|
||||
@State
|
||||
protected String lastSearchedString;
|
||||
|
||||
@State
|
||||
protected boolean wasSearchFocused = false;
|
||||
|
||||
private int currentPage = 0;
|
||||
private int currentNextPage = 0;
|
||||
private Map<Integer, String> menuItemToFilterName;
|
||||
private StreamingService service;
|
||||
private String currentPageUrl;
|
||||
private String nextPageUrl;
|
||||
private String contentCountry;
|
||||
private boolean isSuggestionsEnabled = true;
|
||||
private boolean isSearchHistoryEnabled = true;
|
||||
@@ -130,11 +145,11 @@ public class SearchFragment
|
||||
|
||||
/*////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
public static SearchFragment getInstance(int serviceId, String query) {
|
||||
public static SearchFragment getInstance(int serviceId, String searchString) {
|
||||
SearchFragment searchFragment = new SearchFragment();
|
||||
searchFragment.setQuery(serviceId, query);
|
||||
searchFragment.setQuery(serviceId, searchString, new String[0], "");
|
||||
|
||||
if (!TextUtils.isEmpty(query)) {
|
||||
if (!TextUtils.isEmpty(searchString)) {
|
||||
searchFragment.setSearchOnResume();
|
||||
}
|
||||
|
||||
@@ -202,13 +217,22 @@ public class SearchFragment
|
||||
if (DEBUG) Log.d(TAG, "onResume() called");
|
||||
super.onResume();
|
||||
|
||||
if (!TextUtils.isEmpty(searchQuery)) {
|
||||
try {
|
||||
service = NewPipe.getService(serviceId);
|
||||
} catch (Exception e) {
|
||||
ErrorActivity.reportError(getActivity(), e, getActivity().getClass(),
|
||||
getActivity().findViewById(android.R.id.content),
|
||||
ErrorActivity.ErrorInfo.make(UserAction.UI_ERROR,
|
||||
"",
|
||||
"", R.string.general_error));
|
||||
}
|
||||
|
||||
if (!TextUtils.isEmpty(searchString)) {
|
||||
if (wasLoading.getAndSet(false)) {
|
||||
if (currentNextPage > currentPage) loadMoreItems();
|
||||
else search(searchQuery);
|
||||
search(searchString, contentFilter, sortFilter);
|
||||
} else if (infoListAdapter.getItemsList().size() == 0) {
|
||||
if (savedState == null) {
|
||||
search(searchQuery);
|
||||
search(searchString, contentFilter, sortFilter);
|
||||
} else if (!isLoading.get() && !wasSearchFocused) {
|
||||
infoListAdapter.clearStreamItemList();
|
||||
showEmptyState();
|
||||
@@ -218,7 +242,7 @@ public class SearchFragment
|
||||
|
||||
if (suggestionDisposable == null || suggestionDisposable.isDisposed()) initSuggestionObserver();
|
||||
|
||||
if (TextUtils.isEmpty(searchQuery) || wasSearchFocused) {
|
||||
if (TextUtils.isEmpty(searchString) || wasSearchFocused) {
|
||||
showKeyboardSearch();
|
||||
showSuggestionsPanel();
|
||||
} else {
|
||||
@@ -247,8 +271,9 @@ public class SearchFragment
|
||||
public void onActivityResult(int requestCode, int resultCode, Intent data) {
|
||||
switch (requestCode) {
|
||||
case ReCaptchaActivity.RECAPTCHA_REQUEST:
|
||||
if (resultCode == Activity.RESULT_OK && !TextUtils.isEmpty(searchQuery)) {
|
||||
search(searchQuery);
|
||||
if (resultCode == Activity.RESULT_OK
|
||||
&& !TextUtils.isEmpty(searchString)) {
|
||||
search(searchString, contentFilter, sortFilter);
|
||||
} else Log.e(TAG, "ReCaptcha failed");
|
||||
break;
|
||||
|
||||
@@ -282,20 +307,22 @@ public class SearchFragment
|
||||
@Override
|
||||
public void writeTo(Queue<Object> objectsToSave) {
|
||||
super.writeTo(objectsToSave);
|
||||
objectsToSave.add(currentPage);
|
||||
objectsToSave.add(currentNextPage);
|
||||
objectsToSave.add(currentPageUrl);
|
||||
objectsToSave.add(nextPageUrl);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void readFrom(@NonNull Queue<Object> savedObjects) throws Exception {
|
||||
super.readFrom(savedObjects);
|
||||
currentPage = (int) savedObjects.poll();
|
||||
currentNextPage = (int) savedObjects.poll();
|
||||
currentPageUrl = (String) savedObjects.poll();
|
||||
nextPageUrl = (String) savedObjects.poll();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSaveInstanceState(Bundle bundle) {
|
||||
searchQuery = searchEditText != null ? searchEditText.getText().toString() : searchQuery;
|
||||
searchString = searchEditText != null
|
||||
? searchEditText.getText().toString()
|
||||
: searchString;
|
||||
super.onSaveInstanceState(bundle);
|
||||
}
|
||||
|
||||
@@ -305,8 +332,11 @@ public class SearchFragment
|
||||
|
||||
@Override
|
||||
public void reloadContent() {
|
||||
if (!TextUtils.isEmpty(searchQuery) || (searchEditText != null && !TextUtils.isEmpty(searchEditText.getText()))) {
|
||||
search(!TextUtils.isEmpty(searchQuery) ? searchQuery : searchEditText.getText().toString());
|
||||
if (!TextUtils.isEmpty(searchString)
|
||||
|| (searchEditText != null && !TextUtils.isEmpty(searchEditText.getText()))) {
|
||||
search(!TextUtils.isEmpty(searchString)
|
||||
? searchString
|
||||
: searchEditText.getText().toString(), new String[0], "");
|
||||
} else {
|
||||
if (searchEditText != null) {
|
||||
searchEditText.setText("");
|
||||
@@ -330,22 +360,35 @@ public class SearchFragment
|
||||
supportActionBar.setDisplayHomeAsUpEnabled(true);
|
||||
}
|
||||
|
||||
inflater.inflate(R.menu.menu_search, menu);
|
||||
menuItemToFilterName = new HashMap<>();
|
||||
|
||||
int itemId = 0;
|
||||
boolean isFirstItem = true;
|
||||
final Context c = getContext();
|
||||
for(String filter : service.getSearchQHFactory().getAvailableContentFilter()) {
|
||||
menuItemToFilterName.put(itemId, filter);
|
||||
MenuItem item = menu.add(1,
|
||||
itemId++,
|
||||
0,
|
||||
ServiceHelper.getTranslatedFilterString(filter, c));
|
||||
if(isFirstItem) {
|
||||
item.setChecked(true);
|
||||
isFirstItem = false;
|
||||
}
|
||||
}
|
||||
menu.setGroupCheckable(1, true, true);
|
||||
|
||||
restoreFilterChecked(menu, filterItemCheckedId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(MenuItem item) {
|
||||
switch (item.getItemId()) {
|
||||
case R.id.menu_filter_all:
|
||||
case R.id.menu_filter_video:
|
||||
case R.id.menu_filter_channel:
|
||||
case R.id.menu_filter_playlist:
|
||||
changeFilter(item, getFilterFromMenuId(item.getItemId()));
|
||||
return true;
|
||||
default:
|
||||
return super.onOptionsItemSelected(item);
|
||||
}
|
||||
|
||||
List<String> contentFilter = new ArrayList<>(1);
|
||||
contentFilter.add(menuItemToFilterName.get(item.getItemId()));
|
||||
changeContentFilter(item, contentFilter);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private void restoreFilterChecked(Menu menu, int itemId) {
|
||||
@@ -354,21 +397,6 @@ public class SearchFragment
|
||||
if (item == null) return;
|
||||
|
||||
item.setChecked(true);
|
||||
filter = getFilterFromMenuId(itemId);
|
||||
}
|
||||
}
|
||||
|
||||
private SearchEngine.Filter getFilterFromMenuId(int itemId) {
|
||||
switch (itemId) {
|
||||
case R.id.menu_filter_video:
|
||||
return SearchEngine.Filter.STREAM;
|
||||
case R.id.menu_filter_channel:
|
||||
return SearchEngine.Filter.CHANNEL;
|
||||
case R.id.menu_filter_playlist:
|
||||
return SearchEngine.Filter.PLAYLIST;
|
||||
case R.id.menu_filter_all:
|
||||
default:
|
||||
return SearchEngine.Filter.ANY;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -379,14 +407,21 @@ public class SearchFragment
|
||||
private TextWatcher textWatcher;
|
||||
|
||||
private void showSearchOnStart() {
|
||||
if (DEBUG) Log.d(TAG, "showSearchOnStart() called, searchQuery → " + searchQuery+", lastSearchedQuery → " + lastSearchedQuery);
|
||||
searchEditText.setText(searchQuery);
|
||||
if (DEBUG) Log.d(TAG, "showSearchOnStart() called, searchQuery → "
|
||||
+ searchString
|
||||
+ ", lastSearchedQuery → "
|
||||
+ lastSearchedString);
|
||||
searchEditText.setText(searchString);
|
||||
|
||||
if (TextUtils.isEmpty(searchQuery) || TextUtils.isEmpty(searchEditText.getText())) {
|
||||
if (TextUtils.isEmpty(searchString) || TextUtils.isEmpty(searchEditText.getText())) {
|
||||
searchToolbarContainer.setTranslationX(100);
|
||||
searchToolbarContainer.setAlpha(0f);
|
||||
searchToolbarContainer.setVisibility(View.VISIBLE);
|
||||
searchToolbarContainer.animate().translationX(0).alpha(1f).setDuration(200).setInterpolator(new DecelerateInterpolator()).start();
|
||||
searchToolbarContainer.animate()
|
||||
.translationX(0)
|
||||
.alpha(1f)
|
||||
.setDuration(200)
|
||||
.setInterpolator(new DecelerateInterpolator()).start();
|
||||
} else {
|
||||
searchToolbarContainer.setTranslationX(0);
|
||||
searchToolbarContainer.setAlpha(1f);
|
||||
@@ -396,47 +431,38 @@ public class SearchFragment
|
||||
|
||||
private void initSearchListeners() {
|
||||
if (DEBUG) Log.d(TAG, "initSearchListeners() called");
|
||||
searchClear.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
if (DEBUG) Log.d(TAG, "onClick() called with: v = [" + v + "]");
|
||||
if (TextUtils.isEmpty(searchEditText.getText())) {
|
||||
NavigationHelper.gotoMainFragment(getFragmentManager());
|
||||
return;
|
||||
}
|
||||
|
||||
searchEditText.setText("");
|
||||
suggestionListAdapter.setItems(new ArrayList<SuggestionItem>());
|
||||
showKeyboardSearch();
|
||||
searchClear.setOnClickListener(v -> {
|
||||
if (DEBUG) Log.d(TAG, "onClick() called with: v = [" + v + "]");
|
||||
if (TextUtils.isEmpty(searchEditText.getText())) {
|
||||
NavigationHelper.gotoMainFragment(getFragmentManager());
|
||||
return;
|
||||
}
|
||||
|
||||
searchEditText.setText("");
|
||||
suggestionListAdapter.setItems(new ArrayList<>());
|
||||
showKeyboardSearch();
|
||||
});
|
||||
|
||||
TooltipCompat.setTooltipText(searchClear, getString(R.string.clear));
|
||||
|
||||
searchEditText.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
if (DEBUG) Log.d(TAG, "onClick() called with: v = [" + v + "]");
|
||||
if (isSuggestionsEnabled && errorPanelRoot.getVisibility() != View.VISIBLE) {
|
||||
showSuggestionsPanel();
|
||||
}
|
||||
searchEditText.setOnClickListener(v -> {
|
||||
if (DEBUG) Log.d(TAG, "onClick() called with: v = [" + v + "]");
|
||||
if (isSuggestionsEnabled && errorPanelRoot.getVisibility() != View.VISIBLE) {
|
||||
showSuggestionsPanel();
|
||||
}
|
||||
});
|
||||
|
||||
searchEditText.setOnFocusChangeListener(new View.OnFocusChangeListener() {
|
||||
@Override
|
||||
public void onFocusChange(View v, boolean hasFocus) {
|
||||
if (DEBUG) Log.d(TAG, "onFocusChange() called with: v = [" + v + "], hasFocus = [" + hasFocus + "]");
|
||||
if (isSuggestionsEnabled && hasFocus && errorPanelRoot.getVisibility() != View.VISIBLE) {
|
||||
showSuggestionsPanel();
|
||||
}
|
||||
searchEditText.setOnFocusChangeListener((View v, boolean hasFocus) -> {
|
||||
if (DEBUG) Log.d(TAG, "onFocusChange() called with: v = [" + v + "], hasFocus = [" + hasFocus + "]");
|
||||
if (isSuggestionsEnabled && hasFocus && errorPanelRoot.getVisibility() != View.VISIBLE) {
|
||||
showSuggestionsPanel();
|
||||
}
|
||||
});
|
||||
|
||||
suggestionListAdapter.setListener(new SuggestionListAdapter.OnSuggestionItemSelected() {
|
||||
@Override
|
||||
public void onSuggestionItemSelected(SuggestionItem item) {
|
||||
search(item.query);
|
||||
search(item.query, new String[0], "");
|
||||
searchEditText.setText(item.query);
|
||||
}
|
||||
|
||||
@@ -469,21 +495,22 @@ public class SearchFragment
|
||||
}
|
||||
};
|
||||
searchEditText.addTextChangedListener(textWatcher);
|
||||
searchEditText.setOnEditorActionListener(new TextView.OnEditorActionListener() {
|
||||
@Override
|
||||
public boolean onEditorAction(TextView v, int actionId, KeyEvent event) {
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "onEditorAction() called with: v = [" + v + "], actionId = [" + actionId + "], event = [" + event + "]");
|
||||
}
|
||||
if (event != null && (event.getKeyCode() == KeyEvent.KEYCODE_ENTER || event.getAction() == EditorInfo.IME_ACTION_SEARCH)) {
|
||||
search(searchEditText.getText().toString());
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
});
|
||||
searchEditText.setOnEditorActionListener(
|
||||
(TextView v, int actionId, KeyEvent event) -> {
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "onEditorAction() called with: v = [" + v + "], actionId = [" + actionId + "], event = [" + event + "]");
|
||||
}
|
||||
if (event != null
|
||||
&& (event.getKeyCode() == KeyEvent.KEYCODE_ENTER
|
||||
|| event.getAction() == EditorInfo.IME_ACTION_SEARCH)) {
|
||||
search(searchEditText.getText().toString(), new String[0], "");
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
|
||||
if (suggestionDisposable == null || suggestionDisposable.isDisposed()) initSuggestionObserver();
|
||||
if (suggestionDisposable == null || suggestionDisposable.isDisposed())
|
||||
initSuggestionObserver();
|
||||
}
|
||||
|
||||
private void unsetSearchListeners() {
|
||||
@@ -513,7 +540,8 @@ public class SearchFragment
|
||||
if (searchEditText == null) return;
|
||||
|
||||
if (searchEditText.requestFocus()) {
|
||||
InputMethodManager imm = (InputMethodManager) activity.getSystemService(Context.INPUT_METHOD_SERVICE);
|
||||
InputMethodManager imm = (InputMethodManager) activity.getSystemService(
|
||||
Context.INPUT_METHOD_SERVICE);
|
||||
imm.showSoftInput(searchEditText, InputMethodManager.SHOW_IMPLICIT);
|
||||
}
|
||||
}
|
||||
@@ -522,8 +550,10 @@ public class SearchFragment
|
||||
if (DEBUG) Log.d(TAG, "hideKeyboardSearch() called");
|
||||
if (searchEditText == null) return;
|
||||
|
||||
InputMethodManager imm = (InputMethodManager) activity.getSystemService(Context.INPUT_METHOD_SERVICE);
|
||||
imm.hideSoftInputFromWindow(searchEditText.getWindowToken(), InputMethodManager.HIDE_NOT_ALWAYS);
|
||||
InputMethodManager imm = (InputMethodManager) activity.getSystemService(
|
||||
Context.INPUT_METHOD_SERVICE);
|
||||
imm.hideSoftInputFromWindow(searchEditText.getWindowToken(),
|
||||
InputMethodManager.HIDE_NOT_ALWAYS);
|
||||
|
||||
searchEditText.clearFocus();
|
||||
}
|
||||
@@ -544,9 +574,8 @@ public class SearchFragment
|
||||
howManyDeleted -> suggestionPublisher
|
||||
.onNext(searchEditText.getText().toString()),
|
||||
throwable -> showSnackBarError(throwable,
|
||||
UserAction.SOMETHING_ELSE, "none",
|
||||
"Deleting item failed", R.string.general_error)
|
||||
);
|
||||
UserAction.DELETE_FROM_HISTORY, "none",
|
||||
"Deleting item failed", R.string.general_error));
|
||||
disposables.add(onDelete);
|
||||
})
|
||||
.show();
|
||||
@@ -554,10 +583,12 @@ public class SearchFragment
|
||||
|
||||
@Override
|
||||
public boolean onBackPressed() {
|
||||
if (suggestionsPanel.getVisibility() == View.VISIBLE && infoListAdapter.getItemsList().size() > 0 && !isLoading.get()) {
|
||||
if (suggestionsPanel.getVisibility() == View.VISIBLE
|
||||
&& infoListAdapter.getItemsList().size() > 0
|
||||
&& !isLoading.get()) {
|
||||
hideSuggestionsPanel();
|
||||
hideKeyboardSearch();
|
||||
searchEditText.setText(lastSearchedQuery);
|
||||
searchEditText.setText(lastSearchedString);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
@@ -573,8 +604,10 @@ public class SearchFragment
|
||||
|
||||
final Observable<String> observable = suggestionPublisher
|
||||
.debounce(SUGGESTIONS_DEBOUNCE, TimeUnit.MILLISECONDS)
|
||||
.startWith(searchQuery != null ? searchQuery : "")
|
||||
.filter(query -> isSuggestionsEnabled);
|
||||
.startWith(searchString != null
|
||||
? searchString
|
||||
: "")
|
||||
.filter(searchString -> isSuggestionsEnabled);
|
||||
|
||||
suggestionDisposable = observable
|
||||
.switchMap(query -> {
|
||||
@@ -645,56 +678,44 @@ public class SearchFragment
|
||||
// no-op
|
||||
}
|
||||
|
||||
private void search(final String query) {
|
||||
if (DEBUG) Log.d(TAG, "search() called with: query = [" + query + "]");
|
||||
if (query.isEmpty()) return;
|
||||
private void search(final String searchString, String[] contentFilter, String sortFilter) {
|
||||
if (DEBUG) Log.d(TAG, "search() called with: query = [" + searchString + "]");
|
||||
if (searchString.isEmpty()) return;
|
||||
|
||||
try {
|
||||
final StreamingService service = NewPipe.getServiceByUrl(query);
|
||||
final StreamingService service = NewPipe.getServiceByUrl(searchString);
|
||||
if (service != null) {
|
||||
showLoading();
|
||||
disposables.add(Observable
|
||||
.fromCallable(new Callable<Intent>() {
|
||||
@Override
|
||||
public Intent call() throws Exception {
|
||||
return NavigationHelper.getIntentByLink(activity, service, query);
|
||||
}
|
||||
})
|
||||
.fromCallable(() ->
|
||||
NavigationHelper.getIntentByLink(activity, service, searchString))
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(new Consumer<Intent>() {
|
||||
@Override
|
||||
public void accept(Intent intent) throws Exception {
|
||||
getFragmentManager().popBackStackImmediate();
|
||||
activity.startActivity(intent);
|
||||
}
|
||||
}, new Consumer<Throwable>() {
|
||||
@Override
|
||||
public void accept(Throwable throwable) throws Exception {
|
||||
showError(getString(R.string.url_not_supported_toast), false);
|
||||
}
|
||||
}));
|
||||
.subscribe(intent -> {
|
||||
getFragmentManager().popBackStackImmediate();
|
||||
activity.startActivity(intent);
|
||||
}, throwable ->
|
||||
showError(getString(R.string.url_not_supported_toast), false)));
|
||||
return;
|
||||
}
|
||||
} catch (Exception e) {
|
||||
// Exception occurred, it's not a url
|
||||
}
|
||||
|
||||
lastSearchedQuery = query;
|
||||
searchQuery = query;
|
||||
currentPage = 0;
|
||||
lastSearchedString = this.searchString;
|
||||
this.searchString = searchString;
|
||||
infoListAdapter.clearStreamItemList();
|
||||
hideSuggestionsPanel();
|
||||
hideKeyboardSearch();
|
||||
|
||||
historyRecordManager.onSearched(serviceId, query)
|
||||
historyRecordManager.onSearched(serviceId, searchString)
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(
|
||||
ignored -> {},
|
||||
error -> showSnackBarError(error, UserAction.SEARCHED,
|
||||
NewPipe.getNameOfService(serviceId), query, 0)
|
||||
NewPipe.getNameOfService(serviceId), searchString, 0)
|
||||
);
|
||||
suggestionPublisher.onNext(query);
|
||||
suggestionPublisher.onNext(searchString);
|
||||
startLoading(false);
|
||||
}
|
||||
|
||||
@@ -703,11 +724,16 @@ public class SearchFragment
|
||||
super.startLoading(forceLoad);
|
||||
if (disposables != null) disposables.clear();
|
||||
if (searchDisposable != null) searchDisposable.dispose();
|
||||
searchDisposable = ExtractorHelper.searchFor(serviceId, searchQuery, currentPage, contentCountry, filter)
|
||||
searchDisposable = ExtractorHelper.searchFor(serviceId,
|
||||
searchString,
|
||||
Arrays.asList(contentFilter),
|
||||
sortFilter,
|
||||
contentCountry)
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.doOnEvent((searchResult, throwable) -> isLoading.set(false))
|
||||
.subscribe(this::handleResult, this::onError);
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -715,8 +741,13 @@ public class SearchFragment
|
||||
isLoading.set(true);
|
||||
showListFooter(true);
|
||||
if (searchDisposable != null) searchDisposable.dispose();
|
||||
currentNextPage = currentPage + 1;
|
||||
searchDisposable = ExtractorHelper.getMoreSearchItems(serviceId, searchQuery, currentNextPage, contentCountry, filter)
|
||||
searchDisposable = ExtractorHelper.getMoreSearchItems(
|
||||
serviceId,
|
||||
searchString,
|
||||
asList(contentFilter),
|
||||
sortFilter,
|
||||
nextPageUrl,
|
||||
contentCountry)
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.doOnEvent((nextItemsResult, throwable) -> isLoading.set(false))
|
||||
@@ -739,19 +770,22 @@ public class SearchFragment
|
||||
// Utils
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
private void changeFilter(MenuItem item, SearchEngine.Filter filter) {
|
||||
this.filter = filter;
|
||||
private void changeContentFilter(MenuItem item, List<String> contentFilter) {
|
||||
this.filterItemCheckedId = item.getItemId();
|
||||
item.setChecked(true);
|
||||
|
||||
if (!TextUtils.isEmpty(searchQuery)) {
|
||||
search(searchQuery);
|
||||
this.contentFilter = new String[] {contentFilter.get(0)};
|
||||
|
||||
if (!TextUtils.isEmpty(searchString)) {
|
||||
search(searchString, this.contentFilter, sortFilter);
|
||||
}
|
||||
}
|
||||
|
||||
private void setQuery(int serviceId, String searchQuery) {
|
||||
private void setQuery(int serviceId, String searchString, String[] contentfilter, String sortFilter) {
|
||||
this.serviceId = serviceId;
|
||||
this.searchQuery = searchQuery;
|
||||
this.searchString = searchString;
|
||||
this.contentFilter = contentfilter;
|
||||
this.sortFilter = sortFilter;
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
@@ -772,8 +806,11 @@ public class SearchFragment
|
||||
if (DEBUG) Log.d(TAG, "onSuggestionError() called with: exception = [" + exception + "]");
|
||||
if (super.onError(exception)) return;
|
||||
|
||||
int errorId = exception instanceof ParsingException ? R.string.parsing_error : R.string.general_error;
|
||||
onUnrecoverableError(exception, UserAction.GET_SUGGESTIONS, NewPipe.getNameOfService(serviceId), searchQuery, errorId);
|
||||
int errorId = exception instanceof ParsingException
|
||||
? R.string.parsing_error
|
||||
: R.string.general_error;
|
||||
onUnrecoverableError(exception, UserAction.GET_SUGGESTIONS,
|
||||
NewPipe.getNameOfService(serviceId), searchString, errorId);
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
@@ -798,16 +835,22 @@ public class SearchFragment
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
@Override
|
||||
public void handleResult(@NonNull SearchResult result) {
|
||||
if (!result.errors.isEmpty()) {
|
||||
showSnackBarError(result.errors, UserAction.SEARCHED, NewPipe.getNameOfService(serviceId), searchQuery, 0);
|
||||
public void handleResult(@NonNull SearchInfo result) {
|
||||
final List<Throwable> exceptions = result.getErrors();
|
||||
if (!exceptions.isEmpty()
|
||||
&& !(exceptions.size() == 1
|
||||
&& exceptions.get(0) instanceof SearchExtractor.NothingFoundException)){
|
||||
showSnackBarError(result.getErrors(), UserAction.SEARCHED,
|
||||
NewPipe.getNameOfService(serviceId), searchString, 0);
|
||||
}
|
||||
|
||||
lastSearchedQuery = searchQuery;
|
||||
lastSearchedString = searchString;
|
||||
nextPageUrl = result.getNextPageUrl();
|
||||
currentPageUrl = result.getUrl();
|
||||
|
||||
if (infoListAdapter.getItemsList().size() == 0) {
|
||||
if (!result.getResults().isEmpty()) {
|
||||
infoListAdapter.addInfoItemList(result.getResults());
|
||||
if (!result.getRelatedItems().isEmpty()) {
|
||||
infoListAdapter.addInfoItemList(result.getRelatedItems());
|
||||
} else {
|
||||
infoListAdapter.clearStreamItemList();
|
||||
showEmptyState();
|
||||
@@ -821,12 +864,14 @@ public class SearchFragment
|
||||
@Override
|
||||
public void handleNextItems(ListExtractor.InfoItemsPage result) {
|
||||
showListFooter(false);
|
||||
currentPage = Integer.parseInt(result.getNextPageUrl());
|
||||
currentPageUrl = result.getNextPageUrl();
|
||||
infoListAdapter.addInfoItemList(result.getItems());
|
||||
nextPageUrl = result.getNextPageUrl();
|
||||
|
||||
if (!result.getErrors().isEmpty()) {
|
||||
showSnackBarError(result.getErrors(), UserAction.SEARCHED, NewPipe.getNameOfService(serviceId)
|
||||
, "\"" + searchQuery + "\" → page " + currentPage, 0);
|
||||
showSnackBarError(result.getErrors(), UserAction.SEARCHED,
|
||||
NewPipe.getNameOfService(serviceId)
|
||||
, "\"" + searchString + "\" → page: " + nextPageUrl, 0);
|
||||
}
|
||||
super.handleNextItems(result);
|
||||
}
|
||||
@@ -835,12 +880,15 @@ public class SearchFragment
|
||||
protected boolean onError(Throwable exception) {
|
||||
if (super.onError(exception)) return true;
|
||||
|
||||
if (exception instanceof SearchEngine.NothingFoundException) {
|
||||
if (exception instanceof SearchExtractor.NothingFoundException) {
|
||||
infoListAdapter.clearStreamItemList();
|
||||
showEmptyState();
|
||||
} else {
|
||||
int errorId = exception instanceof ParsingException ? R.string.parsing_error : R.string.general_error;
|
||||
onUnrecoverableError(exception, UserAction.SEARCHED, NewPipe.getNameOfService(serviceId), searchQuery, errorId);
|
||||
int errorId = exception instanceof ParsingException
|
||||
? R.string.parsing_error
|
||||
: R.string.general_error;
|
||||
onUnrecoverableError(exception, UserAction.SEARCHED,
|
||||
NewPipe.getNameOfService(serviceId), searchString, errorId);
|
||||
}
|
||||
|
||||
return true;
|
||||
|
||||
@@ -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,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,171 +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.ImageDisplayConstants;
|
||||
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,
|
||||
ImageDisplayConstants.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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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()));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,4 +1,4 @@
|
||||
package org.schabi.newpipe.fragments.local;
|
||||
package org.schabi.newpipe.local;
|
||||
|
||||
import android.content.Context;
|
||||
import android.widget.ImageView;
|
||||
@@ -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();
|
||||
@@ -67,11 +66,10 @@ public final class BookmarkFragment
|
||||
public View onCreateView(@NonNull LayoutInflater inflater,
|
||||
@Nullable ViewGroup container,
|
||||
Bundle savedInstanceState) {
|
||||
if (activity != null && activity.getSupportActionBar() != null) {
|
||||
activity.getSupportActionBar().setDisplayShowTitleEnabled(true);
|
||||
activity.setTitle(R.string.tab_subscriptions);
|
||||
}
|
||||
|
||||
if(!useAsFrontPage) {
|
||||
setTitle(activity.getString(R.string.tab_bookmarks));
|
||||
}
|
||||
return inflater.inflate(R.layout.fragment_bookmarks, container, false);
|
||||
}
|
||||
|
||||
@@ -93,15 +91,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();
|
||||
@@ -109,9 +98,7 @@ public final class BookmarkFragment
|
||||
itemListAdapter.setSelectedListener(new OnClickGesture<LocalItem>() {
|
||||
@Override
|
||||
public void selected(LocalItem selectedItem) {
|
||||
// Requires the parent fragment to find holder for fragment replacement
|
||||
if (getParentFragment() == null) return;
|
||||
final FragmentManager fragmentManager = getParentFragment().getFragmentManager();
|
||||
final FragmentManager fragmentManager = getFM();
|
||||
|
||||
if (selectedItem instanceof PlaylistMetadataEntry) {
|
||||
final PlaylistMetadataEntry entry = ((PlaylistMetadataEntry) selectedItem);
|
||||
@@ -120,8 +107,11 @@ public final class BookmarkFragment
|
||||
|
||||
} else if (selectedItem instanceof PlaylistRemoteEntity) {
|
||||
final PlaylistRemoteEntity entry = ((PlaylistRemoteEntity) selectedItem);
|
||||
NavigationHelper.openPlaylistFragment(fragmentManager, entry.getServiceId(),
|
||||
entry.getUrl(), entry.getName());
|
||||
NavigationHelper.openPlaylistFragment(
|
||||
fragmentManager,
|
||||
entry.getServiceId(),
|
||||
entry.getUrl(),
|
||||
entry.getName());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -135,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());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////
|
||||
@@ -179,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,9 +18,9 @@ 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;
|
||||
@@ -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;
|
||||
@@ -22,7 +22,7 @@ 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.report.UserAction;
|
||||
import org.schabi.newpipe.subscription.SubscriptionService;
|
||||
import org.schabi.newpipe.local.subscription.SubscriptionService;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.HashSet;
|
||||
@@ -71,6 +71,10 @@ public class FeedFragment extends BaseListFragment<List<SubscriptionEntity>, Voi
|
||||
|
||||
@Override
|
||||
public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, Bundle savedInstanceState) {
|
||||
|
||||
if(!useAsFrontPage) {
|
||||
setTitle(activity.getString(R.string.fragment_whats_new));
|
||||
}
|
||||
return inflater.inflate(R.layout.fragment_feed, container, false);
|
||||
}
|
||||
|
||||
@@ -105,20 +109,19 @@ public class FeedFragment extends BaseListFragment<List<SubscriptionEntity>, Voi
|
||||
super.onDestroyView();
|
||||
}
|
||||
|
||||
/*@Override
|
||||
protected RecyclerView.LayoutManager getListLayoutManager() {
|
||||
boolean isPortrait = getResources().getDisplayMetrics().heightPixels > getResources().getDisplayMetrics().widthPixels;
|
||||
return new GridLayoutManager(activity, isPortrait ? 1 : 2);
|
||||
}*/
|
||||
@Override
|
||||
public void setUserVisibleHint(boolean isVisibleToUser) {
|
||||
super.setUserVisibleHint(isVisibleToUser);
|
||||
if (activity != null && isVisibleToUser) {
|
||||
setTitle(activity.getString(R.string.fragment_whats_new));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
|
||||
super.onCreateOptionsMenu(menu, inflater);
|
||||
|
||||
ActionBar supportActionBar = activity.getSupportActionBar();
|
||||
if (supportActionBar != null) {
|
||||
supportActionBar.setTitle(R.string.fragment_whats_new);
|
||||
}
|
||||
|
||||
if(useAsFrontPage) {
|
||||
supportActionBar.setDisplayShowTitleEnabled(true);
|
||||
@@ -176,19 +179,9 @@ public class FeedFragment extends BaseListFragment<List<SubscriptionEntity>, Voi
|
||||
showLoading();
|
||||
showListFooter(true);
|
||||
subscriptionObserver = subscriptionService.getSubscription()
|
||||
.onErrorReturnItem(Collections.<SubscriptionEntity>emptyList())
|
||||
.onErrorReturnItem(Collections.emptyList())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(new Consumer<List<SubscriptionEntity>>() {
|
||||
@Override
|
||||
public void accept(List<SubscriptionEntity> subscriptionEntities) throws Exception {
|
||||
handleResult(subscriptionEntities);
|
||||
}
|
||||
}, new Consumer<Throwable>() {
|
||||
@Override
|
||||
public void accept(Throwable throwable) throws Exception {
|
||||
onError(throwable);
|
||||
}
|
||||
});
|
||||
.subscribe(this::handleResult, this::onError);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -239,13 +232,12 @@ public class FeedFragment extends BaseListFragment<List<SubscriptionEntity>, Voi
|
||||
if (!itemsLoaded.contains(subscriptionEntity.getServiceId() + subscriptionEntity.getUrl())) {
|
||||
subscriptionService.getChannelInfo(subscriptionEntity)
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.onErrorComplete(new Predicate<Throwable>() {
|
||||
@Override
|
||||
public boolean test(@io.reactivex.annotations.NonNull Throwable throwable) throws Exception {
|
||||
return FeedFragment.super.onError(throwable);
|
||||
}
|
||||
})
|
||||
.subscribe(getChannelInfoObserver(subscriptionEntity.getServiceId(), subscriptionEntity.getUrl()));
|
||||
.onErrorComplete(
|
||||
(@io.reactivex.annotations.NonNull Throwable throwable) ->
|
||||
FeedFragment.super.onError(throwable))
|
||||
.subscribe(
|
||||
getChannelInfoObserver(subscriptionEntity.getServiceId(),
|
||||
subscriptionEntity.getUrl()));
|
||||
} else {
|
||||
requestFeed(1);
|
||||
}
|
||||
@@ -316,7 +308,10 @@ public class FeedFragment extends BaseListFragment<List<SubscriptionEntity>, Voi
|
||||
|
||||
@Override
|
||||
public void onError(Throwable exception) {
|
||||
showSnackBarError(exception, UserAction.SUBSCRIPTION, NewPipe.getNameOfService(serviceId), url, 0);
|
||||
showSnackBarError(exception,
|
||||
UserAction.SUBSCRIPTION,
|
||||
NewPipe.getNameOfService(serviceId),
|
||||
url, 0);
|
||||
requestFeed(1);
|
||||
onDone();
|
||||
}
|
||||
@@ -361,12 +356,7 @@ public class FeedFragment extends BaseListFragment<List<SubscriptionEntity>, Voi
|
||||
delayHandler.removeCallbacksAndMessages(null);
|
||||
// Add a little of a delay when requesting more items because the cache is so fast,
|
||||
// that the view seems stuck to the user when he scroll to the bottom
|
||||
delayHandler.postDelayed(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
requestFeed(FEED_LOAD_COUNT);
|
||||
}
|
||||
}, 300);
|
||||
delayHandler.postDelayed(() -> requestFeed(FEED_LOAD_COUNT), 300);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -423,7 +413,9 @@ public class FeedFragment extends BaseListFragment<List<SubscriptionEntity>, Voi
|
||||
int heightPixels = getResources().getDisplayMetrics().heightPixels;
|
||||
int itemHeightPixels = activity.getResources().getDimensionPixelSize(R.dimen.video_item_search_height);
|
||||
|
||||
int items = itemHeightPixels > 0 ? heightPixels / itemHeightPixels + OFF_SCREEN_ITEMS_COUNT : MIN_ITEMS_INITIAL_LOAD;
|
||||
int items = itemHeightPixels > 0
|
||||
? heightPixels / itemHeightPixels + OFF_SCREEN_ITEMS_COUNT
|
||||
: MIN_ITEMS_INITIAL_LOAD;
|
||||
return Math.max(MIN_ITEMS_INITIAL_LOAD, items);
|
||||
}
|
||||
|
||||
@@ -441,8 +433,14 @@ public class FeedFragment extends BaseListFragment<List<SubscriptionEntity>, Voi
|
||||
protected boolean onError(Throwable exception) {
|
||||
if (super.onError(exception)) return true;
|
||||
|
||||
int errorId = exception instanceof ExtractionException ? R.string.parsing_error : R.string.general_error;
|
||||
onUnrecoverableError(exception, UserAction.SOMETHING_ELSE, "none", "Requesting feed", errorId);
|
||||
int errorId = exception instanceof ExtractionException
|
||||
? R.string.parsing_error
|
||||
: R.string.general_error;
|
||||
onUnrecoverableError(exception,
|
||||
UserAction.SOMETHING_ELSE,
|
||||
"none",
|
||||
"Requesting feed",
|
||||
errorId);
|
||||
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,15 @@ 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.fragments.list.BaseListFragment;
|
||||
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 +37,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 +57,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.compare(right.watchCount, left.watchCount));
|
||||
return results;
|
||||
default: return null;
|
||||
}
|
||||
}
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////
|
||||
// Fragment LifeCycle - Creation
|
||||
@@ -71,6 +97,14 @@ public abstract class StatisticsPlaylistFragment
|
||||
return inflater.inflate(R.layout.fragment_playlist, container, false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setUserVisibleHint(boolean isVisibleToUser) {
|
||||
super.setUserVisibleHint(isVisibleToUser);
|
||||
if (activity != null && isVisibleToUser) {
|
||||
setTitle(activity.getString(R.string.title_activity_history));
|
||||
}
|
||||
}
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////
|
||||
// Fragment LifeCycle - Views
|
||||
///////////////////////////////////////////////////////////////////////////
|
||||
@@ -78,16 +112,22 @@ public abstract class StatisticsPlaylistFragment
|
||||
@Override
|
||||
protected void initViews(View rootView, Bundle savedInstanceState) {
|
||||
super.initViews(rootView, savedInstanceState);
|
||||
setTitle(getName());
|
||||
if(!useAsFrontPage) {
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -100,8 +140,10 @@ public abstract class StatisticsPlaylistFragment
|
||||
public void selected(LocalItem selectedItem) {
|
||||
if (selectedItem instanceof StreamStatisticsEntry) {
|
||||
final StreamStatisticsEntry item = (StreamStatisticsEntry) selectedItem;
|
||||
NavigationHelper.openVideoDetailFragment(getFragmentManager(),
|
||||
item.serviceId, item.url, item.title);
|
||||
NavigationHelper.openVideoDetailFragment(getFM(),
|
||||
item.serviceId,
|
||||
item.url,
|
||||
item.title);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -193,6 +235,8 @@ public abstract class StatisticsPlaylistFragment
|
||||
super.handleResult(result);
|
||||
if (itemListAdapter == null) return;
|
||||
|
||||
playlistCtrl.setVisibility(View.VISIBLE);
|
||||
|
||||
itemListAdapter.clearStreamItemList();
|
||||
|
||||
if (result.isEmpty()) {
|
||||
@@ -212,6 +256,7 @@ public abstract class StatisticsPlaylistFragment
|
||||
NavigationHelper.playOnPopupPlayer(activity, getPlayQueue()));
|
||||
headerBackgroundButton.setOnClickListener(view ->
|
||||
NavigationHelper.playOnBackgroundPlayer(activity, getPlayQueue()));
|
||||
sortButton.setOnClickListener(view -> toggleSortMode());
|
||||
|
||||
hideLoading();
|
||||
}
|
||||
@@ -238,6 +283,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 +310,8 @@ 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),
|
||||
context.getResources().getString(R.string.share)
|
||||
};
|
||||
|
||||
final DialogInterface.OnClickListener actions = (dialogInterface, i) -> {
|
||||
@@ -270,6 +332,12 @@ public abstract class StatisticsPlaylistFragment
|
||||
case 4:
|
||||
NavigationHelper.playOnPopupPlayer(activity, getPlayQueue(index));
|
||||
break;
|
||||
case 5:
|
||||
deleteEntry(index);
|
||||
break;
|
||||
case 6:
|
||||
shareUrl(item.toStreamInfoItem().getName(), item.toStreamInfoItem().getUrl());
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
@@ -278,6 +346,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(
|
||||
howManyDeleted -> {
|
||||
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,11 +1,11 @@
|
||||
package org.schabi.newpipe.fragments.local.holder;
|
||||
package org.schabi.newpipe.local.holder;
|
||||
|
||||
import android.support.v7.widget.RecyclerView;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.ViewGroup;
|
||||
|
||||
import org.schabi.newpipe.database.LocalItem;
|
||||
import org.schabi.newpipe.fragments.local.LocalItemBuilder;
|
||||
import org.schabi.newpipe.local.LocalItemBuilder;
|
||||
|
||||
import java.text.DateFormat;
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
package org.schabi.newpipe.fragments.local.holder;
|
||||
package org.schabi.newpipe.local.holder;
|
||||
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
|
||||
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;
|
||||
@@ -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.MotionEvent;
|
||||
@@ -11,7 +11,7 @@ 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;
|
||||
|
||||
@@ -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;
|
||||
@@ -10,7 +10,7 @@ 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;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package org.schabi.newpipe.fragments.local.holder;
|
||||
package org.schabi.newpipe.local.holder;
|
||||
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.ImageView;
|
||||
@@ -6,7 +6,7 @@ import android.widget.TextView;
|
||||
|
||||
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;
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
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;
|
||||
|
||||
@@ -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;
|
||||
@@ -520,7 +520,8 @@ public class LocalPlaylistFragment extends BaseLocalListFragment<List<PlaylistSt
|
||||
context.getResources().getString(R.string.start_here_on_background),
|
||||
context.getResources().getString(R.string.start_here_on_popup),
|
||||
context.getResources().getString(R.string.set_as_playlist_thumbnail),
|
||||
context.getResources().getString(R.string.delete)
|
||||
context.getResources().getString(R.string.delete),
|
||||
context.getResources().getString(R.string.share)
|
||||
};
|
||||
|
||||
final DialogInterface.OnClickListener actions = (dialogInterface, i) -> {
|
||||
@@ -549,6 +550,9 @@ public class LocalPlaylistFragment extends BaseLocalListFragment<List<PlaylistSt
|
||||
case 6:
|
||||
deleteItem(item);
|
||||
break;
|
||||
case 7:
|
||||
shareUrl(item.toStreamInfoItem().getName(), item.toStreamInfoItem().getUrl());
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -42,8 +40,11 @@ public class RemotePlaylistManager {
|
||||
}).subscribeOn(Schedulers.io());
|
||||
}
|
||||
|
||||
public Single<Integer> onUpdate(final PlaylistInfo playlistInfo) {
|
||||
return Single.fromCallable(() -> playlistRemoteTable.update(new PlaylistRemoteEntity(playlistInfo)))
|
||||
.subscribeOn(Schedulers.io());
|
||||
public Single<Integer> onUpdate(final long playlistId, final PlaylistInfo playlistInfo) {
|
||||
return Single.fromCallable(() -> {
|
||||
PlaylistRemoteEntity playlist = new PlaylistRemoteEntity(playlistInfo);
|
||||
playlist.setUid(playlistId);
|
||||
return playlistRemoteTable.update(playlist);
|
||||
}).subscribeOn(Schedulers.io());
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package org.schabi.newpipe.fragments.subscription;
|
||||
package org.schabi.newpipe.local.subscription;
|
||||
|
||||
import android.app.AlertDialog;
|
||||
import android.app.Dialog;
|
||||
@@ -1,4 +1,4 @@
|
||||
package org.schabi.newpipe.subscription;
|
||||
package org.schabi.newpipe.local.subscription;
|
||||
|
||||
public interface ImportExportEventListener {
|
||||
/**
|
||||
@@ -17,7 +17,7 @@
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.schabi.newpipe.subscription;
|
||||
package org.schabi.newpipe.local.subscription;
|
||||
|
||||
import android.support.annotation.Nullable;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package org.schabi.newpipe.fragments.subscription;
|
||||
package org.schabi.newpipe.local.subscription;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.BroadcastReceiver;
|
||||
@@ -13,8 +13,10 @@ import android.os.Parcelable;
|
||||
import android.support.annotation.DrawableRes;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.support.v4.app.FragmentManager;
|
||||
import android.support.v4.content.LocalBroadcastManager;
|
||||
import android.support.v7.app.ActionBar;
|
||||
import android.support.v7.app.AppCompatActivity;
|
||||
import android.support.v7.widget.LinearLayoutManager;
|
||||
import android.support.v7.widget.RecyclerView;
|
||||
import android.view.LayoutInflater;
|
||||
@@ -38,10 +40,10 @@ 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.ErrorActivity;
|
||||
import org.schabi.newpipe.report.UserAction;
|
||||
import org.schabi.newpipe.subscription.SubscriptionService;
|
||||
import org.schabi.newpipe.subscription.services.SubscriptionsExportService;
|
||||
import org.schabi.newpipe.subscription.services.SubscriptionsImportService;
|
||||
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;
|
||||
@@ -53,7 +55,6 @@ import java.io.File;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.Comparator;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
@@ -65,9 +66,9 @@ import io.reactivex.disposables.CompositeDisposable;
|
||||
import io.reactivex.disposables.Disposable;
|
||||
import io.reactivex.schedulers.Schedulers;
|
||||
|
||||
import static org.schabi.newpipe.subscription.services.SubscriptionsImportService.KEY_MODE;
|
||||
import static org.schabi.newpipe.subscription.services.SubscriptionsImportService.KEY_VALUE;
|
||||
import static org.schabi.newpipe.subscription.services.SubscriptionsImportService.PREVIOUS_EXPORT_MODE;
|
||||
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;
|
||||
|
||||
@@ -209,7 +210,8 @@ public class SubscriptionFragment extends BaseStateFragment<List<SubscriptionEnt
|
||||
}
|
||||
|
||||
private void setupImportFromItems(final ViewGroup listHolder) {
|
||||
final View previousBackupItem = addItemView(getString(R.string.previous_export), ThemeHelper.resolveResourceIdFromAttr(getContext(), R.attr.ic_backup), 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;
|
||||
@@ -241,8 +243,8 @@ public class SubscriptionFragment extends BaseStateFragment<List<SubscriptionEnt
|
||||
}
|
||||
|
||||
private void onImportFromServiceSelected(int serviceId) {
|
||||
if (getParentFragment() == null) return;
|
||||
NavigationHelper.openSubscriptionsImportFragment(getParentFragment().getFragmentManager(), serviceId);
|
||||
FragmentManager fragmentManager = getFM();
|
||||
NavigationHelper.openSubscriptionsImportFragment(fragmentManager, serviceId);
|
||||
}
|
||||
|
||||
private void onImportPreviousSelected() {
|
||||
@@ -320,15 +322,19 @@ public class SubscriptionFragment extends BaseStateFragment<List<SubscriptionEnt
|
||||
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());
|
||||
final FragmentManager fragmentManager = getFM();
|
||||
NavigationHelper.openChannelFragment(fragmentManager,
|
||||
selectedItem.getServiceId(),
|
||||
selectedItem.getUrl(),
|
||||
selectedItem.getName());
|
||||
}
|
||||
});
|
||||
|
||||
//noinspection ConstantConditions
|
||||
whatsNewItemListHeader.setOnClickListener(v ->
|
||||
NavigationHelper.openWhatsNewFragment(getParentFragment().getFragmentManager()));
|
||||
whatsNewItemListHeader.setOnClickListener(v -> {
|
||||
FragmentManager fragmentManager = getFM();
|
||||
NavigationHelper.openWhatsNewFragment(fragmentManager);
|
||||
});
|
||||
importExportListHeader.setOnClickListener(v -> importExportOptions.switchState());
|
||||
}
|
||||
|
||||
@@ -399,10 +405,13 @@ public class SubscriptionFragment extends BaseStateFragment<List<SubscriptionEnt
|
||||
|
||||
private List<InfoItem> getSubscriptionItems(List<SubscriptionEntity> subscriptions) {
|
||||
List<InfoItem> items = new ArrayList<>();
|
||||
for (final SubscriptionEntity subscription : subscriptions) items.add(subscription.toChannelInfoItem());
|
||||
for (final SubscriptionEntity subscription : subscriptions) {
|
||||
items.add(subscription.toChannelInfoItem());
|
||||
}
|
||||
|
||||
Collections.sort(items,
|
||||
(InfoItem o1, InfoItem o2) -> o1.getName().compareToIgnoreCase(o2.getName()));
|
||||
(InfoItem o1, InfoItem o2) ->
|
||||
o1.getName().compareToIgnoreCase(o2.getName()));
|
||||
return items;
|
||||
}
|
||||
|
||||
@@ -431,7 +440,11 @@ public class SubscriptionFragment extends BaseStateFragment<List<SubscriptionEnt
|
||||
resetFragment();
|
||||
if (super.onError(exception)) return true;
|
||||
|
||||
onUnrecoverableError(exception, UserAction.SOMETHING_ELSE, "none", "Subscriptions", R.string.general_error);
|
||||
onUnrecoverableError(exception,
|
||||
UserAction.SOMETHING_ELSE,
|
||||
"none",
|
||||
"Subscriptions",
|
||||
R.string.general_error);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package org.schabi.newpipe.subscription;
|
||||
package org.schabi.newpipe.local.subscription;
|
||||
|
||||
import android.content.Context;
|
||||
import android.support.annotation.NonNull;
|
||||
@@ -1,4 +1,4 @@
|
||||
package org.schabi.newpipe.fragments.subscription;
|
||||
package org.schabi.newpipe.local.subscription;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.Intent;
|
||||
@@ -26,7 +26,7 @@ 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.subscription.services.SubscriptionsImportService;
|
||||
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;
|
||||
@@ -37,10 +37,10 @@ import java.util.List;
|
||||
import icepick.State;
|
||||
|
||||
import static org.schabi.newpipe.extractor.subscription.SubscriptionExtractor.ContentSource.CHANNEL_URL;
|
||||
import static org.schabi.newpipe.subscription.services.SubscriptionsImportService.CHANNEL_URL_MODE;
|
||||
import static org.schabi.newpipe.subscription.services.SubscriptionsImportService.INPUT_STREAM_MODE;
|
||||
import static org.schabi.newpipe.subscription.services.SubscriptionsImportService.KEY_MODE;
|
||||
import static org.schabi.newpipe.subscription.services.SubscriptionsImportService.KEY_VALUE;
|
||||
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;
|
||||
@@ -17,7 +17,7 @@
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.schabi.newpipe.subscription.services;
|
||||
package org.schabi.newpipe.local.subscription.services;
|
||||
|
||||
import android.app.Service;
|
||||
import android.content.Intent;
|
||||
@@ -36,8 +36,8 @@ 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.subscription.ImportExportEventListener;
|
||||
import org.schabi.newpipe.subscription.SubscriptionService;
|
||||
import org.schabi.newpipe.local.subscription.ImportExportEventListener;
|
||||
import org.schabi.newpipe.local.subscription.SubscriptionService;
|
||||
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.IOException;
|
||||
@@ -17,7 +17,7 @@
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.schabi.newpipe.subscription.services;
|
||||
package org.schabi.newpipe.local.subscription.services;
|
||||
|
||||
import android.content.Intent;
|
||||
import android.support.v4.content.LocalBroadcastManager;
|
||||
@@ -29,7 +29,7 @@ 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.subscription.ImportExportJsonHelper;
|
||||
import org.schabi.newpipe.local.subscription.ImportExportJsonHelper;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileNotFoundException;
|
||||
@@ -49,7 +49,7 @@ public class SubscriptionsExportService extends BaseImportExportService {
|
||||
/**
|
||||
* 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.subscription.services.SubscriptionsExportService.EXPORT_COMPLETE";
|
||||
public static final String EXPORT_COMPLETE_ACTION = "org.schabi.newpipe.local.subscription.services.SubscriptionsExportService.EXPORT_COMPLETE";
|
||||
|
||||
private Subscription subscription;
|
||||
private File outFile;
|
||||
@@ -17,7 +17,7 @@
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.schabi.newpipe.subscription.services;
|
||||
package org.schabi.newpipe.local.subscription.services;
|
||||
|
||||
import android.content.Intent;
|
||||
import android.support.annotation.NonNull;
|
||||
@@ -33,7 +33,7 @@ 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.subscription.ImportExportJsonHelper;
|
||||
import org.schabi.newpipe.local.subscription.ImportExportJsonHelper;
|
||||
import org.schabi.newpipe.util.Constants;
|
||||
import org.schabi.newpipe.util.ExtractorHelper;
|
||||
|
||||
@@ -64,7 +64,7 @@ public class SubscriptionsImportService extends BaseImportExportService {
|
||||
/**
|
||||
* 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.subscription.services.SubscriptionsImportService.IMPORT_COMPLETE";
|
||||
public static final String IMPORT_COMPLETE_ACTION = "org.schabi.newpipe.local.subscription.services.SubscriptionsImportService.IMPORT_COMPLETE";
|
||||
|
||||
private Subscription subscription;
|
||||
private int currentMode;
|
||||
@@ -28,7 +28,6 @@ import android.content.IntentFilter;
|
||||
import android.graphics.Bitmap;
|
||||
import android.os.Build;
|
||||
import android.os.IBinder;
|
||||
import android.support.annotation.IntRange;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.support.v4.app.NotificationCompat;
|
||||
@@ -39,17 +38,16 @@ import android.widget.RemoteViews;
|
||||
import com.google.android.exoplayer2.PlaybackParameters;
|
||||
import com.google.android.exoplayer2.Player;
|
||||
import com.google.android.exoplayer2.source.MediaSource;
|
||||
import com.nostra13.universalimageloader.core.assist.FailReason;
|
||||
|
||||
import org.schabi.newpipe.BuildConfig;
|
||||
import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.extractor.MediaFormat;
|
||||
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.player.helper.PlayerHelper;
|
||||
import org.schabi.newpipe.playlist.PlayQueueItem;
|
||||
import org.schabi.newpipe.util.ListHelper;
|
||||
import org.schabi.newpipe.player.playqueue.PlayQueueItem;
|
||||
import org.schabi.newpipe.player.resolver.AudioPlaybackResolver;
|
||||
import org.schabi.newpipe.player.resolver.MediaSourceTag;
|
||||
import org.schabi.newpipe.util.NavigationHelper;
|
||||
import org.schabi.newpipe.util.ThemeHelper;
|
||||
|
||||
@@ -94,7 +92,6 @@ public final class BackgroundPlayer extends Service {
|
||||
private NotificationCompat.Builder notBuilder;
|
||||
private RemoteViews notRemoteView;
|
||||
private RemoteViews bigNotRemoteView;
|
||||
private final String setAlphaMethodName = (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) ? "setImageAlpha" : "setAlpha";
|
||||
|
||||
private boolean shouldUpdateOnProgress;
|
||||
|
||||
@@ -118,8 +115,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;
|
||||
}
|
||||
|
||||
@@ -160,6 +161,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();
|
||||
}
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
@@ -183,7 +189,9 @@ public final class BackgroundPlayer extends Service {
|
||||
.setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
|
||||
.setCustomContentView(notRemoteView)
|
||||
.setCustomBigContentView(bigNotRemoteView);
|
||||
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.JELLY_BEAN) builder.setPriority(NotificationCompat.PRIORITY_MAX);
|
||||
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.JELLY_BEAN) {
|
||||
builder.setPriority(NotificationCompat.PRIORITY_MAX);
|
||||
}
|
||||
return builder;
|
||||
}
|
||||
|
||||
@@ -240,15 +248,6 @@ public final class BackgroundPlayer extends Service {
|
||||
notificationManager.notify(NOTIFICATION_ID, notBuilder.build());
|
||||
}
|
||||
|
||||
private void setControlsOpacity(@IntRange(from = 0, to = 255) int opacity) {
|
||||
if (notRemoteView != null) notRemoteView.setInt(R.id.notificationPlayPause, setAlphaMethodName, opacity);
|
||||
if (bigNotRemoteView != null) bigNotRemoteView.setInt(R.id.notificationPlayPause, setAlphaMethodName, opacity);
|
||||
if (notRemoteView != null) notRemoteView.setInt(R.id.notificationFForward, setAlphaMethodName, opacity);
|
||||
if (bigNotRemoteView != null) bigNotRemoteView.setInt(R.id.notificationFForward, setAlphaMethodName, opacity);
|
||||
if (notRemoteView != null) notRemoteView.setInt(R.id.notificationFRewind, setAlphaMethodName, opacity);
|
||||
if (bigNotRemoteView != null) bigNotRemoteView.setInt(R.id.notificationFRewind, setAlphaMethodName, opacity);
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Utils
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
@@ -270,8 +269,16 @@ public final class BackgroundPlayer extends Service {
|
||||
|
||||
protected class BasePlayerImpl extends BasePlayer {
|
||||
|
||||
@NonNull final private AudioPlaybackResolver resolver;
|
||||
|
||||
BasePlayerImpl(Context context) {
|
||||
super(context);
|
||||
this.resolver = new AudioPlaybackResolver(context, dataSource);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void initPlayer(boolean playOnReady) {
|
||||
super.initPlayer(playOnReady);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -284,30 +291,41 @@ public final class BackgroundPlayer extends Service {
|
||||
startForeground(NOTIFICATION_ID, notBuilder.build());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void initThumbnail(final String url) {
|
||||
resetNotification();
|
||||
if (notRemoteView != null) notRemoteView.setImageViewResource(R.id.notificationCover, R.drawable.dummy_thumbnail);
|
||||
if (bigNotRemoteView != null) bigNotRemoteView.setImageViewResource(R.id.notificationCover, R.drawable.dummy_thumbnail);
|
||||
updateNotification(-1);
|
||||
super.initThumbnail(url);
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Thumbnail Loading
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
private void updateNotificationThumbnail() {
|
||||
if (basePlayerImpl == null) return;
|
||||
if (notRemoteView != null) {
|
||||
notRemoteView.setImageViewBitmap(R.id.notificationCover,
|
||||
basePlayerImpl.getThumbnail());
|
||||
}
|
||||
if (bigNotRemoteView != null) {
|
||||
bigNotRemoteView.setImageViewBitmap(R.id.notificationCover,
|
||||
basePlayerImpl.getThumbnail());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
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
|
||||
resetNotification();
|
||||
|
||||
if (notRemoteView != null) notRemoteView.setImageViewBitmap(R.id.notificationCover, loadedImage);
|
||||
if (bigNotRemoteView != null) bigNotRemoteView.setImageViewBitmap(R.id.notificationCover, loadedImage);
|
||||
|
||||
updateNotification(-1);
|
||||
}
|
||||
resetNotification();
|
||||
updateNotificationThumbnail();
|
||||
updateNotification(-1);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLoadingFailed(String imageUri, View view, FailReason failReason) {
|
||||
super.onLoadingFailed(imageUri, view, failReason);
|
||||
resetNotification();
|
||||
updateNotificationThumbnail();
|
||||
updateNotification(-1);
|
||||
}
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// States Implementation
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
@Override
|
||||
public void onPrepared(boolean playWhenReady) {
|
||||
super.onPrepared(playWhenReady);
|
||||
@@ -326,6 +344,7 @@ public final class BackgroundPlayer extends Service {
|
||||
|
||||
if (!shouldUpdateOnProgress) return;
|
||||
resetNotification();
|
||||
if(Build.VERSION.SDK_INT >= 26 /*Oreo*/) updateNotificationThumbnail();
|
||||
if (bigNotRemoteView != null) {
|
||||
bigNotRemoteView.setProgressBar(R.id.notificationProgressBar, duration, currentProgress, false);
|
||||
bigNotRemoteView.setTextViewText(R.id.notificationTime, getTimeString(currentProgress) + " / " + getTimeString(duration));
|
||||
@@ -381,29 +400,18 @@ public final class BackgroundPlayer extends Service {
|
||||
// Playback Listener
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
protected void onMetadataChanged(@NonNull final PlayQueueItem item,
|
||||
@Nullable final StreamInfo info,
|
||||
final int newPlayQueueIndex,
|
||||
final boolean hasPlayQueueItemChanged) {
|
||||
if (shouldUpdateOnProgress || hasPlayQueueItemChanged) {
|
||||
resetNotification();
|
||||
updateNotification(-1);
|
||||
updateMetadata();
|
||||
}
|
||||
protected void onMetadataChanged(@NonNull final MediaSourceTag tag) {
|
||||
super.onMetadataChanged(tag);
|
||||
resetNotification();
|
||||
updateNotificationThumbnail();
|
||||
updateNotification(-1);
|
||||
updateMetadata();
|
||||
}
|
||||
|
||||
@Override
|
||||
@Nullable
|
||||
public MediaSource sourceOf(final PlayQueueItem item, final StreamInfo info) {
|
||||
final MediaSource liveSource = super.sourceOf(item, info);
|
||||
if (liveSource != null) return liveSource;
|
||||
|
||||
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()));
|
||||
return resolver.resolve(info);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -430,8 +438,8 @@ public final class BackgroundPlayer extends Service {
|
||||
}
|
||||
|
||||
private void updateMetadata() {
|
||||
if (activityListener != null && currentInfo != null) {
|
||||
activityListener.onMetadataUpdate(currentInfo);
|
||||
if (activityListener != null && getCurrentMetadata() != null) {
|
||||
activityListener.onMetadataUpdate(getCurrentMetadata().getMetadata());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -522,45 +530,36 @@ public final class BackgroundPlayer extends Service {
|
||||
updatePlayback();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBlocked() {
|
||||
super.onBlocked();
|
||||
|
||||
setControlsOpacity(77);
|
||||
updateNotification(-1);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPlaying() {
|
||||
super.onPlaying();
|
||||
|
||||
setControlsOpacity(255);
|
||||
resetNotification();
|
||||
updateNotificationThumbnail();
|
||||
updateNotification(R.drawable.ic_pause_white);
|
||||
|
||||
lockManager.acquireWifiAndCpu();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPaused() {
|
||||
super.onPaused();
|
||||
|
||||
resetNotification();
|
||||
updateNotificationThumbnail();
|
||||
updateNotification(R.drawable.ic_play_arrow_white);
|
||||
if (isProgressLoopRunning()) stopProgressLoop();
|
||||
|
||||
lockManager.releaseWifiAndCpu();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCompleted() {
|
||||
super.onCompleted();
|
||||
|
||||
setControlsOpacity(255);
|
||||
|
||||
resetNotification();
|
||||
if (bigNotRemoteView != null) bigNotRemoteView.setProgressBar(R.id.notificationProgressBar, 100, 100, false);
|
||||
if (notRemoteView != null) notRemoteView.setProgressBar(R.id.notificationProgressBar, 100, 100, false);
|
||||
if (bigNotRemoteView != null) {
|
||||
bigNotRemoteView.setProgressBar(R.id.notificationProgressBar, 100, 100, false);
|
||||
}
|
||||
if (notRemoteView != null) {
|
||||
notRemoteView.setProgressBar(R.id.notificationProgressBar, 100, 100, false);
|
||||
}
|
||||
updateNotificationThumbnail();
|
||||
updateNotification(R.drawable.ic_replay_white);
|
||||
|
||||
lockManager.releaseWifiAndCpu();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,16 +24,14 @@ import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.IntentFilter;
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.BitmapFactory;
|
||||
import android.media.AudioManager;
|
||||
import android.net.Uri;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.text.TextUtils;
|
||||
import android.util.Log;
|
||||
import android.view.View;
|
||||
import android.widget.Toast;
|
||||
|
||||
import com.google.android.exoplayer2.C;
|
||||
import com.google.android.exoplayer2.DefaultRenderersFactory;
|
||||
import com.google.android.exoplayer2.ExoPlaybackException;
|
||||
import com.google.android.exoplayer2.ExoPlayerFactory;
|
||||
@@ -46,31 +44,33 @@ import com.google.android.exoplayer2.Timeline;
|
||||
import com.google.android.exoplayer2.source.BehindLiveWindowException;
|
||||
import com.google.android.exoplayer2.source.MediaSource;
|
||||
import com.google.android.exoplayer2.source.TrackGroupArray;
|
||||
import com.google.android.exoplayer2.trackselection.AdaptiveTrackSelection;
|
||||
import com.google.android.exoplayer2.trackselection.TrackSelection;
|
||||
import com.google.android.exoplayer2.trackselection.TrackSelectionArray;
|
||||
import com.google.android.exoplayer2.upstream.DefaultBandwidthMeter;
|
||||
import com.google.android.exoplayer2.util.Util;
|
||||
import com.nostra13.universalimageloader.core.ImageLoader;
|
||||
import com.nostra13.universalimageloader.core.assist.FailReason;
|
||||
import com.nostra13.universalimageloader.core.listener.ImageLoadingListener;
|
||||
|
||||
import org.schabi.newpipe.BuildConfig;
|
||||
import org.schabi.newpipe.Downloader;
|
||||
import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.extractor.stream.StreamInfo;
|
||||
import org.schabi.newpipe.extractor.stream.StreamType;
|
||||
import org.schabi.newpipe.history.HistoryRecordManager;
|
||||
import org.schabi.newpipe.local.history.HistoryRecordManager;
|
||||
import org.schabi.newpipe.player.helper.AudioReactor;
|
||||
import org.schabi.newpipe.player.helper.LoadController;
|
||||
import org.schabi.newpipe.player.helper.MediaSessionManager;
|
||||
import org.schabi.newpipe.player.helper.PlayerDataSource;
|
||||
import org.schabi.newpipe.player.helper.PlayerHelper;
|
||||
import org.schabi.newpipe.player.mediasource.FailedMediaSource;
|
||||
import org.schabi.newpipe.player.playback.BasePlayerMediaSession;
|
||||
import org.schabi.newpipe.player.playback.CustomTrackSelector;
|
||||
import org.schabi.newpipe.player.playback.MediaSourceManager;
|
||||
import org.schabi.newpipe.player.playback.PlaybackListener;
|
||||
import org.schabi.newpipe.playlist.PlayQueue;
|
||||
import org.schabi.newpipe.playlist.PlayQueueAdapter;
|
||||
import org.schabi.newpipe.playlist.PlayQueueItem;
|
||||
import org.schabi.newpipe.player.playqueue.PlayQueue;
|
||||
import org.schabi.newpipe.player.playqueue.PlayQueueAdapter;
|
||||
import org.schabi.newpipe.player.playqueue.PlayQueueItem;
|
||||
import org.schabi.newpipe.player.resolver.MediaSourceTag;
|
||||
import org.schabi.newpipe.util.ImageDisplayConstants;
|
||||
import org.schabi.newpipe.util.SerializedCache;
|
||||
|
||||
import java.io.IOException;
|
||||
@@ -81,12 +81,12 @@ import io.reactivex.Observable;
|
||||
import io.reactivex.android.schedulers.AndroidSchedulers;
|
||||
import io.reactivex.disposables.CompositeDisposable;
|
||||
import io.reactivex.disposables.Disposable;
|
||||
import io.reactivex.disposables.SerialDisposable;
|
||||
|
||||
import static com.google.android.exoplayer2.Player.DISCONTINUITY_REASON_INTERNAL;
|
||||
import static com.google.android.exoplayer2.Player.DISCONTINUITY_REASON_PERIOD_TRANSITION;
|
||||
import static com.google.android.exoplayer2.Player.DISCONTINUITY_REASON_SEEK;
|
||||
import static com.google.android.exoplayer2.Player.DISCONTINUITY_REASON_SEEK_ADJUSTMENT;
|
||||
import static org.schabi.newpipe.player.helper.PlayerHelper.getTimeString;
|
||||
|
||||
/**
|
||||
* Base for the players, joining the common properties
|
||||
@@ -97,7 +97,7 @@ import static org.schabi.newpipe.player.helper.PlayerHelper.getTimeString;
|
||||
public abstract class BasePlayer implements
|
||||
Player.EventListener, PlaybackListener, ImageLoadingListener {
|
||||
|
||||
public static final boolean DEBUG = true;
|
||||
public static final boolean DEBUG = !BuildConfig.BUILD_TYPE.equals("release");
|
||||
@NonNull public static final String TAG = "BasePlayer";
|
||||
|
||||
@NonNull final protected Context context;
|
||||
@@ -107,56 +107,58 @@ public abstract class BasePlayer implements
|
||||
|
||||
@NonNull final protected HistoryRecordManager recordManager;
|
||||
|
||||
@NonNull final protected CustomTrackSelector trackSelector;
|
||||
@NonNull final protected PlayerDataSource dataSource;
|
||||
|
||||
@NonNull final private LoadControl loadControl;
|
||||
@NonNull final private RenderersFactory renderFactory;
|
||||
|
||||
@NonNull final private SerialDisposable progressUpdateReactor;
|
||||
@NonNull final private CompositeDisposable databaseUpdateReactor;
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Intent
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
public static final String REPEAT_MODE = "repeat_mode";
|
||||
public static final String PLAYBACK_PITCH = "playback_pitch";
|
||||
public static final String PLAYBACK_SPEED = "playback_speed";
|
||||
public static final String PLAYBACK_QUALITY = "playback_quality";
|
||||
public static final String PLAY_QUEUE_KEY = "play_queue_key";
|
||||
public static final String APPEND_ONLY = "append_only";
|
||||
public static final String SELECT_ON_APPEND = "select_on_append";
|
||||
@NonNull public static final String REPEAT_MODE = "repeat_mode";
|
||||
@NonNull public static final String PLAYBACK_PITCH = "playback_pitch";
|
||||
@NonNull public static final String PLAYBACK_SPEED = "playback_speed";
|
||||
@NonNull public static final String PLAYBACK_SKIP_SILENCE = "playback_skip_silence";
|
||||
@NonNull public static final String PLAYBACK_QUALITY = "playback_quality";
|
||||
@NonNull public static final String PLAY_QUEUE_KEY = "play_queue_key";
|
||||
@NonNull public static final String APPEND_ONLY = "append_only";
|
||||
@NonNull public static final String SELECT_ON_APPEND = "select_on_append";
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Playback
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
protected static final float[] PLAYBACK_SPEEDS = {0.5f, 0.75f, 1f, 1.25f, 1.5f, 1.75f, 2f};
|
||||
protected static final float[] PLAYBACK_PITCHES = {0.8f, 0.9f, 0.95f, 1f, 1.05f, 1.1f, 1.2f};
|
||||
|
||||
protected PlayQueue playQueue;
|
||||
protected PlayQueueAdapter playQueueAdapter;
|
||||
|
||||
protected MediaSourceManager playbackManager;
|
||||
@Nullable protected MediaSourceManager playbackManager;
|
||||
|
||||
protected StreamInfo currentInfo;
|
||||
protected PlayQueueItem currentItem;
|
||||
@Nullable private PlayQueueItem currentItem;
|
||||
@Nullable private MediaSourceTag currentMetadata;
|
||||
@Nullable private Bitmap currentThumbnail;
|
||||
|
||||
protected Toast errorToast;
|
||||
@Nullable protected Toast errorToast;
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Player
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
protected final static int FAST_FORWARD_REWIND_AMOUNT = 10000; // 10 Seconds
|
||||
protected final static int PLAY_PREV_ACTIVATION_LIMIT = 5000; // 5 seconds
|
||||
protected final static int PROGRESS_LOOP_INTERVAL = 500;
|
||||
protected final static int RECOVERY_SKIP_THRESHOLD = 3000; // 3 seconds
|
||||
|
||||
protected CustomTrackSelector trackSelector;
|
||||
protected PlayerDataSource dataSource;
|
||||
protected final static int FAST_FORWARD_REWIND_AMOUNT_MILLIS = 10000; // 10 Seconds
|
||||
protected final static int PLAY_PREV_ACTIVATION_LIMIT_MILLIS = 5000; // 5 seconds
|
||||
protected final static int PROGRESS_LOOP_INTERVAL_MILLIS = 500;
|
||||
protected final static int RECOVERY_SKIP_THRESHOLD_MILLIS = 3000; // 3 seconds
|
||||
|
||||
protected SimpleExoPlayer simpleExoPlayer;
|
||||
protected AudioReactor audioReactor;
|
||||
protected MediaSessionManager mediaSessionManager;
|
||||
|
||||
private boolean isPrepared = false;
|
||||
private boolean isSynchronizing = false;
|
||||
|
||||
protected Disposable progressUpdateReactor;
|
||||
protected CompositeDisposable databaseUpdateReactor;
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
@@ -171,40 +173,44 @@ public abstract class BasePlayer implements
|
||||
};
|
||||
this.intentFilter = new IntentFilter();
|
||||
setupBroadcastReceiver(intentFilter);
|
||||
context.registerReceiver(broadcastReceiver, intentFilter);
|
||||
|
||||
this.recordManager = new HistoryRecordManager(context);
|
||||
}
|
||||
|
||||
public void setup() {
|
||||
if (simpleExoPlayer == null) initPlayer();
|
||||
initListeners();
|
||||
}
|
||||
|
||||
public void initPlayer() {
|
||||
if (DEBUG) Log.d(TAG, "initPlayer() called with: context = [" + context + "]");
|
||||
|
||||
if (databaseUpdateReactor != null) databaseUpdateReactor.dispose();
|
||||
databaseUpdateReactor = new CompositeDisposable();
|
||||
this.progressUpdateReactor = new SerialDisposable();
|
||||
this.databaseUpdateReactor = new CompositeDisposable();
|
||||
|
||||
final String userAgent = Downloader.USER_AGENT;
|
||||
final DefaultBandwidthMeter bandwidthMeter = new DefaultBandwidthMeter();
|
||||
dataSource = new PlayerDataSource(context, userAgent, bandwidthMeter);
|
||||
this.dataSource = new PlayerDataSource(context, userAgent, bandwidthMeter);
|
||||
|
||||
final AdaptiveTrackSelection.Factory trackSelectionFactory =
|
||||
new AdaptiveTrackSelection.Factory(bandwidthMeter);
|
||||
trackSelector = new CustomTrackSelector(trackSelectionFactory);
|
||||
final TrackSelection.Factory trackSelectionFactory =
|
||||
PlayerHelper.getQualitySelector(context, bandwidthMeter);
|
||||
this.trackSelector = new CustomTrackSelector(trackSelectionFactory);
|
||||
|
||||
this.loadControl = new LoadController(context);
|
||||
this.renderFactory = new DefaultRenderersFactory(context);
|
||||
}
|
||||
|
||||
public void setup() {
|
||||
if (simpleExoPlayer == null) {
|
||||
initPlayer(/*playOnInit=*/true);
|
||||
}
|
||||
initListeners();
|
||||
}
|
||||
|
||||
public void initPlayer(final boolean playOnReady) {
|
||||
if (DEBUG) Log.d(TAG, "initPlayer() called with: context = [" + context + "]");
|
||||
|
||||
final LoadControl loadControl = new LoadController(context);
|
||||
final RenderersFactory renderFactory = new DefaultRenderersFactory(context);
|
||||
simpleExoPlayer = ExoPlayerFactory.newSimpleInstance(renderFactory, trackSelector, loadControl);
|
||||
simpleExoPlayer.addListener(this);
|
||||
simpleExoPlayer.setPlayWhenReady(true);
|
||||
simpleExoPlayer.setPlayWhenReady(playOnReady);
|
||||
simpleExoPlayer.setSeekParameters(PlayerHelper.getSeekParameters(context));
|
||||
|
||||
audioReactor = new AudioReactor(context, simpleExoPlayer);
|
||||
mediaSessionManager = new MediaSessionManager(context, simpleExoPlayer,
|
||||
new BasePlayerMediaSession(this));
|
||||
|
||||
registerBroadcastReceiver();
|
||||
}
|
||||
|
||||
public void initListeners() {}
|
||||
@@ -235,19 +241,24 @@ public abstract class BasePlayer implements
|
||||
final int repeatMode = intent.getIntExtra(REPEAT_MODE, getRepeatMode());
|
||||
final float playbackSpeed = intent.getFloatExtra(PLAYBACK_SPEED, getPlaybackSpeed());
|
||||
final float playbackPitch = intent.getFloatExtra(PLAYBACK_PITCH, getPlaybackPitch());
|
||||
final boolean playbackSkipSilence = intent.getBooleanExtra(PLAYBACK_SKIP_SILENCE,
|
||||
getPlaybackSkipSilence());
|
||||
|
||||
// Good to go...
|
||||
initPlayback(queue, repeatMode, playbackSpeed, playbackPitch);
|
||||
initPlayback(queue, repeatMode, playbackSpeed, playbackPitch, playbackSkipSilence,
|
||||
/*playOnInit=*/true);
|
||||
}
|
||||
|
||||
protected void initPlayback(@NonNull final PlayQueue queue,
|
||||
@Player.RepeatMode final int repeatMode,
|
||||
final float playbackSpeed,
|
||||
final float playbackPitch) {
|
||||
final float playbackPitch,
|
||||
final boolean playbackSkipSilence,
|
||||
final boolean playOnReady) {
|
||||
destroyPlayer();
|
||||
initPlayer();
|
||||
initPlayer(playOnReady);
|
||||
setRepeatMode(repeatMode);
|
||||
setPlaybackParameters(playbackSpeed, playbackPitch);
|
||||
setPlaybackParameters(playbackSpeed, playbackPitch, playbackSkipSilence);
|
||||
|
||||
playQueue = queue;
|
||||
playQueue.init();
|
||||
@@ -269,7 +280,7 @@ public abstract class BasePlayer implements
|
||||
if (playQueue != null) playQueue.dispose();
|
||||
if (audioReactor != null) audioReactor.dispose();
|
||||
if (playbackManager != null) playbackManager.dispose();
|
||||
if (databaseUpdateReactor != null) databaseUpdateReactor.dispose();
|
||||
if (mediaSessionManager != null) mediaSessionManager.dispose();
|
||||
|
||||
if (playQueueAdapter != null) {
|
||||
playQueueAdapter.unsetSelectedListener();
|
||||
@@ -282,20 +293,22 @@ public abstract class BasePlayer implements
|
||||
destroyPlayer();
|
||||
unregisterBroadcastReceiver();
|
||||
|
||||
trackSelector = null;
|
||||
databaseUpdateReactor.clear();
|
||||
progressUpdateReactor.set(null);
|
||||
|
||||
simpleExoPlayer = null;
|
||||
mediaSessionManager = null;
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Thumbnail Loading
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
public void initThumbnail(final String url) {
|
||||
private void initThumbnail(final String url) {
|
||||
if (DEBUG) Log.d(TAG, "Thumbnail - initThumbnail() called");
|
||||
if (url == null || url.isEmpty()) return;
|
||||
ImageLoader.getInstance().resume();
|
||||
ImageLoader.getInstance().loadImage(url, this);
|
||||
ImageLoader.getInstance().loadImage(url, ImageDisplayConstants.DISPLAY_THUMBNAIL_OPTIONS,
|
||||
this);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -308,6 +321,7 @@ public abstract class BasePlayer implements
|
||||
public void onLoadingFailed(String imageUri, View view, FailReason failReason) {
|
||||
Log.e(TAG, "Thumbnail - onLoadingFailed() called on imageUri = [" + imageUri + "]",
|
||||
failReason.getCause());
|
||||
currentThumbnail = null;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -315,64 +329,14 @@ public abstract class BasePlayer implements
|
||||
if (DEBUG) Log.d(TAG, "Thumbnail - onLoadingComplete() called with: " +
|
||||
"imageUri = [" + imageUri + "], view = [" + view + "], " +
|
||||
"loadedImage = [" + loadedImage + "]");
|
||||
currentThumbnail = loadedImage;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLoadingCancelled(String imageUri, View view) {
|
||||
if (DEBUG) Log.d(TAG, "Thumbnail - onLoadingCancelled() called with: " +
|
||||
"imageUri = [" + imageUri + "], view = [" + view + "]");
|
||||
}
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// MediaSource Building
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
public MediaSource buildLiveMediaSource(@NonNull final String sourceUrl,
|
||||
@C.ContentType final int type) {
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "buildLiveMediaSource() called with: url = [" + sourceUrl +
|
||||
"], content type = [" + type + "]");
|
||||
}
|
||||
if (dataSource == null) return null;
|
||||
|
||||
final Uri uri = Uri.parse(sourceUrl);
|
||||
switch (type) {
|
||||
case C.TYPE_SS:
|
||||
return dataSource.getLiveSsMediaSourceFactory().createMediaSource(uri);
|
||||
case C.TYPE_DASH:
|
||||
return dataSource.getLiveDashMediaSourceFactory().createMediaSource(uri);
|
||||
case C.TYPE_HLS:
|
||||
return dataSource.getLiveHlsMediaSourceFactory().createMediaSource(uri);
|
||||
default:
|
||||
throw new IllegalStateException("Unsupported type: " + type);
|
||||
}
|
||||
}
|
||||
|
||||
public MediaSource buildMediaSource(@NonNull final String sourceUrl,
|
||||
@NonNull final String cacheKey,
|
||||
@NonNull final String overrideExtension) {
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "buildMediaSource() called with: url = [" + sourceUrl +
|
||||
"], cacheKey = [" + cacheKey + "]" +
|
||||
"], overrideExtension = [" + overrideExtension + "]");
|
||||
}
|
||||
if (dataSource == null) return null;
|
||||
|
||||
final Uri uri = Uri.parse(sourceUrl);
|
||||
@C.ContentType final int type = TextUtils.isEmpty(overrideExtension) ?
|
||||
Util.inferContentType(uri) : Util.inferContentType("." + overrideExtension);
|
||||
|
||||
switch (type) {
|
||||
case C.TYPE_SS:
|
||||
return dataSource.getLiveSsMediaSourceFactory().createMediaSource(uri);
|
||||
case C.TYPE_DASH:
|
||||
return dataSource.getDashMediaSourceFactory().createMediaSource(uri);
|
||||
case C.TYPE_HLS:
|
||||
return dataSource.getHlsMediaSourceFactory().createMediaSource(uri);
|
||||
case C.TYPE_OTHER:
|
||||
return dataSource.getExtractorMediaSourceFactory(cacheKey).createMediaSource(uri);
|
||||
default:
|
||||
throw new IllegalStateException("Unsupported type: " + type);
|
||||
}
|
||||
currentThumbnail = null;
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
@@ -397,11 +361,17 @@ public abstract class BasePlayer implements
|
||||
}
|
||||
}
|
||||
|
||||
public void unregisterBroadcastReceiver() {
|
||||
protected void registerBroadcastReceiver() {
|
||||
// Try to unregister current first
|
||||
unregisterBroadcastReceiver();
|
||||
context.registerReceiver(broadcastReceiver, intentFilter);
|
||||
}
|
||||
|
||||
protected void unregisterBroadcastReceiver() {
|
||||
try {
|
||||
context.unregisterReceiver(broadcastReceiver);
|
||||
} catch (final IllegalArgumentException unregisteredException) {
|
||||
Log.e(TAG, "Broadcast receiver already unregistered.", unregisteredException);
|
||||
Log.w(TAG, "Broadcast receiver already unregistered (" + unregisteredException.getMessage() + ")");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -508,27 +478,27 @@ public abstract class BasePlayer implements
|
||||
public abstract void onUpdateProgress(int currentProgress, int duration, int bufferPercent);
|
||||
|
||||
protected void startProgressLoop() {
|
||||
if (progressUpdateReactor != null) progressUpdateReactor.dispose();
|
||||
progressUpdateReactor = getProgressReactor();
|
||||
progressUpdateReactor.set(getProgressReactor());
|
||||
}
|
||||
|
||||
protected void stopProgressLoop() {
|
||||
if (progressUpdateReactor != null) progressUpdateReactor.dispose();
|
||||
progressUpdateReactor = null;
|
||||
progressUpdateReactor.set(null);
|
||||
}
|
||||
|
||||
public void triggerProgressUpdate() {
|
||||
if (simpleExoPlayer == null) return;
|
||||
onUpdateProgress(
|
||||
(int) simpleExoPlayer.getCurrentPosition(),
|
||||
Math.max((int) simpleExoPlayer.getCurrentPosition(), 0),
|
||||
(int) simpleExoPlayer.getDuration(),
|
||||
simpleExoPlayer.getBufferedPercentage()
|
||||
);
|
||||
}
|
||||
|
||||
private Disposable getProgressReactor() {
|
||||
return Observable.interval(PROGRESS_LOOP_INTERVAL, TimeUnit.MILLISECONDS)
|
||||
return Observable.interval(PROGRESS_LOOP_INTERVAL_MILLIS, TimeUnit.MILLISECONDS)
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(ignored -> triggerProgressUpdate());
|
||||
.subscribe(ignored -> triggerProgressUpdate(),
|
||||
error -> Log.e(TAG, "Progress update failure: ", error));
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
@@ -542,28 +512,16 @@ public abstract class BasePlayer implements
|
||||
(manifest == null ? "no manifest" : "available manifest") + ", " +
|
||||
"timeline size = [" + timeline.getWindowCount() + "], " +
|
||||
"reason = [" + reason + "]");
|
||||
if (playQueue == null) return;
|
||||
|
||||
switch (reason) {
|
||||
case Player.TIMELINE_CHANGE_REASON_RESET: // called after #block
|
||||
case Player.TIMELINE_CHANGE_REASON_PREPARED: // called after #unblock
|
||||
case Player.TIMELINE_CHANGE_REASON_DYNAMIC: // called after playlist changes
|
||||
// Ensures MediaSourceManager#update is complete
|
||||
final boolean isPlaylistStable = timeline.getWindowCount() == playQueue.size();
|
||||
// Ensure dynamic/livestream timeline changes does not cause negative position
|
||||
if (isPlaylistStable && !isCurrentWindowValid() && !isSynchronizing) {
|
||||
if (DEBUG) Log.d(TAG, "Playback - negative time position reached, " +
|
||||
"clamping position to 0ms.");
|
||||
seekTo(/*clampToTime=*/0);
|
||||
}
|
||||
break;
|
||||
}
|
||||
maybeUpdateCurrentMetadata();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTracksChanged(TrackGroupArray trackGroups, TrackSelectionArray trackSelections) {
|
||||
if (DEBUG) Log.d(TAG, "ExoPlayer - onTracksChanged(), " +
|
||||
"track group size = " + trackGroups.length);
|
||||
|
||||
maybeUpdateCurrentMetadata();
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -583,6 +541,8 @@ public abstract class BasePlayer implements
|
||||
} else if (isLoading && !isProgressLoopRunning()) {
|
||||
startProgressLoop();
|
||||
}
|
||||
|
||||
maybeUpdateCurrentMetadata();
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -606,6 +566,7 @@ public abstract class BasePlayer implements
|
||||
}
|
||||
break;
|
||||
case Player.STATE_READY: //3
|
||||
maybeUpdateCurrentMetadata();
|
||||
maybeCorrectSeekPosition();
|
||||
if (!isPrepared) {
|
||||
isPrepared = true;
|
||||
@@ -622,38 +583,19 @@ public abstract class BasePlayer implements
|
||||
}
|
||||
|
||||
private void maybeCorrectSeekPosition() {
|
||||
if (playQueue == null || simpleExoPlayer == null || currentInfo == null) return;
|
||||
if (playQueue == null || simpleExoPlayer == null || currentMetadata == null) return;
|
||||
|
||||
final int currentSourceIndex = playQueue.getIndex();
|
||||
final PlayQueueItem currentSourceItem = playQueue.getItem();
|
||||
if (currentSourceItem == null) return;
|
||||
|
||||
final long recoveryPositionMillis = currentSourceItem.getRecoveryPosition();
|
||||
final boolean isCurrentWindowCorrect =
|
||||
simpleExoPlayer.getCurrentPeriodIndex() == currentSourceIndex;
|
||||
final StreamInfo currentInfo = currentMetadata.getMetadata();
|
||||
final long presetStartPositionMillis = currentInfo.getStartPosition() * 1000;
|
||||
|
||||
if (recoveryPositionMillis != PlayQueueItem.RECOVERY_UNSET && isCurrentWindowCorrect) {
|
||||
// Is recovering previous playback?
|
||||
if (DEBUG) Log.d(TAG, "Playback - Rewinding to recovery time=" +
|
||||
"[" + getTimeString((int)recoveryPositionMillis) + "]");
|
||||
seekTo(recoveryPositionMillis);
|
||||
playQueue.unsetRecovery(currentSourceIndex);
|
||||
|
||||
} else if (isSynchronizing && simpleExoPlayer.isCurrentWindowDynamic()) {
|
||||
if (DEBUG) Log.d(TAG, "Playback - Synchronizing livestream to default time");
|
||||
// Is still synchronizing?
|
||||
seekToDefault();
|
||||
|
||||
} else if (isSynchronizing && presetStartPositionMillis != 0L) {
|
||||
if (presetStartPositionMillis > 0L) {
|
||||
// Has another start position?
|
||||
if (DEBUG) Log.d(TAG, "Playback - Seeking to preset start " +
|
||||
"position=[" + presetStartPositionMillis + "]");
|
||||
// Has another start position?
|
||||
seekTo(presetStartPositionMillis);
|
||||
currentInfo.setStartPosition(0);
|
||||
}
|
||||
|
||||
isSynchronizing = false;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -700,41 +642,23 @@ public abstract class BasePlayer implements
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Processes {@link ExoPlaybackException} tagged with {@link ExoPlaybackException#TYPE_SOURCE}.
|
||||
* <br><br>
|
||||
* If the current {@link com.google.android.exoplayer2.Timeline.Window window} is valid,
|
||||
* then we know the error is produced by transitioning into a bad window, therefore we report
|
||||
* an error to the play queue based on if the current error can be skipped.
|
||||
* <br><br>
|
||||
* This is done because ExoPlayer reports the source exceptions before window is
|
||||
* transitioned on seamless playback. Because player error causes ExoPlayer to go
|
||||
* back to {@link Player#STATE_IDLE STATE_IDLE}, we reset and prepare the media source
|
||||
* again to resume playback.
|
||||
* <br><br>
|
||||
* In the event that this error is produced during a valid stream playback, we save the
|
||||
* current position so the playback may be recovered and resumed manually by the user. This
|
||||
* happens only if the playback is {@link #RECOVERY_SKIP_THRESHOLD} milliseconds until complete.
|
||||
* <br><br>
|
||||
* In the event of livestreaming being lagged behind for any reason, most notably pausing for
|
||||
* too long, a {@link BehindLiveWindowException} will be produced. This will trigger a reload
|
||||
* instead of skipping or removal.
|
||||
* */
|
||||
private void processSourceError(final IOException error) {
|
||||
if (simpleExoPlayer == null || playQueue == null) return;
|
||||
|
||||
if (simpleExoPlayer.getCurrentPosition() <
|
||||
simpleExoPlayer.getDuration() - RECOVERY_SKIP_THRESHOLD) {
|
||||
setRecovery();
|
||||
}
|
||||
setRecovery();
|
||||
|
||||
final Throwable cause = error.getCause();
|
||||
if (cause instanceof BehindLiveWindowException) {
|
||||
if (error instanceof BehindLiveWindowException) {
|
||||
reload();
|
||||
} else if (cause instanceof UnknownHostException) {
|
||||
playQueue.error(/*isNetworkProblem=*/true);
|
||||
} else if (isCurrentWindowValid()) {
|
||||
playQueue.error(/*isTransitioningToBadStream=*/true);
|
||||
} else if (cause instanceof FailedMediaSource.MediaSourceResolutionException) {
|
||||
playQueue.error(/*recoverableWithNoAvailableStream=*/false);
|
||||
} else if (cause instanceof FailedMediaSource.StreamInfoLoadException) {
|
||||
playQueue.error(/*recoverableIfLoadFailsWhenNetworkIsFine=*/false);
|
||||
} else {
|
||||
playQueue.error(isCurrentWindowValid());
|
||||
playQueue.error(/*noIdeaWhatHappenedAndLetUserChooseWhatToDo=*/true);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -742,22 +666,29 @@ public abstract class BasePlayer implements
|
||||
public void onPositionDiscontinuity(@Player.DiscontinuityReason final int reason) {
|
||||
if (DEBUG) Log.d(TAG, "ExoPlayer - onPositionDiscontinuity() called with " +
|
||||
"reason = [" + reason + "]");
|
||||
// Refresh the playback if there is a transition to the next video
|
||||
final int newPeriodIndex = simpleExoPlayer.getCurrentPeriodIndex();
|
||||
if (playQueue == null) return;
|
||||
|
||||
/* Discontinuity reasons!! Thank you ExoPlayer lords */
|
||||
// Refresh the playback if there is a transition to the next video
|
||||
final int newWindowIndex = simpleExoPlayer.getCurrentWindowIndex();
|
||||
switch (reason) {
|
||||
case DISCONTINUITY_REASON_PERIOD_TRANSITION:
|
||||
if (newPeriodIndex == playQueue.getIndex()) {
|
||||
// When player is in single repeat mode and a period transition occurs,
|
||||
// we need to register a view count here since no metadata has changed
|
||||
if (getRepeatMode() == Player.REPEAT_MODE_ONE &&
|
||||
newWindowIndex == playQueue.getIndex()) {
|
||||
registerView();
|
||||
} else {
|
||||
playQueue.offsetIndex(+1);
|
||||
break;
|
||||
}
|
||||
case DISCONTINUITY_REASON_SEEK:
|
||||
case DISCONTINUITY_REASON_SEEK_ADJUSTMENT:
|
||||
case DISCONTINUITY_REASON_INTERNAL:
|
||||
if (playQueue.getIndex() != newWindowIndex) {
|
||||
playQueue.setIndex(newWindowIndex);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
maybeUpdateCurrentMetadata();
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -787,9 +718,10 @@ public abstract class BasePlayer implements
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
@Override
|
||||
public boolean isNearPlaybackEdge(final long timeToEndMillis) {
|
||||
public boolean isApproachingPlaybackEdge(final long timeToEndMillis) {
|
||||
// If live, then not near playback edge
|
||||
if (simpleExoPlayer == null || simpleExoPlayer.isCurrentWindowDynamic()) return false;
|
||||
// If not playing, then not approaching playback edge
|
||||
if (simpleExoPlayer == null || isLive() || !isPlaying()) return false;
|
||||
|
||||
final long currentPositionMillis = simpleExoPlayer.getCurrentPosition();
|
||||
final long currentDurationMillis = simpleExoPlayer.getDuration();
|
||||
@@ -802,7 +734,7 @@ public abstract class BasePlayer implements
|
||||
if (DEBUG) Log.d(TAG, "Playback - onPlaybackBlock() called");
|
||||
|
||||
currentItem = null;
|
||||
currentInfo = null;
|
||||
currentMetadata = null;
|
||||
simpleExoPlayer.stop();
|
||||
isPrepared = false;
|
||||
|
||||
@@ -819,42 +751,21 @@ public abstract class BasePlayer implements
|
||||
simpleExoPlayer.prepare(mediaSource);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPlaybackSynchronize(@NonNull final PlayQueueItem item,
|
||||
@Nullable final StreamInfo info) {
|
||||
public void onPlaybackSynchronize(@NonNull final PlayQueueItem item) {
|
||||
if (DEBUG) Log.d(TAG, "Playback - onPlaybackSynchronize() called with " +
|
||||
(info != null ? "available" : "null") + " info, " +
|
||||
"item=[" + item.getTitle() + "], url=[" + item.getUrl() + "]");
|
||||
if (simpleExoPlayer == null || playQueue == null) return;
|
||||
|
||||
final boolean onPlaybackInitial = currentItem == null;
|
||||
final boolean hasPlayQueueItemChanged = currentItem != item;
|
||||
final boolean hasStreamInfoChanged = currentInfo != info;
|
||||
|
||||
final int currentPlayQueueIndex = playQueue.indexOf(item);
|
||||
final int currentPlaylistIndex = simpleExoPlayer.getCurrentWindowIndex();
|
||||
final int currentPlaylistSize = simpleExoPlayer.getCurrentTimeline().getWindowCount();
|
||||
|
||||
// when starting playback on the last item when not repeating, maybe auto queue
|
||||
if (info != null && currentPlayQueueIndex == playQueue.size() - 1 &&
|
||||
getRepeatMode() == Player.REPEAT_MODE_OFF &&
|
||||
PlayerHelper.isAutoQueueEnabled(context)) {
|
||||
final PlayQueue autoQueue = PlayerHelper.autoQueueOf(info, playQueue.getStreams());
|
||||
if (autoQueue != null) playQueue.append(autoQueue.getStreams());
|
||||
}
|
||||
// If nothing to synchronize
|
||||
if (!hasPlayQueueItemChanged && !hasStreamInfoChanged) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!hasPlayQueueItemChanged) return;
|
||||
currentItem = item;
|
||||
currentInfo = info;
|
||||
if (hasPlayQueueItemChanged) {
|
||||
// updates only to the stream info should not trigger another view count
|
||||
registerView();
|
||||
initThumbnail(info == null ? item.getThumbnailUrl() : info.getThumbnailUrl());
|
||||
}
|
||||
onMetadataChanged(item, info, currentPlayQueueIndex, hasPlayQueueItemChanged);
|
||||
|
||||
// Check if on wrong window
|
||||
if (currentPlayQueueIndex != playQueue.getIndex()) {
|
||||
@@ -869,39 +780,29 @@ public abstract class BasePlayer implements
|
||||
"index=[" + currentPlayQueueIndex + "] with " +
|
||||
"playlist length=[" + currentPlaylistSize + "]");
|
||||
|
||||
// If not playing correct stream, change window position and sets flag
|
||||
// for synchronizing once window position is corrected
|
||||
// @see maybeCorrectSeekPosition()
|
||||
} else if (currentPlaylistIndex != currentPlayQueueIndex || onPlaybackInitial ||
|
||||
!isPlaying()) {
|
||||
if (DEBUG) Log.d(TAG, "Playback - Rewinding to correct" +
|
||||
" index=[" + currentPlayQueueIndex + "]," +
|
||||
" from=[" + currentPlaylistIndex + "], size=[" + currentPlaylistSize + "].");
|
||||
isSynchronizing = true;
|
||||
simpleExoPlayer.seekToDefaultPosition(currentPlayQueueIndex);
|
||||
|
||||
if (item.getRecoveryPosition() != PlayQueueItem.RECOVERY_UNSET) {
|
||||
simpleExoPlayer.seekTo(currentPlayQueueIndex, item.getRecoveryPosition());
|
||||
playQueue.unsetRecovery(currentPlayQueueIndex);
|
||||
} else {
|
||||
simpleExoPlayer.seekToDefaultPosition(currentPlayQueueIndex);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
abstract protected void onMetadataChanged(@NonNull final PlayQueueItem item,
|
||||
@Nullable final StreamInfo info,
|
||||
final int newPlayQueueIndex,
|
||||
final boolean hasPlayQueueItemChanged);
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public MediaSource sourceOf(PlayQueueItem item, StreamInfo info) {
|
||||
final StreamType streamType = info.getStreamType();
|
||||
if (!(streamType == StreamType.AUDIO_LIVE_STREAM || streamType == StreamType.LIVE_STREAM)) {
|
||||
return null;
|
||||
protected void onMetadataChanged(@NonNull final MediaSourceTag tag) {
|
||||
final StreamInfo info = tag.getMetadata();
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "Playback - onMetadataChanged() called, playing: " + info.getName());
|
||||
}
|
||||
|
||||
if (!info.getHlsUrl().isEmpty()) {
|
||||
return buildLiveMediaSource(info.getHlsUrl(), C.TYPE_HLS);
|
||||
} else if (!info.getDashMpdUrl().isEmpty()) {
|
||||
return buildLiveMediaSource(info.getDashMpdUrl(), C.TYPE_DASH);
|
||||
}
|
||||
|
||||
return null;
|
||||
initThumbnail(info.getThumbnailUrl());
|
||||
registerView();
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -985,22 +886,22 @@ public abstract class BasePlayer implements
|
||||
|
||||
public void onFastRewind() {
|
||||
if (DEBUG) Log.d(TAG, "onFastRewind() called");
|
||||
seekBy(-FAST_FORWARD_REWIND_AMOUNT);
|
||||
seekBy(-FAST_FORWARD_REWIND_AMOUNT_MILLIS);
|
||||
}
|
||||
|
||||
public void onFastForward() {
|
||||
if (DEBUG) Log.d(TAG, "onFastForward() called");
|
||||
seekBy(FAST_FORWARD_REWIND_AMOUNT);
|
||||
seekBy(FAST_FORWARD_REWIND_AMOUNT_MILLIS);
|
||||
}
|
||||
|
||||
public void onPlayPrevious() {
|
||||
if (simpleExoPlayer == null || playQueue == null) return;
|
||||
if (DEBUG) Log.d(TAG, "onPlayPrevious() called");
|
||||
|
||||
/* If current playback has run for PLAY_PREV_ACTIVATION_LIMIT milliseconds,
|
||||
/* If current playback has run for PLAY_PREV_ACTIVATION_LIMIT_MILLIS milliseconds,
|
||||
* restart current track. Also restart the track if the current track
|
||||
* is the first in a queue.*/
|
||||
if (simpleExoPlayer.getCurrentPosition() > PLAY_PREV_ACTIVATION_LIMIT ||
|
||||
if (simpleExoPlayer.getCurrentPosition() > PLAY_PREV_ACTIVATION_LIMIT_MILLIS ||
|
||||
playQueue.getIndex() == 0) {
|
||||
seekToDefault();
|
||||
playQueue.offsetIndex(0);
|
||||
@@ -1034,9 +935,7 @@ public abstract class BasePlayer implements
|
||||
|
||||
public void seekTo(long positionMillis) {
|
||||
if (DEBUG) Log.d(TAG, "seekBy() called with: position = [" + positionMillis + "]");
|
||||
if (simpleExoPlayer == null || positionMillis < 0 ||
|
||||
positionMillis > simpleExoPlayer.getDuration()) return;
|
||||
simpleExoPlayer.seekTo(positionMillis);
|
||||
if (simpleExoPlayer != null) simpleExoPlayer.seekTo(positionMillis);
|
||||
}
|
||||
|
||||
public void seekBy(long offsetMillis) {
|
||||
@@ -1050,7 +949,9 @@ public abstract class BasePlayer implements
|
||||
}
|
||||
|
||||
public void seekToDefault() {
|
||||
if (simpleExoPlayer != null) simpleExoPlayer.seekToDefaultPosition();
|
||||
if (simpleExoPlayer != null) {
|
||||
simpleExoPlayer.seekToDefaultPosition();
|
||||
}
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
@@ -1058,12 +959,14 @@ public abstract class BasePlayer implements
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
private void registerView() {
|
||||
if (databaseUpdateReactor == null || currentInfo == null) return;
|
||||
databaseUpdateReactor.add(recordManager.onViewed(currentInfo).onErrorComplete()
|
||||
if (currentMetadata == null) return;
|
||||
final StreamInfo currentInfo = currentMetadata.getMetadata();
|
||||
final Disposable viewRegister = recordManager.onViewed(currentInfo).onErrorComplete()
|
||||
.subscribe(
|
||||
ignored -> {/* successful */},
|
||||
error -> Log.e(TAG, "Player onViewed() failure: ", error)
|
||||
));
|
||||
);
|
||||
databaseUpdateReactor.add(viewRegister);
|
||||
}
|
||||
|
||||
protected void reload() {
|
||||
@@ -1077,7 +980,7 @@ public abstract class BasePlayer implements
|
||||
}
|
||||
|
||||
protected void savePlaybackState(final StreamInfo info, final long progress) {
|
||||
if (info == null || databaseUpdateReactor == null) return;
|
||||
if (info == null) return;
|
||||
final Disposable stateSaver = recordManager.saveStreamState(info, progress)
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.onErrorComplete()
|
||||
@@ -1089,14 +992,45 @@ public abstract class BasePlayer implements
|
||||
}
|
||||
|
||||
private void savePlaybackState() {
|
||||
if (simpleExoPlayer == null || currentInfo == null) return;
|
||||
if (simpleExoPlayer == null || currentMetadata == null) return;
|
||||
final StreamInfo currentInfo = currentMetadata.getMetadata();
|
||||
|
||||
if (simpleExoPlayer.getCurrentPosition() > RECOVERY_SKIP_THRESHOLD &&
|
||||
if (simpleExoPlayer.getCurrentPosition() > RECOVERY_SKIP_THRESHOLD_MILLIS &&
|
||||
simpleExoPlayer.getCurrentPosition() <
|
||||
simpleExoPlayer.getDuration() - RECOVERY_SKIP_THRESHOLD) {
|
||||
simpleExoPlayer.getDuration() - RECOVERY_SKIP_THRESHOLD_MILLIS) {
|
||||
savePlaybackState(currentInfo, simpleExoPlayer.getCurrentPosition());
|
||||
}
|
||||
}
|
||||
|
||||
private void maybeUpdateCurrentMetadata() {
|
||||
if (simpleExoPlayer == null) return;
|
||||
|
||||
final MediaSourceTag metadata;
|
||||
try {
|
||||
metadata = (MediaSourceTag) simpleExoPlayer.getCurrentTag();
|
||||
} catch (IndexOutOfBoundsException | ClassCastException error) {
|
||||
if(DEBUG) Log.d(TAG, "Could not update metadata: " + error.getMessage());
|
||||
if(DEBUG) error.printStackTrace();
|
||||
return;
|
||||
}
|
||||
|
||||
if (metadata == null) return;
|
||||
maybeAutoQueueNextStream(metadata);
|
||||
|
||||
if (currentMetadata == metadata) return;
|
||||
currentMetadata = metadata;
|
||||
onMetadataChanged(metadata);
|
||||
}
|
||||
|
||||
private void maybeAutoQueueNextStream(@NonNull final MediaSourceTag currentMetadata) {
|
||||
if (playQueue == null || playQueue.getIndex() != playQueue.size() - 1 ||
|
||||
getRepeatMode() != Player.REPEAT_MODE_OFF ||
|
||||
!PlayerHelper.isAutoQueueEnabled(context)) return;
|
||||
// auto queue when starting playback on the last item when not repeating
|
||||
final PlayQueue autoQueue = PlayerHelper.autoQueueOf(currentMetadata.getMetadata(),
|
||||
playQueue.getStreams());
|
||||
if (autoQueue != null) playQueue.append(autoQueue.getStreams());
|
||||
}
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Getters and Setters
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
@@ -1113,23 +1047,37 @@ public abstract class BasePlayer implements
|
||||
return currentState;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public MediaSourceTag getCurrentMetadata() {
|
||||
return currentMetadata;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public String getVideoUrl() {
|
||||
return currentItem == null ? context.getString(R.string.unknown_content) : currentItem.getUrl();
|
||||
return currentMetadata == null ? context.getString(R.string.unknown_content) : currentMetadata.getMetadata().getUrl();
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public String getVideoTitle() {
|
||||
return currentItem == null ? context.getString(R.string.unknown_content) : currentItem.getTitle();
|
||||
return currentMetadata == null ? context.getString(R.string.unknown_content) : currentMetadata.getMetadata().getName();
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public String getUploaderName() {
|
||||
return currentItem == null ? context.getString(R.string.unknown_content) : currentItem.getUploader();
|
||||
return currentMetadata == null ? context.getString(R.string.unknown_content) : currentMetadata.getMetadata().getUploaderName();
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public Bitmap getThumbnail() {
|
||||
return currentThumbnail == null ?
|
||||
BitmapFactory.decodeResource(context.getResources(), R.drawable.dummy_thumbnail) :
|
||||
currentThumbnail;
|
||||
}
|
||||
|
||||
/** Checks if the current playback is a livestream AND is playing at or beyond the live edge */
|
||||
@SuppressWarnings("BooleanMethodIsAlwaysInverted")
|
||||
public boolean isLiveEdge() {
|
||||
if (simpleExoPlayer == null) return false;
|
||||
final boolean isLive = simpleExoPlayer.isCurrentWindowDynamic();
|
||||
if (!isLive) return false;
|
||||
if (simpleExoPlayer == null || !isLive()) return false;
|
||||
|
||||
final Timeline currentTimeline = simpleExoPlayer.getCurrentTimeline();
|
||||
final int currentWindowIndex = simpleExoPlayer.getCurrentWindowIndex();
|
||||
@@ -1143,6 +1091,19 @@ public abstract class BasePlayer implements
|
||||
return timelineWindow.getDefaultPositionMs() <= simpleExoPlayer.getCurrentPosition();
|
||||
}
|
||||
|
||||
public boolean isLive() {
|
||||
if (simpleExoPlayer == null) return false;
|
||||
try {
|
||||
return simpleExoPlayer.isCurrentWindowDynamic();
|
||||
} catch (@NonNull IndexOutOfBoundsException ignored) {
|
||||
// Why would this even happen =(
|
||||
// But lets log it anyway. Save is save
|
||||
if(DEBUG) Log.d(TAG, "Could not update metadata: " + ignored.getMessage());
|
||||
if(DEBUG) ignored.printStackTrace();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public boolean isPlaying() {
|
||||
final int state = simpleExoPlayer.getPlaybackState();
|
||||
return (state == Player.STATE_READY || state == Player.STATE_BUFFERING)
|
||||
@@ -1151,11 +1112,11 @@ public abstract class BasePlayer implements
|
||||
|
||||
@Player.RepeatMode
|
||||
public int getRepeatMode() {
|
||||
return simpleExoPlayer.getRepeatMode();
|
||||
return simpleExoPlayer == null ? Player.REPEAT_MODE_OFF : simpleExoPlayer.getRepeatMode();
|
||||
}
|
||||
|
||||
public void setRepeatMode(@Player.RepeatMode final int repeatMode) {
|
||||
simpleExoPlayer.setRepeatMode(repeatMode);
|
||||
if (simpleExoPlayer != null) simpleExoPlayer.setRepeatMode(repeatMode);
|
||||
}
|
||||
|
||||
public float getPlaybackSpeed() {
|
||||
@@ -1166,23 +1127,22 @@ public abstract class BasePlayer implements
|
||||
return getPlaybackParameters().pitch;
|
||||
}
|
||||
|
||||
public void setPlaybackSpeed(float speed) {
|
||||
setPlaybackParameters(speed, getPlaybackPitch());
|
||||
public boolean getPlaybackSkipSilence() {
|
||||
return getPlaybackParameters().skipSilence;
|
||||
}
|
||||
|
||||
public void setPlaybackPitch(float pitch) {
|
||||
setPlaybackParameters(getPlaybackSpeed(), pitch);
|
||||
public void setPlaybackSpeed(float speed) {
|
||||
setPlaybackParameters(speed, getPlaybackPitch(), getPlaybackSkipSilence());
|
||||
}
|
||||
|
||||
public PlaybackParameters getPlaybackParameters() {
|
||||
final PlaybackParameters defaultParameters = new PlaybackParameters(1f, 1f);
|
||||
if (simpleExoPlayer == null) return defaultParameters;
|
||||
if (simpleExoPlayer == null) return PlaybackParameters.DEFAULT;
|
||||
final PlaybackParameters parameters = simpleExoPlayer.getPlaybackParameters();
|
||||
return parameters == null ? defaultParameters : parameters;
|
||||
return parameters == null ? PlaybackParameters.DEFAULT : parameters;
|
||||
}
|
||||
|
||||
public void setPlaybackParameters(float speed, float pitch) {
|
||||
simpleExoPlayer.setPlaybackParameters(new PlaybackParameters(speed, pitch));
|
||||
public void setPlaybackParameters(float speed, float pitch, boolean skipSilence) {
|
||||
simpleExoPlayer.setPlaybackParameters(new PlaybackParameters(speed, pitch, skipSilence));
|
||||
}
|
||||
|
||||
public PlayQueue getPlayQueue() {
|
||||
@@ -1198,7 +1158,7 @@ public abstract class BasePlayer implements
|
||||
}
|
||||
|
||||
public boolean isProgressLoopRunning() {
|
||||
return progressUpdateReactor != null && !progressUpdateReactor.isDisposed();
|
||||
return progressUpdateReactor.get() != null;
|
||||
}
|
||||
|
||||
public void setRecovery() {
|
||||
|
||||
@@ -25,14 +25,18 @@ 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.content.res.AppCompatResources;
|
||||
import android.support.v7.widget.RecyclerView;
|
||||
import android.support.v7.widget.helper.ItemTouchHelper;
|
||||
import android.util.DisplayMetrics;
|
||||
@@ -43,27 +47,30 @@ 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.ProgressBar;
|
||||
import android.widget.RelativeLayout;
|
||||
import android.widget.SeekBar;
|
||||
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;
|
||||
|
||||
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.PlayQueue;
|
||||
import org.schabi.newpipe.playlist.PlayQueueItem;
|
||||
import org.schabi.newpipe.playlist.PlayQueueItemBuilder;
|
||||
import org.schabi.newpipe.playlist.PlayQueueItemHolder;
|
||||
import org.schabi.newpipe.playlist.PlayQueueItemTouchCallback;
|
||||
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.player.resolver.MediaSourceTag;
|
||||
import org.schabi.newpipe.player.resolver.VideoPlaybackResolver;
|
||||
import org.schabi.newpipe.util.AnimationUtils;
|
||||
import org.schabi.newpipe.util.ListHelper;
|
||||
import org.schabi.newpipe.util.NavigationHelper;
|
||||
@@ -78,6 +85,7 @@ 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.SCALE_AND_ALPHA;
|
||||
import static org.schabi.newpipe.util.AnimationUtils.Type.SLIDE_AND_ALPHA;
|
||||
import static org.schabi.newpipe.util.AnimationUtils.animateRotation;
|
||||
import static org.schabi.newpipe.util.AnimationUtils.animateView;
|
||||
@@ -95,12 +103,13 @@ public final class MainVideoPlayer extends AppCompatActivity
|
||||
|
||||
private GestureDetector gestureDetector;
|
||||
|
||||
private boolean activityPaused;
|
||||
private VideoPlayerImpl playerImpl;
|
||||
|
||||
private SharedPreferences defaultPreferences;
|
||||
|
||||
@Nullable private StateSaver.SavedState savedState;
|
||||
@Nullable private PlayerState playerState;
|
||||
private boolean isInMultiWindow;
|
||||
private boolean isBackPressed;
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Activity LifeCycle
|
||||
@@ -112,12 +121,17 @@ public final class MainVideoPlayer extends AppCompatActivity
|
||||
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);
|
||||
|
||||
WindowManager.LayoutParams lp = getWindow().getAttributes();
|
||||
lp.screenBrightness = PlayerHelper.getScreenBrightness(getApplicationContext());
|
||||
getWindow().setAttributes(lp);
|
||||
|
||||
hideSystemUi();
|
||||
setContentView(R.layout.activity_main_player);
|
||||
playerImpl = new VideoPlayerImpl(this);
|
||||
playerImpl = new VideoPlayerImpl(this);
|
||||
playerImpl.setup(findViewById(android.R.id.content));
|
||||
|
||||
if (savedInstanceState != null && savedInstanceState.get(KEY_SAVED_STATE) != null) {
|
||||
@@ -135,39 +149,45 @@ public final class MainVideoPlayer extends AppCompatActivity
|
||||
|
||||
@Override
|
||||
protected void onRestoreInstanceState(@NonNull Bundle bundle) {
|
||||
if (DEBUG) Log.d(TAG, "onRestoreInstanceState() called");
|
||||
super.onRestoreInstanceState(bundle);
|
||||
savedState = StateSaver.tryToRestore(bundle, this);
|
||||
StateSaver.tryToRestore(bundle, this);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onNewIntent(Intent intent) {
|
||||
if (DEBUG) Log.d(TAG, "onNewIntent() called with: intent = [" + intent + "]");
|
||||
super.onNewIntent(intent);
|
||||
playerImpl.handleIntent(intent);
|
||||
if (intent != null) {
|
||||
playerState = null;
|
||||
playerImpl.handleIntent(intent);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onResume() {
|
||||
super.onResume();
|
||||
if (DEBUG) Log.d(TAG, "onResume() called");
|
||||
if (playerImpl.getPlayer() != null && activityPaused && playerImpl.wasPlaying()
|
||||
&& !playerImpl.isPlaying()) {
|
||||
playerImpl.onPlay();
|
||||
}
|
||||
activityPaused = false;
|
||||
super.onResume();
|
||||
|
||||
if(globalScreenOrientationLocked()) {
|
||||
boolean lastOrientationWasLandscape
|
||||
= defaultPreferences.getBoolean(getString(R.string.last_orientation_landscape_key), false);
|
||||
if (globalScreenOrientationLocked()) {
|
||||
boolean lastOrientationWasLandscape = defaultPreferences.getBoolean(
|
||||
getString(R.string.last_orientation_landscape_key), false);
|
||||
setLandscape(lastOrientationWasLandscape);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBackPressed() {
|
||||
if (DEBUG) Log.d(TAG, "onBackPressed() called");
|
||||
super.onBackPressed();
|
||||
if (playerImpl.isPlaying()) playerImpl.getPlayer().setPlayWhenReady(false);
|
||||
// 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.isPlaybackSkipSilence(), playerState.wasPlaying());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -181,32 +201,40 @@ public final class MainVideoPlayer extends AppCompatActivity
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPause() {
|
||||
super.onPause();
|
||||
if (DEBUG) Log.d(TAG, "onPause() called");
|
||||
|
||||
if (playerImpl != null && playerImpl.getPlayer() != null && !activityPaused) {
|
||||
playerImpl.wasPlaying = playerImpl.isPlaying();
|
||||
playerImpl.onPause();
|
||||
}
|
||||
activityPaused = true;
|
||||
public void onBackPressed() {
|
||||
super.onBackPressed();
|
||||
isBackPressed = true;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onSaveInstanceState(Bundle outState) {
|
||||
if (DEBUG) Log.d(TAG, "onSaveInstanceState() called");
|
||||
super.onSaveInstanceState(outState);
|
||||
if (playerImpl == null) return;
|
||||
|
||||
playerImpl.setRecovery();
|
||||
savedState = StateSaver.tryToSave(isChangingConfigurations(), savedState,
|
||||
outState, this);
|
||||
playerState = new PlayerState(playerImpl.getPlayQueue(), playerImpl.getRepeatMode(),
|
||||
playerImpl.getPlaybackSpeed(), playerImpl.getPlaybackPitch(),
|
||||
playerImpl.getPlaybackQuality(), playerImpl.getPlaybackSkipSilence(),
|
||||
playerImpl.isPlaying());
|
||||
StateSaver.tryToSave(isChangingConfigurations(), null, outState, this);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDestroy() {
|
||||
super.onDestroy();
|
||||
if (DEBUG) Log.d(TAG, "onDestroy() called");
|
||||
if (playerImpl != null) playerImpl.destroy();
|
||||
protected void onStop() {
|
||||
if (DEBUG) Log.d(TAG, "onStop() called");
|
||||
super.onStop();
|
||||
PlayerHelper.setScreenBrightness(getApplicationContext(),
|
||||
getWindow().getAttributes().screenBrightness);
|
||||
|
||||
if (playerImpl == null) return;
|
||||
if (!isBackPressed) {
|
||||
playerImpl.minimize();
|
||||
}
|
||||
playerImpl.destroy();
|
||||
|
||||
isInMultiWindow = false;
|
||||
isBackPressed = false;
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
@@ -221,48 +249,19 @@ public final class MainVideoPlayer extends AppCompatActivity
|
||||
@Override
|
||||
public void writeTo(Queue<Object> objectsToSave) {
|
||||
if (objectsToSave == null) return;
|
||||
objectsToSave.add(playerImpl.getPlayQueue());
|
||||
objectsToSave.add(playerImpl.getRepeatMode());
|
||||
objectsToSave.add(playerImpl.getPlaybackSpeed());
|
||||
objectsToSave.add(playerImpl.getPlaybackPitch());
|
||||
objectsToSave.add(playerImpl.getPlaybackQuality());
|
||||
objectsToSave.add(playerState);
|
||||
}
|
||||
|
||||
@Override
|
||||
@SuppressWarnings("unchecked")
|
||||
public void readFrom(@NonNull Queue<Object> savedObjects) throws Exception {
|
||||
@NonNull final PlayQueue queue = (PlayQueue) savedObjects.poll();
|
||||
final int repeatMode = (int) savedObjects.poll();
|
||||
final float playbackSpeed = (float) savedObjects.poll();
|
||||
final float playbackPitch = (float) savedObjects.poll();
|
||||
@NonNull final String playbackQuality = (String) savedObjects.poll();
|
||||
|
||||
playerImpl.setPlaybackQuality(playbackQuality);
|
||||
playerImpl.initPlayback(queue, repeatMode, playbackSpeed, playbackPitch);
|
||||
|
||||
StateSaver.onDestroy(savedState);
|
||||
public void readFrom(@NonNull Queue<Object> savedObjects) {
|
||||
playerState = (PlayerState) savedObjects.poll();
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// View
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
/**
|
||||
* Prior to Kitkat, hiding system ui causes the player view to be overlaid and require two
|
||||
* clicks to get rid of that invisible overlay. By showing the system UI on actions/events,
|
||||
* that overlay is removed and the player view is put to the foreground.
|
||||
*
|
||||
* Post Kitkat, navbar and status bar can be pulled out by swiping the edge of
|
||||
* screen, therefore, we can do nothing or hide the UI on actions/events.
|
||||
* */
|
||||
private void changeSystemUi() {
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT) {
|
||||
showSystemUi();
|
||||
} else {
|
||||
hideSystemUi();
|
||||
}
|
||||
}
|
||||
|
||||
private void showSystemUi() {
|
||||
if (DEBUG) Log.d(TAG, "showSystemUi() called");
|
||||
if (playerImpl != null && playerImpl.queueVisible) return;
|
||||
@@ -275,6 +274,14 @@ public final class MainVideoPlayer extends AppCompatActivity
|
||||
} 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);
|
||||
}
|
||||
@@ -342,23 +349,36 @@ public final class MainVideoPlayer extends AppCompatActivity
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
public void onPlaybackParameterChanged(float playbackTempo, float playbackPitch,
|
||||
boolean playbackSkipSilence) {
|
||||
if (playerImpl != null) {
|
||||
playerImpl.setPlaybackParameters(playbackTempo, playbackPitch, playbackSkipSilence);
|
||||
}
|
||||
}
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////
|
||||
|
||||
@SuppressWarnings({"unused", "WeakerAccess"})
|
||||
private class VideoPlayerImpl extends VideoPlayer {
|
||||
private final float MAX_GESTURE_LENGTH = 0.75f;
|
||||
|
||||
private TextView titleTextView;
|
||||
private TextView channelTextView;
|
||||
private TextView volumeTextView;
|
||||
private TextView brightnessTextView;
|
||||
private RelativeLayout volumeRelativeLayout;
|
||||
private ProgressBar volumeProgressBar;
|
||||
private ImageView volumeImageView;
|
||||
private RelativeLayout brightnessRelativeLayout;
|
||||
private ProgressBar brightnessProgressBar;
|
||||
private ImageView brightnessImageView;
|
||||
private ImageButton queueButton;
|
||||
private ImageButton repeatButton;
|
||||
private ImageButton shuffleButton;
|
||||
@@ -382,6 +402,8 @@ public final class MainVideoPlayer extends AppCompatActivity
|
||||
private RelativeLayout windowRootLayout;
|
||||
private View secondaryControls;
|
||||
|
||||
private int maxGestureLength;
|
||||
|
||||
VideoPlayerImpl(final Context context) {
|
||||
super("VideoPlayerImpl" + MainVideoPlayer.TAG, context);
|
||||
}
|
||||
@@ -391,8 +413,12 @@ public final class MainVideoPlayer extends AppCompatActivity
|
||||
super.initViews(rootView);
|
||||
this.titleTextView = rootView.findViewById(R.id.titleTextView);
|
||||
this.channelTextView = rootView.findViewById(R.id.channelTextView);
|
||||
this.volumeTextView = rootView.findViewById(R.id.volumeTextView);
|
||||
this.brightnessTextView = rootView.findViewById(R.id.brightnessTextView);
|
||||
this.volumeRelativeLayout = rootView.findViewById(R.id.volumeRelativeLayout);
|
||||
this.volumeProgressBar = rootView.findViewById(R.id.volumeProgressBar);
|
||||
this.volumeImageView = rootView.findViewById(R.id.volumeImageView);
|
||||
this.brightnessRelativeLayout = rootView.findViewById(R.id.brightnessRelativeLayout);
|
||||
this.brightnessProgressBar = rootView.findViewById(R.id.brightnessProgressBar);
|
||||
this.brightnessImageView = rootView.findViewById(R.id.brightnessImageView);
|
||||
this.queueButton = rootView.findViewById(R.id.queueButton);
|
||||
this.repeatButton = rootView.findViewById(R.id.repeatButton);
|
||||
this.shuffleButton = rootView.findViewById(R.id.shuffleButton);
|
||||
@@ -411,15 +437,6 @@ public final class MainVideoPlayer extends AppCompatActivity
|
||||
this.itemsListCloseButton = findViewById(R.id.playQueueClose);
|
||||
this.itemsList = findViewById(R.id.playQueue);
|
||||
|
||||
this.windowRootLayout = rootView.findViewById(R.id.playbackWindowRoot);
|
||||
// Prior to Kitkat, there is no way of setting translucent navbar programmatically.
|
||||
// Thus, fit system windows is opted instead.
|
||||
// See https://stackoverflow.com/questions/29069070/completely-transparent-status-bar-and-navigation-bar-on-lollipop
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
|
||||
windowRootLayout.setFitsSystemWindows(false);
|
||||
windowRootLayout.invalidate();
|
||||
}
|
||||
|
||||
titleTextView.setSelected(true);
|
||||
channelTextView.setSelected(true);
|
||||
|
||||
@@ -428,27 +445,22 @@ public final class MainVideoPlayer extends AppCompatActivity
|
||||
|
||||
@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
|
||||
public void initListeners() {
|
||||
super.initListeners();
|
||||
|
||||
MySimpleOnGestureListener listener = new MySimpleOnGestureListener();
|
||||
PlayerGestureListener listener = new PlayerGestureListener();
|
||||
gestureDetector = new GestureDetector(context, listener);
|
||||
gestureDetector.setIsLongpressEnabled(false);
|
||||
getRootView().setOnTouchListener(listener);
|
||||
@@ -465,6 +477,37 @@ public final class MainVideoPlayer extends AppCompatActivity
|
||||
toggleOrientationButton.setOnClickListener(this);
|
||||
switchBackgroundButton.setOnClickListener(this);
|
||||
switchPopupButton.setOnClickListener(this);
|
||||
|
||||
getRootView().addOnLayoutChangeListener((view, l, t, r, b, ol, ot, or, ob) -> {
|
||||
if (l != ol || t != ot || r != or || b != ob) {
|
||||
// Use smaller value to be consistent between screen orientations
|
||||
// (and to make usage easier)
|
||||
int width = r - l, height = b - t;
|
||||
maxGestureLength = (int) (Math.min(width, height) * MAX_GESTURE_LENGTH);
|
||||
|
||||
if (DEBUG) Log.d(TAG, "maxGestureLength = " + maxGestureLength);
|
||||
|
||||
volumeProgressBar.setMax(maxGestureLength);
|
||||
brightnessProgressBar.setMax(maxGestureLength);
|
||||
|
||||
setInitialGestureValues();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public void minimize() {
|
||||
switch (PlayerHelper.getMinimizeOnExitAction(context)) {
|
||||
case PlayerHelper.MinimizeMode.MINIMIZE_ON_EXIT_MODE_BACKGROUND:
|
||||
onPlayBackgroundButtonClicked();
|
||||
break;
|
||||
case PlayerHelper.MinimizeMode.MINIMIZE_ON_EXIT_MODE_POPUP:
|
||||
onFullScreenButtonClicked();
|
||||
break;
|
||||
case PlayerHelper.MinimizeMode.MINIMIZE_ON_EXIT_MODE_NONE:
|
||||
default:
|
||||
// No action
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
@@ -487,14 +530,11 @@ public final class MainVideoPlayer extends AppCompatActivity
|
||||
// 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);
|
||||
protected void onMetadataChanged(@NonNull final MediaSourceTag tag) {
|
||||
super.onMetadataChanged(tag);
|
||||
|
||||
titleTextView.setText(getVideoTitle());
|
||||
channelTextView.setText(getUploaderName());
|
||||
titleTextView.setText(tag.getMetadata().getName());
|
||||
channelTextView.setText(tag.getMetadata().getUploaderName());
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -527,6 +567,7 @@ public final class MainVideoPlayer extends AppCompatActivity
|
||||
this.getRepeatMode(),
|
||||
this.getPlaybackSpeed(),
|
||||
this.getPlaybackPitch(),
|
||||
this.getPlaybackSkipSilence(),
|
||||
this.getPlaybackQuality()
|
||||
);
|
||||
context.startService(intent);
|
||||
@@ -548,6 +589,7 @@ public final class MainVideoPlayer extends AppCompatActivity
|
||||
this.getRepeatMode(),
|
||||
this.getPlaybackSpeed(),
|
||||
this.getPlaybackPitch(),
|
||||
this.getPlaybackSkipSilence(),
|
||||
this.getPlaybackQuality()
|
||||
);
|
||||
context.startService(intent);
|
||||
@@ -643,7 +685,8 @@ public final class MainVideoPlayer extends AppCompatActivity
|
||||
|
||||
@Override
|
||||
public void onPlaybackSpeedClicked() {
|
||||
PlaybackParameterDialog.newInstance(getPlaybackSpeed(), getPlaybackPitch())
|
||||
PlaybackParameterDialog
|
||||
.newInstance(getPlaybackSpeed(), getPlaybackPitch(), getPlaybackSkipSilence())
|
||||
.show(getSupportFragmentManager(), TAG);
|
||||
}
|
||||
|
||||
@@ -673,14 +716,19 @@ public final class MainVideoPlayer extends AppCompatActivity
|
||||
}
|
||||
|
||||
@Override
|
||||
protected int getDefaultResolutionIndex(final List<VideoStream> sortedVideos) {
|
||||
return ListHelper.getDefaultResolutionIndex(context, sortedVideos);
|
||||
}
|
||||
protected VideoPlaybackResolver.QualityResolver getQualityResolver() {
|
||||
return new VideoPlaybackResolver.QualityResolver() {
|
||||
@Override
|
||||
public int getDefaultResolutionIndex(List<VideoStream> sortedVideos) {
|
||||
return ListHelper.getDefaultResolutionIndex(context, sortedVideos);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected int getOverrideResolutionIndex(final List<VideoStream> sortedVideos,
|
||||
final String playbackQuality) {
|
||||
return ListHelper.getDefaultResolutionIndex(context, sortedVideos, playbackQuality);
|
||||
@Override
|
||||
public int getOverrideResolutionIndex(List<VideoStream> sortedVideos,
|
||||
String playbackQuality) {
|
||||
return ListHelper.getResolutionIndex(context, sortedVideos, playbackQuality);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
@@ -704,7 +752,6 @@ public final class MainVideoPlayer extends AppCompatActivity
|
||||
@Override
|
||||
public void onBuffering() {
|
||||
super.onBuffering();
|
||||
animatePlayButtons(false, 100);
|
||||
getRootView().setKeepScreenOn(true);
|
||||
}
|
||||
|
||||
@@ -727,7 +774,7 @@ public final class MainVideoPlayer extends AppCompatActivity
|
||||
animatePlayButtons(true, 200);
|
||||
});
|
||||
|
||||
changeSystemUi();
|
||||
showSystemUi();
|
||||
getRootView().setKeepScreenOn(false);
|
||||
}
|
||||
|
||||
@@ -754,6 +801,13 @@ public final class MainVideoPlayer extends AppCompatActivity
|
||||
// Utils
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
private void setInitialGestureValues() {
|
||||
if (getAudioReactor() != null) {
|
||||
final float currentVolumeNormalized = (float) getAudioReactor().getVolume() / getAudioReactor().getMaxVolume();
|
||||
volumeProgressBar.setProgress((int) (volumeProgressBar.getMax() * currentVolumeNormalized));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void showControlsThenHide() {
|
||||
if (queueVisible) return;
|
||||
@@ -857,12 +911,28 @@ public final class MainVideoPlayer extends AppCompatActivity
|
||||
return channelTextView;
|
||||
}
|
||||
|
||||
public TextView getVolumeTextView() {
|
||||
return volumeTextView;
|
||||
public RelativeLayout getVolumeRelativeLayout() {
|
||||
return volumeRelativeLayout;
|
||||
}
|
||||
|
||||
public TextView getBrightnessTextView() {
|
||||
return brightnessTextView;
|
||||
public ProgressBar getVolumeProgressBar() {
|
||||
return volumeProgressBar;
|
||||
}
|
||||
|
||||
public ImageView getVolumeImageView() {
|
||||
return volumeImageView;
|
||||
}
|
||||
|
||||
public RelativeLayout getBrightnessRelativeLayout() {
|
||||
return brightnessRelativeLayout;
|
||||
}
|
||||
|
||||
public ProgressBar getBrightnessProgressBar() {
|
||||
return brightnessProgressBar;
|
||||
}
|
||||
|
||||
public ImageView getBrightnessImageView() {
|
||||
return brightnessImageView;
|
||||
}
|
||||
|
||||
public ImageButton getRepeatButton() {
|
||||
@@ -872,20 +942,25 @@ public final class MainVideoPlayer extends AppCompatActivity
|
||||
public ImageButton getPlayPauseButton() {
|
||||
return playPauseButton;
|
||||
}
|
||||
|
||||
public int getMaxGestureLength() {
|
||||
return maxGestureLength;
|
||||
}
|
||||
}
|
||||
|
||||
private class MySimpleOnGestureListener extends GestureDetector.SimpleOnGestureListener implements View.OnTouchListener {
|
||||
private class PlayerGestureListener extends GestureDetector.SimpleOnGestureListener implements View.OnTouchListener {
|
||||
private boolean isMoving;
|
||||
|
||||
@Override
|
||||
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.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;
|
||||
@@ -900,7 +975,7 @@ public final class MainVideoPlayer extends AppCompatActivity
|
||||
playerImpl.hideControls(150, 0);
|
||||
} else {
|
||||
playerImpl.showControlsThenHide();
|
||||
changeSystemUi();
|
||||
showSystemUi();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
@@ -912,89 +987,91 @@ public final class MainVideoPlayer extends AppCompatActivity
|
||||
return super.onDown(e);
|
||||
}
|
||||
|
||||
private static final int MOVEMENT_THRESHOLD = 40;
|
||||
|
||||
private final boolean isPlayerGestureEnabled = PlayerHelper.isPlayerGestureEnabled(getApplicationContext());
|
||||
private final int maxVolume = playerImpl.getAudioReactor().getMaxVolume();
|
||||
|
||||
private final float stepsBrightness = 15, stepBrightness = (1f / stepsBrightness), minBrightness = .01f;
|
||||
private float currentBrightness = .5f;
|
||||
|
||||
private int currentVolume, maxVolume = playerImpl.getAudioReactor().getMaxVolume();
|
||||
private final float stepsVolume = 15, stepVolume = (float) Math.ceil(maxVolume / stepsVolume), minVolume = 0;
|
||||
|
||||
private final String brightnessUnicode = new String(Character.toChars(0x2600));
|
||||
private final String volumeUnicode = new String(Character.toChars(0x1F508));
|
||||
|
||||
private final int MOVEMENT_THRESHOLD = 40;
|
||||
private final int eventsThreshold = 8;
|
||||
private boolean triggered = false;
|
||||
private int eventsNum;
|
||||
|
||||
// TODO: Improve video gesture controls
|
||||
@Override
|
||||
public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
|
||||
public boolean onScroll(MotionEvent initialEvent, MotionEvent movingEvent, float distanceX, float distanceY) {
|
||||
if (!isPlayerGestureEnabled) return false;
|
||||
|
||||
//noinspection PointlessBooleanExpression
|
||||
if (DEBUG && false) Log.d(TAG, "MainVideoPlayer.onScroll = " +
|
||||
", e1.getRaw = [" + e1.getRawX() + ", " + e1.getRawY() + "]" +
|
||||
", e2.getRaw = [" + e2.getRawX() + ", " + e2.getRawY() + "]" +
|
||||
", e1.getRaw = [" + initialEvent.getRawX() + ", " + initialEvent.getRawY() + "]" +
|
||||
", e2.getRaw = [" + movingEvent.getRawX() + ", " + movingEvent.getRawY() + "]" +
|
||||
", distanceXy = [" + distanceX + ", " + distanceY + "]");
|
||||
float abs = Math.abs(e2.getY() - e1.getY());
|
||||
if (!triggered) {
|
||||
triggered = abs > MOVEMENT_THRESHOLD;
|
||||
|
||||
final boolean insideThreshold = Math.abs(movingEvent.getY() - initialEvent.getY()) <= MOVEMENT_THRESHOLD;
|
||||
if (!isMoving && (insideThreshold || Math.abs(distanceX) > Math.abs(distanceY))
|
||||
|| playerImpl.getCurrentState() == BasePlayer.STATE_COMPLETED) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (eventsNum++ % eventsThreshold != 0 || playerImpl.getCurrentState() == BasePlayer.STATE_COMPLETED) return false;
|
||||
isMoving = true;
|
||||
// boolean up = !((e2.getY() - e1.getY()) > 0) && distanceY > 0; // Android's origin point is on top
|
||||
boolean up = distanceY > 0;
|
||||
|
||||
|
||||
if (e1.getX() > playerImpl.getRootView().getWidth() / 2) {
|
||||
double floor = Math.floor(up ? stepVolume : -stepVolume);
|
||||
currentVolume = (int) (playerImpl.getAudioReactor().getVolume() + floor);
|
||||
if (currentVolume >= maxVolume) currentVolume = maxVolume;
|
||||
if (currentVolume <= minVolume) currentVolume = (int) minVolume;
|
||||
if (initialEvent.getX() > playerImpl.getRootView().getWidth() / 2) {
|
||||
playerImpl.getVolumeProgressBar().incrementProgressBy((int) distanceY);
|
||||
float currentProgressPercent =
|
||||
(float) playerImpl.getVolumeProgressBar().getProgress() / playerImpl.getMaxGestureLength();
|
||||
int currentVolume = (int) (maxVolume * currentProgressPercent);
|
||||
playerImpl.getAudioReactor().setVolume(currentVolume);
|
||||
|
||||
currentVolume = playerImpl.getAudioReactor().getVolume();
|
||||
if (DEBUG) Log.d(TAG, "onScroll().volumeControl, currentVolume = " + currentVolume);
|
||||
final String volumeText = volumeUnicode + " " + Math.round((((float) currentVolume) / maxVolume) * 100) + "%";
|
||||
playerImpl.getVolumeTextView().setText(volumeText);
|
||||
|
||||
if (playerImpl.getVolumeTextView().getVisibility() != View.VISIBLE) animateView(playerImpl.getVolumeTextView(), true, 200);
|
||||
if (playerImpl.getBrightnessTextView().getVisibility() == View.VISIBLE) playerImpl.getBrightnessTextView().setVisibility(View.GONE);
|
||||
final int resId =
|
||||
currentProgressPercent <= 0 ? R.drawable.ic_volume_off_white_72dp
|
||||
: currentProgressPercent < 0.25 ? R.drawable.ic_volume_mute_white_72dp
|
||||
: currentProgressPercent < 0.75 ? R.drawable.ic_volume_down_white_72dp
|
||||
: R.drawable.ic_volume_up_white_72dp;
|
||||
|
||||
playerImpl.getVolumeImageView().setImageDrawable(
|
||||
AppCompatResources.getDrawable(getApplicationContext(), resId)
|
||||
);
|
||||
|
||||
if (playerImpl.getVolumeRelativeLayout().getVisibility() != View.VISIBLE) {
|
||||
animateView(playerImpl.getVolumeRelativeLayout(), SCALE_AND_ALPHA, true, 200);
|
||||
}
|
||||
if (playerImpl.getBrightnessRelativeLayout().getVisibility() == View.VISIBLE) {
|
||||
playerImpl.getBrightnessRelativeLayout().setVisibility(View.GONE);
|
||||
}
|
||||
} else {
|
||||
WindowManager.LayoutParams lp = getWindow().getAttributes();
|
||||
currentBrightness += up ? stepBrightness : -stepBrightness;
|
||||
if (currentBrightness >= 1f) currentBrightness = 1f;
|
||||
if (currentBrightness <= minBrightness) currentBrightness = minBrightness;
|
||||
playerImpl.getBrightnessProgressBar().incrementProgressBy((int) distanceY);
|
||||
float currentProgressPercent =
|
||||
(float) playerImpl.getBrightnessProgressBar().getProgress() / playerImpl.getMaxGestureLength();
|
||||
WindowManager.LayoutParams layoutParams = getWindow().getAttributes();
|
||||
layoutParams.screenBrightness = currentProgressPercent;
|
||||
getWindow().setAttributes(layoutParams);
|
||||
|
||||
lp.screenBrightness = currentBrightness;
|
||||
getWindow().setAttributes(lp);
|
||||
if (DEBUG) Log.d(TAG, "onScroll().brightnessControl, currentBrightness = " + currentBrightness);
|
||||
int brightnessNormalized = Math.round(currentBrightness * 100);
|
||||
if (DEBUG) Log.d(TAG, "onScroll().brightnessControl, currentBrightness = " + currentProgressPercent);
|
||||
|
||||
final String brightnessText = brightnessUnicode + " " + (brightnessNormalized == 1 ? 0 : brightnessNormalized) + "%";
|
||||
playerImpl.getBrightnessTextView().setText(brightnessText);
|
||||
final int resId =
|
||||
currentProgressPercent < 0.25 ? R.drawable.ic_brightness_low_white_72dp
|
||||
: currentProgressPercent < 0.75 ? R.drawable.ic_brightness_medium_white_72dp
|
||||
: R.drawable.ic_brightness_high_white_72dp;
|
||||
|
||||
if (playerImpl.getBrightnessTextView().getVisibility() != View.VISIBLE) animateView(playerImpl.getBrightnessTextView(), true, 200);
|
||||
if (playerImpl.getVolumeTextView().getVisibility() == View.VISIBLE) playerImpl.getVolumeTextView().setVisibility(View.GONE);
|
||||
playerImpl.getBrightnessImageView().setImageDrawable(
|
||||
AppCompatResources.getDrawable(getApplicationContext(), resId)
|
||||
);
|
||||
|
||||
if (playerImpl.getBrightnessRelativeLayout().getVisibility() != View.VISIBLE) {
|
||||
animateView(playerImpl.getBrightnessRelativeLayout(), SCALE_AND_ALPHA, true, 200);
|
||||
}
|
||||
if (playerImpl.getVolumeRelativeLayout().getVisibility() == View.VISIBLE) {
|
||||
playerImpl.getVolumeRelativeLayout().setVisibility(View.GONE);
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private void onScrollEnd() {
|
||||
if (DEBUG) Log.d(TAG, "onScrollEnd() called");
|
||||
triggered = false;
|
||||
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.getVolumeRelativeLayout().getVisibility() == View.VISIBLE) {
|
||||
animateView(playerImpl.getVolumeRelativeLayout(), SCALE_AND_ALPHA, false, 200, 200);
|
||||
}
|
||||
if (playerImpl.getBrightnessTextView().getVisibility() == View.VISIBLE) {
|
||||
animateView(playerImpl.getBrightnessTextView(), false, 200, 200);
|
||||
if (playerImpl.getBrightnessRelativeLayout().getVisibility() == View.VISIBLE) {
|
||||
animateView(playerImpl.getBrightnessRelativeLayout(), SCALE_AND_ALPHA, false, 200, 200);
|
||||
}
|
||||
|
||||
if (playerImpl.isControlsVisible() && playerImpl.getCurrentState() == STATE_PLAYING) {
|
||||
|
||||
77
app/src/main/java/org/schabi/newpipe/player/PlayerState.java
Normal file
77
app/src/main/java/org/schabi/newpipe/player/PlayerState.java
Normal file
@@ -0,0 +1,77 @@
|
||||
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 playbackSkipSilence;
|
||||
private final boolean wasPlaying;
|
||||
|
||||
PlayerState(@NonNull final PlayQueue playQueue, final int repeatMode,
|
||||
final float playbackSpeed, final float playbackPitch,
|
||||
final boolean playbackSkipSilence, final boolean wasPlaying) {
|
||||
this(playQueue, repeatMode, playbackSpeed, playbackPitch, null,
|
||||
playbackSkipSilence, wasPlaying);
|
||||
}
|
||||
|
||||
PlayerState(@NonNull final PlayQueue playQueue, final int repeatMode,
|
||||
final float playbackSpeed, final float playbackPitch,
|
||||
@Nullable final String playbackQuality, final boolean playbackSkipSilence,
|
||||
final boolean wasPlaying) {
|
||||
this.playQueue = playQueue;
|
||||
this.repeatMode = repeatMode;
|
||||
this.playbackSpeed = playbackSpeed;
|
||||
this.playbackPitch = playbackPitch;
|
||||
this.playbackQuality = playbackQuality;
|
||||
this.playbackSkipSilence = playbackSkipSilence;
|
||||
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 isPlaybackSkipSilence() {
|
||||
return playbackSkipSilence;
|
||||
}
|
||||
|
||||
public boolean wasPlaying() {
|
||||
return wasPlaying;
|
||||
}
|
||||
}
|
||||
@@ -19,6 +19,8 @@
|
||||
|
||||
package org.schabi.newpipe.player;
|
||||
|
||||
import android.animation.Animator;
|
||||
import android.animation.AnimatorListenerAdapter;
|
||||
import android.annotation.SuppressLint;
|
||||
import android.app.NotificationManager;
|
||||
import android.app.PendingIntent;
|
||||
@@ -34,7 +36,7 @@ import android.os.Build;
|
||||
import android.os.IBinder;
|
||||
import android.preference.PreferenceManager;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.support.design.widget.FloatingActionButton;
|
||||
import android.support.v4.app.NotificationCompat;
|
||||
import android.util.DisplayMetrics;
|
||||
import android.util.Log;
|
||||
@@ -42,8 +44,11 @@ import android.view.GestureDetector;
|
||||
import android.view.Gravity;
|
||||
import android.view.MotionEvent;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.view.WindowManager;
|
||||
import android.view.animation.AnticipateInterpolator;
|
||||
import android.widget.ImageButton;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.PopupMenu;
|
||||
import android.widget.RemoteViews;
|
||||
import android.widget.SeekBar;
|
||||
@@ -52,18 +57,20 @@ 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;
|
||||
import com.nostra13.universalimageloader.core.assist.FailReason;
|
||||
|
||||
import org.schabi.newpipe.BuildConfig;
|
||||
import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.extractor.stream.StreamInfo;
|
||||
import org.schabi.newpipe.extractor.stream.VideoStream;
|
||||
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.resolver.MediaSourceTag;
|
||||
import org.schabi.newpipe.player.resolver.VideoPlaybackResolver;
|
||||
import org.schabi.newpipe.util.ListHelper;
|
||||
import org.schabi.newpipe.util.NavigationHelper;
|
||||
import org.schabi.newpipe.util.ThemeHelper;
|
||||
@@ -96,11 +103,19 @@ public final class PopupVideoPlayer extends Service {
|
||||
|
||||
private static final int MINIMUM_SHOW_EXTRA_WIDTH_DP = 300;
|
||||
|
||||
private WindowManager windowManager;
|
||||
private WindowManager.LayoutParams windowLayoutParams;
|
||||
private GestureDetector gestureDetector;
|
||||
private static final int IDLE_WINDOW_FLAGS = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE |
|
||||
WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM;
|
||||
private static final int ONGOING_PLAYBACK_WINDOW_FLAGS = IDLE_WINDOW_FLAGS |
|
||||
WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON;
|
||||
|
||||
private WindowManager windowManager;
|
||||
private WindowManager.LayoutParams popupLayoutParams;
|
||||
private GestureDetector popupGestureDetector;
|
||||
|
||||
private View closeOverlayView;
|
||||
private FloatingActionButton closeOverlayButton;
|
||||
private WindowManager.LayoutParams closeOverlayLayoutParams;
|
||||
|
||||
private int shutdownFlingVelocity;
|
||||
private int tossFlingVelocity;
|
||||
|
||||
private float screenWidth, screenHeight;
|
||||
@@ -115,6 +130,7 @@ public final class PopupVideoPlayer extends Service {
|
||||
|
||||
private VideoPlayerImpl playerImpl;
|
||||
private LockManager lockManager;
|
||||
private boolean isPopupClosing = false;
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Service-Activity Binder
|
||||
@@ -143,7 +159,10 @@ public final class PopupVideoPlayer extends Service {
|
||||
public int onStartCommand(final Intent intent, int flags, int startId) {
|
||||
if (DEBUG)
|
||||
Log.d(TAG, "onStartCommand() called with: intent = [" + intent + "], flags = [" + flags + "], startId = [" + startId + "]");
|
||||
if (playerImpl.getPlayer() == null) initPopup();
|
||||
if (playerImpl.getPlayer() == null) {
|
||||
initPopup();
|
||||
initPopupCloseOverlay();
|
||||
}
|
||||
if (!playerImpl.isPlaying()) playerImpl.getPlayer().setPlayWhenReady(true);
|
||||
|
||||
playerImpl.handleIntent(intent);
|
||||
@@ -153,15 +172,16 @@ public final class PopupVideoPlayer extends Service {
|
||||
|
||||
@Override
|
||||
public void onConfigurationChanged(Configuration newConfig) {
|
||||
if (DEBUG) Log.d(TAG, "onConfigurationChanged() called with: newConfig = [" + newConfig + "]");
|
||||
updateScreenSize();
|
||||
updatePopupSize(windowLayoutParams.width, -1);
|
||||
checkPositionBounds();
|
||||
updatePopupSize(popupLayoutParams.width, -1);
|
||||
checkPopupPositionBounds();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroy() {
|
||||
if (DEBUG) Log.d(TAG, "onDestroy() called");
|
||||
onClose();
|
||||
closePopup();
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -179,7 +199,6 @@ public final class PopupVideoPlayer extends Service {
|
||||
View rootView = View.inflate(this, R.layout.player_popup, null);
|
||||
playerImpl.setup(rootView);
|
||||
|
||||
shutdownFlingVelocity = PlayerHelper.getShutdownFlingVelocity(this);
|
||||
tossFlingVelocity = PlayerHelper.getTossFlingVelocity(this);
|
||||
|
||||
updateScreenSize();
|
||||
@@ -189,28 +208,56 @@ public final class PopupVideoPlayer extends Service {
|
||||
SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this);
|
||||
popupWidth = popupRememberSizeAndPos ? sharedPreferences.getFloat(POPUP_SAVED_WIDTH, defaultSize) : defaultSize;
|
||||
|
||||
final int layoutParamType = Build.VERSION.SDK_INT < android.os.Build.VERSION_CODES.O ? WindowManager.LayoutParams.TYPE_PHONE : WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY;
|
||||
final int layoutParamType = Build.VERSION.SDK_INT < android.os.Build.VERSION_CODES.O ?
|
||||
WindowManager.LayoutParams.TYPE_PHONE :
|
||||
WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY;
|
||||
|
||||
windowLayoutParams = new WindowManager.LayoutParams(
|
||||
popupLayoutParams = new WindowManager.LayoutParams(
|
||||
(int) popupWidth, (int) getMinimumVideoHeight(popupWidth),
|
||||
layoutParamType,
|
||||
WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE,
|
||||
IDLE_WINDOW_FLAGS,
|
||||
PixelFormat.TRANSLUCENT);
|
||||
windowLayoutParams.gravity = Gravity.LEFT | Gravity.TOP;
|
||||
popupLayoutParams.gravity = Gravity.LEFT | Gravity.TOP;
|
||||
popupLayoutParams.softInputMode = WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE;
|
||||
|
||||
int centerX = (int) (screenWidth / 2f - popupWidth / 2f);
|
||||
int centerY = (int) (screenHeight / 2f - popupHeight / 2f);
|
||||
windowLayoutParams.x = popupRememberSizeAndPos ? sharedPreferences.getInt(POPUP_SAVED_X, centerX) : centerX;
|
||||
windowLayoutParams.y = popupRememberSizeAndPos ? sharedPreferences.getInt(POPUP_SAVED_Y, centerY) : centerY;
|
||||
popupLayoutParams.x = popupRememberSizeAndPos ? sharedPreferences.getInt(POPUP_SAVED_X, centerX) : centerX;
|
||||
popupLayoutParams.y = popupRememberSizeAndPos ? sharedPreferences.getInt(POPUP_SAVED_Y, centerY) : centerY;
|
||||
|
||||
checkPositionBounds();
|
||||
checkPopupPositionBounds();
|
||||
|
||||
MySimpleOnGestureListener listener = new MySimpleOnGestureListener();
|
||||
gestureDetector = new GestureDetector(this, listener);
|
||||
PopupWindowGestureListener listener = new PopupWindowGestureListener();
|
||||
popupGestureDetector = new GestureDetector(this, listener);
|
||||
rootView.setOnTouchListener(listener);
|
||||
playerImpl.getLoadingPanel().setMinimumWidth(windowLayoutParams.width);
|
||||
playerImpl.getLoadingPanel().setMinimumHeight(windowLayoutParams.height);
|
||||
windowManager.addView(rootView, windowLayoutParams);
|
||||
|
||||
playerImpl.getLoadingPanel().setMinimumWidth(popupLayoutParams.width);
|
||||
playerImpl.getLoadingPanel().setMinimumHeight(popupLayoutParams.height);
|
||||
windowManager.addView(rootView, popupLayoutParams);
|
||||
}
|
||||
|
||||
@SuppressLint("RtlHardcoded")
|
||||
private void initPopupCloseOverlay() {
|
||||
if (DEBUG) Log.d(TAG, "initPopupCloseOverlay() called");
|
||||
closeOverlayView = View.inflate(this, R.layout.player_popup_close_overlay, null);
|
||||
closeOverlayButton = closeOverlayView.findViewById(R.id.closeButton);
|
||||
|
||||
final int layoutParamType = Build.VERSION.SDK_INT < android.os.Build.VERSION_CODES.O ?
|
||||
WindowManager.LayoutParams.TYPE_PHONE :
|
||||
WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY;
|
||||
final int flags = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE | WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE
|
||||
| WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM;
|
||||
|
||||
closeOverlayLayoutParams = new WindowManager.LayoutParams(
|
||||
ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT,
|
||||
layoutParamType,
|
||||
flags,
|
||||
PixelFormat.TRANSLUCENT);
|
||||
closeOverlayLayoutParams.gravity = Gravity.LEFT | Gravity.TOP;
|
||||
closeOverlayLayoutParams.softInputMode = WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE;
|
||||
|
||||
closeOverlayButton.setVisibility(View.GONE);
|
||||
windowManager.addView(closeOverlayView, closeOverlayLayoutParams);
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
@@ -226,6 +273,7 @@ public final class PopupVideoPlayer extends Service {
|
||||
|
||||
notRemoteView.setTextViewText(R.id.notificationSongName, playerImpl.getVideoTitle());
|
||||
notRemoteView.setTextViewText(R.id.notificationArtist, playerImpl.getUploaderName());
|
||||
notRemoteView.setImageViewBitmap(R.id.notificationCover, playerImpl.getThumbnail());
|
||||
|
||||
notRemoteView.setOnClickPendingIntent(R.id.notificationPlayPause,
|
||||
PendingIntent.getBroadcast(this, NOTIFICATION_ID, new Intent(ACTION_PLAY_PAUSE), PendingIntent.FLAG_UPDATE_CURRENT));
|
||||
@@ -241,11 +289,15 @@ public final class PopupVideoPlayer extends Service {
|
||||
|
||||
setRepeatModeRemote(notRemoteView, playerImpl.getRepeatMode());
|
||||
|
||||
return new NotificationCompat.Builder(this, getString(R.string.notification_channel_id))
|
||||
NotificationCompat.Builder builder = new NotificationCompat.Builder(this, getString(R.string.notification_channel_id))
|
||||
.setOngoing(true)
|
||||
.setSmallIcon(R.drawable.ic_newpipe_triangle_white)
|
||||
.setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
|
||||
.setContent(notRemoteView);
|
||||
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.JELLY_BEAN) {
|
||||
builder.setPriority(NotificationCompat.PRIORITY_MAX);
|
||||
}
|
||||
return builder;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -265,44 +317,105 @@ public final class PopupVideoPlayer extends Service {
|
||||
// Misc
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
public void onClose() {
|
||||
if (DEBUG) Log.d(TAG, "onClose() called");
|
||||
public void closePopup() {
|
||||
if (DEBUG) Log.d(TAG, "closePopup() called, isPopupClosing = " + isPopupClosing);
|
||||
if (isPopupClosing) return;
|
||||
isPopupClosing = true;
|
||||
|
||||
if (playerImpl != null) {
|
||||
if (playerImpl.getRootView() != null) {
|
||||
windowManager.removeView(playerImpl.getRootView());
|
||||
playerImpl.setRootView(null);
|
||||
}
|
||||
playerImpl.setRootView(null);
|
||||
playerImpl.stopActivityBinding();
|
||||
playerImpl.destroy();
|
||||
playerImpl = null;
|
||||
}
|
||||
|
||||
mBinder = null;
|
||||
if (lockManager != null) lockManager.releaseWifiAndCpu();
|
||||
if (notificationManager != null) notificationManager.cancel(NOTIFICATION_ID);
|
||||
mBinder = null;
|
||||
playerImpl = null;
|
||||
|
||||
stopForeground(true);
|
||||
stopSelf();
|
||||
animateOverlayAndFinishService();
|
||||
}
|
||||
|
||||
private void animateOverlayAndFinishService() {
|
||||
final int targetTranslationY = (int) (closeOverlayButton.getRootView().getHeight() - closeOverlayButton.getY());
|
||||
|
||||
closeOverlayButton.animate().setListener(null).cancel();
|
||||
closeOverlayButton.animate()
|
||||
.setInterpolator(new AnticipateInterpolator())
|
||||
.translationY(targetTranslationY)
|
||||
.setDuration(400)
|
||||
.setListener(new AnimatorListenerAdapter() {
|
||||
@Override
|
||||
public void onAnimationCancel(Animator animation) {
|
||||
end();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAnimationEnd(Animator animation) {
|
||||
end();
|
||||
}
|
||||
|
||||
private void end() {
|
||||
windowManager.removeView(closeOverlayView);
|
||||
|
||||
stopForeground(true);
|
||||
stopSelf();
|
||||
}
|
||||
}).start();
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Utils
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
private void checkPositionBounds() {
|
||||
if (windowLayoutParams.x > screenWidth - windowLayoutParams.width)
|
||||
windowLayoutParams.x = (int) (screenWidth - windowLayoutParams.width);
|
||||
if (windowLayoutParams.x < 0) windowLayoutParams.x = 0;
|
||||
if (windowLayoutParams.y > screenHeight - windowLayoutParams.height)
|
||||
windowLayoutParams.y = (int) (screenHeight - windowLayoutParams.height);
|
||||
if (windowLayoutParams.y < 0) windowLayoutParams.y = 0;
|
||||
/**
|
||||
* @see #checkPopupPositionBounds(float, float)
|
||||
*/
|
||||
@SuppressWarnings("UnusedReturnValue")
|
||||
private boolean checkPopupPositionBounds() {
|
||||
return checkPopupPositionBounds(screenWidth, screenHeight);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if {@link #popupLayoutParams}' position is within a arbitrary boundary that goes from (0,0) to (boundaryWidth,boundaryHeight).
|
||||
* <p>
|
||||
* If it's out of these boundaries, {@link #popupLayoutParams}' position is changed and {@code true} is returned
|
||||
* to represent this change.
|
||||
*
|
||||
* @return if the popup was out of bounds and have been moved back to it
|
||||
*/
|
||||
private boolean checkPopupPositionBounds(final float boundaryWidth, final float boundaryHeight) {
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "checkPopupPositionBounds() called with: boundaryWidth = [" + boundaryWidth + "], boundaryHeight = [" + boundaryHeight + "]");
|
||||
}
|
||||
|
||||
if (popupLayoutParams.x < 0) {
|
||||
popupLayoutParams.x = 0;
|
||||
return true;
|
||||
} else if (popupLayoutParams.x > boundaryWidth - popupLayoutParams.width) {
|
||||
popupLayoutParams.x = (int) (boundaryWidth - popupLayoutParams.width);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (popupLayoutParams.y < 0) {
|
||||
popupLayoutParams.y = 0;
|
||||
return true;
|
||||
} else if (popupLayoutParams.y > boundaryHeight - popupLayoutParams.height) {
|
||||
popupLayoutParams.y = (int) (boundaryHeight - popupLayoutParams.height);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private void savePositionAndSize() {
|
||||
SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(PopupVideoPlayer.this);
|
||||
sharedPreferences.edit().putInt(POPUP_SAVED_X, windowLayoutParams.x).apply();
|
||||
sharedPreferences.edit().putInt(POPUP_SAVED_Y, windowLayoutParams.y).apply();
|
||||
sharedPreferences.edit().putFloat(POPUP_SAVED_WIDTH, windowLayoutParams.width).apply();
|
||||
sharedPreferences.edit().putInt(POPUP_SAVED_X, popupLayoutParams.x).apply();
|
||||
sharedPreferences.edit().putInt(POPUP_SAVED_Y, popupLayoutParams.y).apply();
|
||||
sharedPreferences.edit().putFloat(POPUP_SAVED_WIDTH, popupLayoutParams.width).apply();
|
||||
}
|
||||
|
||||
private float getMinimumVideoHeight(float width) {
|
||||
@@ -337,13 +450,13 @@ public final class PopupVideoPlayer extends Service {
|
||||
if (height == -1) height = (int) getMinimumVideoHeight(width);
|
||||
else height = (int) (height > maximumHeight ? maximumHeight : height < minimumHeight ? minimumHeight : height);
|
||||
|
||||
windowLayoutParams.width = width;
|
||||
windowLayoutParams.height = height;
|
||||
popupLayoutParams.width = width;
|
||||
popupLayoutParams.height = height;
|
||||
popupWidth = width;
|
||||
popupHeight = height;
|
||||
|
||||
if (DEBUG) Log.d(TAG, "updatePopupSize() updated values: width = [" + width + "], height = [" + height + "]");
|
||||
windowManager.updateViewLayout(playerImpl.getRootView(), windowLayoutParams);
|
||||
windowManager.updateViewLayout(playerImpl.getRootView(), popupLayoutParams);
|
||||
}
|
||||
|
||||
protected void setRepeatModeRemote(final RemoteViews remoteViews, final int repeatMode) {
|
||||
@@ -364,13 +477,21 @@ public final class PopupVideoPlayer extends Service {
|
||||
}
|
||||
}
|
||||
|
||||
private void updateWindowFlags(final int flags) {
|
||||
if (popupLayoutParams == null || windowManager == null || playerImpl == null) return;
|
||||
|
||||
popupLayoutParams.flags = flags;
|
||||
windowManager.updateViewLayout(playerImpl.getRootView(), popupLayoutParams);
|
||||
}
|
||||
///////////////////////////////////////////////////////////////////////////
|
||||
|
||||
protected class VideoPlayerImpl extends VideoPlayer implements View.OnLayoutChangeListener {
|
||||
private TextView resizingIndicator;
|
||||
private ImageButton fullScreenButton;
|
||||
private ImageView videoPlayPause;
|
||||
|
||||
private View extraOptionsView;
|
||||
private View closingOverlayView;
|
||||
|
||||
@Override
|
||||
public void handleIntent(Intent intent) {
|
||||
@@ -390,21 +511,27 @@ 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);
|
||||
|
||||
extraOptionsView = rootView.findViewById(R.id.extraOptionsView);
|
||||
closingOverlayView = rootView.findViewById(R.id.closingOverlay);
|
||||
rootView.addOnLayoutChangeListener(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void initListeners() {
|
||||
super.initListeners();
|
||||
videoPlayPause.setOnClickListener(v -> onPlayPause());
|
||||
}
|
||||
|
||||
@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);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -421,21 +548,6 @@ public final class PopupVideoPlayer extends Service {
|
||||
super.destroy();
|
||||
}
|
||||
|
||||
@Override
|
||||
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, loadedImage);
|
||||
}
|
||||
|
||||
updateNotification(-1);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFullScreenButtonClicked() {
|
||||
super.onFullScreenButtonClicked();
|
||||
@@ -452,6 +564,7 @@ public final class PopupVideoPlayer extends Service {
|
||||
this.getRepeatMode(),
|
||||
this.getPlaybackSpeed(),
|
||||
this.getPlaybackPitch(),
|
||||
this.getPlaybackSkipSilence(),
|
||||
this.getPlaybackQuality()
|
||||
);
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||
@@ -464,7 +577,7 @@ public final class PopupVideoPlayer extends Service {
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||
}
|
||||
context.startActivity(intent);
|
||||
onClose();
|
||||
closePopup();
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -503,14 +616,47 @@ public final class PopupVideoPlayer extends Service {
|
||||
}
|
||||
|
||||
@Override
|
||||
protected int getDefaultResolutionIndex(final List<VideoStream> sortedVideos) {
|
||||
return ListHelper.getPopupDefaultResolutionIndex(context, sortedVideos);
|
||||
protected VideoPlaybackResolver.QualityResolver getQualityResolver() {
|
||||
return new VideoPlaybackResolver.QualityResolver() {
|
||||
@Override
|
||||
public int getDefaultResolutionIndex(List<VideoStream> sortedVideos) {
|
||||
return ListHelper.getPopupDefaultResolutionIndex(context, sortedVideos);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getOverrideResolutionIndex(List<VideoStream> sortedVideos,
|
||||
String playbackQuality) {
|
||||
return ListHelper.getPopupResolutionIndex(context, sortedVideos,
|
||||
playbackQuality);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Thumbnail Loading
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
@Override
|
||||
public void onLoadingComplete(String imageUri, View view, Bitmap loadedImage) {
|
||||
super.onLoadingComplete(imageUri, view, loadedImage);
|
||||
// rebuild notification here since remote view does not release bitmaps,
|
||||
// causing memory leaks
|
||||
resetNotification();
|
||||
updateNotification(-1);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected int getOverrideResolutionIndex(final List<VideoStream> sortedVideos,
|
||||
final String playbackQuality) {
|
||||
return ListHelper.getPopupDefaultResolutionIndex(context, sortedVideos, playbackQuality);
|
||||
public void onLoadingFailed(String imageUri, View view, FailReason failReason) {
|
||||
super.onLoadingFailed(imageUri, view, failReason);
|
||||
resetNotification();
|
||||
updateNotification(-1);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLoadingCancelled(String imageUri, View view) {
|
||||
super.onLoadingCancelled(imageUri, view);
|
||||
resetNotification();
|
||||
updateNotification(-1);
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
@@ -531,8 +677,8 @@ public final class PopupVideoPlayer extends Service {
|
||||
}
|
||||
|
||||
private void updateMetadata() {
|
||||
if (activityListener != null && currentInfo != null) {
|
||||
activityListener.onMetadataUpdate(currentInfo);
|
||||
if (activityListener != null && getCurrentMetadata() != null) {
|
||||
activityListener.onMetadataUpdate(getCurrentMetadata().getMetadata());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -564,8 +710,9 @@ public final class PopupVideoPlayer extends Service {
|
||||
public void onRepeatModeChanged(int i) {
|
||||
super.onRepeatModeChanged(i);
|
||||
setRepeatModeRemote(notRemoteView, i);
|
||||
updateNotification(-1);
|
||||
updatePlayback();
|
||||
resetNotification();
|
||||
updateNotification(-1);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -578,18 +725,17 @@ public final class PopupVideoPlayer extends Service {
|
||||
// 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);
|
||||
protected void onMetadataChanged(@NonNull final MediaSourceTag tag) {
|
||||
super.onMetadataChanged(tag);
|
||||
resetNotification();
|
||||
updateNotification(-1);
|
||||
updateMetadata();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPlaybackShutdown() {
|
||||
super.onPlaybackShutdown();
|
||||
onClose();
|
||||
closePopup();
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
@@ -615,7 +761,7 @@ public final class PopupVideoPlayer extends Service {
|
||||
if (DEBUG) Log.d(TAG, "onBroadcastReceived() called with: intent = [" + intent + "]");
|
||||
switch (intent.getAction()) {
|
||||
case ACTION_CLOSE:
|
||||
onClose();
|
||||
closePopup();
|
||||
break;
|
||||
case ACTION_PLAY_PAUSE:
|
||||
onPlayPause();
|
||||
@@ -645,44 +791,85 @@ public final class PopupVideoPlayer extends Service {
|
||||
@Override
|
||||
public void onBlocked() {
|
||||
super.onBlocked();
|
||||
resetNotification();
|
||||
updateNotification(R.drawable.ic_play_arrow_white);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPlaying() {
|
||||
super.onPlaying();
|
||||
updateNotification(R.drawable.ic_pause_white);
|
||||
lockManager.acquireWifiAndCpu();
|
||||
|
||||
updateWindowFlags(ONGOING_PLAYBACK_WINDOW_FLAGS);
|
||||
|
||||
resetNotification();
|
||||
updateNotification(R.drawable.ic_pause_white);
|
||||
|
||||
videoPlayPause.setBackgroundResource(R.drawable.ic_pause_white);
|
||||
hideControls(DEFAULT_CONTROLS_DURATION, DEFAULT_CONTROLS_HIDE_TIME);
|
||||
|
||||
startForeground(NOTIFICATION_ID, notBuilder.build());
|
||||
lockManager.acquireWifiAndCpu();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBuffering() {
|
||||
super.onBuffering();
|
||||
resetNotification();
|
||||
updateNotification(R.drawable.ic_play_arrow_white);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPaused() {
|
||||
super.onPaused();
|
||||
|
||||
updateWindowFlags(IDLE_WINDOW_FLAGS);
|
||||
|
||||
resetNotification();
|
||||
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();
|
||||
|
||||
stopForeground(false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPausedSeek() {
|
||||
super.onPausedSeek();
|
||||
resetNotification();
|
||||
updateNotification(R.drawable.ic_play_arrow_white);
|
||||
|
||||
videoPlayPause.setBackgroundResource(R.drawable.ic_pause_white);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCompleted() {
|
||||
super.onCompleted();
|
||||
|
||||
updateWindowFlags(IDLE_WINDOW_FLAGS);
|
||||
|
||||
resetNotification();
|
||||
updateNotification(R.drawable.ic_replay_white);
|
||||
showAndAnimateControl(R.drawable.ic_replay_white, false);
|
||||
|
||||
videoPlayPause.setBackgroundResource(R.drawable.ic_replay_white);
|
||||
lockManager.releaseWifiAndCpu();
|
||||
|
||||
stopForeground(false);
|
||||
}
|
||||
|
||||
@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);
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
@@ -691,8 +878,9 @@ public final class PopupVideoPlayer extends Service {
|
||||
|
||||
/*package-private*/ void enableVideoRenderer(final boolean enable) {
|
||||
final int videoRendererIndex = getRendererIndex(C.TRACK_TYPE_VIDEO);
|
||||
if (trackSelector != null && videoRendererIndex != RENDERER_UNAVAILABLE) {
|
||||
trackSelector.setRendererDisabled(videoRendererIndex, !enable);
|
||||
if (videoRendererIndex != RENDERER_UNAVAILABLE) {
|
||||
trackSelector.setParameters(trackSelector.buildUponParameters()
|
||||
.setRendererDisabled(videoRendererIndex, !enable));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -704,12 +892,15 @@ public final class PopupVideoPlayer extends Service {
|
||||
public TextView getResizingIndicator() {
|
||||
return resizingIndicator;
|
||||
}
|
||||
|
||||
public View getClosingOverlayView() {
|
||||
return closingOverlayView;
|
||||
}
|
||||
}
|
||||
|
||||
private class MySimpleOnGestureListener extends GestureDetector.SimpleOnGestureListener implements View.OnTouchListener {
|
||||
private class PopupWindowGestureListener extends GestureDetector.SimpleOnGestureListener implements View.OnTouchListener {
|
||||
private int initialPopupX, initialPopupY;
|
||||
private boolean isMoving;
|
||||
|
||||
private boolean isResizing;
|
||||
|
||||
@Override
|
||||
@@ -718,6 +909,8 @@ public final class PopupVideoPlayer extends Service {
|
||||
Log.d(TAG, "onDoubleTap() called with: e = [" + e + "]" + "rawXy = " + e.getRawX() + ", " + e.getRawY() + ", xy = " + e.getX() + ", " + e.getY());
|
||||
if (playerImpl == null || !playerImpl.isPlaying()) return false;
|
||||
|
||||
playerImpl.hideControls(0, 0);
|
||||
|
||||
if (e.getX() > popupWidth / 2) {
|
||||
playerImpl.onFastForward();
|
||||
} else {
|
||||
@@ -731,17 +924,27 @@ 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.onPlayPause();
|
||||
if (playerImpl.isControlsVisible()) {
|
||||
playerImpl.hideControls(100, 100);
|
||||
} else {
|
||||
playerImpl.showControlsThenHide();
|
||||
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onDown(MotionEvent e) {
|
||||
if (DEBUG) Log.d(TAG, "onDown() called with: e = [" + e + "]");
|
||||
initialPopupX = windowLayoutParams.x;
|
||||
initialPopupY = windowLayoutParams.y;
|
||||
popupWidth = windowLayoutParams.width;
|
||||
popupHeight = windowLayoutParams.height;
|
||||
|
||||
// Fix popup position when the user touch it, it may have the wrong one
|
||||
// because the soft input is visible (the draggable area is currently resized).
|
||||
checkPopupPositionBounds(closeOverlayView.getWidth(), closeOverlayView.getHeight());
|
||||
|
||||
initialPopupX = popupLayoutParams.x;
|
||||
initialPopupY = popupLayoutParams.y;
|
||||
popupWidth = popupLayoutParams.width;
|
||||
popupHeight = popupLayoutParams.height;
|
||||
return super.onDown(e);
|
||||
}
|
||||
|
||||
@@ -749,20 +952,22 @@ public final class PopupVideoPlayer extends Service {
|
||||
public void onLongPress(MotionEvent e) {
|
||||
if (DEBUG) Log.d(TAG, "onLongPress() called with: e = [" + e + "]");
|
||||
updateScreenSize();
|
||||
checkPositionBounds();
|
||||
checkPopupPositionBounds();
|
||||
updatePopupSize((int) screenWidth, -1);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
|
||||
if (isResizing || playerImpl == null) return super.onScroll(e1, e2, distanceX, distanceY);
|
||||
public boolean onScroll(MotionEvent initialEvent, MotionEvent movingEvent, float distanceX, float distanceY) {
|
||||
if (isResizing || playerImpl == null) return super.onScroll(initialEvent, movingEvent, distanceX, distanceY);
|
||||
|
||||
if (!isMoving) {
|
||||
animateView(closeOverlayButton, true, 200);
|
||||
}
|
||||
|
||||
if (playerImpl.getCurrentState() != BasePlayer.STATE_BUFFERING
|
||||
&& (!isMoving || playerImpl.getControlsRoot().getAlpha() != 1f)) playerImpl.showControls(0);
|
||||
isMoving = true;
|
||||
|
||||
float diffX = (int) (e2.getRawX() - e1.getRawX()), posX = (int) (initialPopupX + diffX);
|
||||
float diffY = (int) (e2.getRawY() - e1.getRawY()), posY = (int) (initialPopupY + diffY);
|
||||
float diffX = (int) (movingEvent.getRawX() - initialEvent.getRawX()), posX = (int) (initialPopupX + diffX);
|
||||
float diffY = (int) (movingEvent.getRawY() - initialEvent.getRawY()), posY = (int) (initialPopupY + diffY);
|
||||
|
||||
if (posX > (screenWidth - popupWidth)) posX = (int) (screenWidth - popupWidth);
|
||||
else if (posX < 0) posX = 0;
|
||||
@@ -770,26 +975,49 @@ public final class PopupVideoPlayer extends Service {
|
||||
if (posY > (screenHeight - popupHeight)) posY = (int) (screenHeight - popupHeight);
|
||||
else if (posY < 0) posY = 0;
|
||||
|
||||
windowLayoutParams.x = (int) posX;
|
||||
windowLayoutParams.y = (int) posY;
|
||||
popupLayoutParams.x = (int) posX;
|
||||
popupLayoutParams.y = (int) posY;
|
||||
|
||||
final View closingOverlayView = playerImpl.getClosingOverlayView();
|
||||
if (isInsideClosingRadius(movingEvent)) {
|
||||
if (closingOverlayView.getVisibility() == View.GONE) {
|
||||
animateView(closingOverlayView, true, 250);
|
||||
}
|
||||
} else {
|
||||
if (closingOverlayView.getVisibility() == View.VISIBLE) {
|
||||
animateView(closingOverlayView, false, 0);
|
||||
}
|
||||
}
|
||||
|
||||
//noinspection PointlessBooleanExpression
|
||||
if (DEBUG && false) Log.d(TAG, "PopupVideoPlayer.onScroll = " +
|
||||
", e1.getRaw = [" + e1.getRawX() + ", " + e1.getRawY() + "]" +
|
||||
", e2.getRaw = [" + e2.getRawX() + ", " + e2.getRawY() + "]" +
|
||||
", distanceXy = [" + distanceX + ", " + distanceY + "]" +
|
||||
", posXy = [" + posX + ", " + posY + "]" +
|
||||
", popupWh = [" + popupWidth + " x " + popupHeight + "]");
|
||||
windowManager.updateViewLayout(playerImpl.getRootView(), windowLayoutParams);
|
||||
if (DEBUG && false) {
|
||||
Log.d(TAG, "PopupVideoPlayer.onScroll = " +
|
||||
", e1.getRaw = [" + initialEvent.getRawX() + ", " + initialEvent.getRawY() + "]" + ", e1.getX,Y = [" + initialEvent.getX() + ", " + initialEvent.getY() + "]" +
|
||||
", e2.getRaw = [" + movingEvent.getRawX() + ", " + movingEvent.getRawY() + "]" + ", e2.getX,Y = [" + movingEvent.getX() + ", " + movingEvent.getY() + "]" +
|
||||
", distanceX,Y = [" + distanceX + ", " + distanceY + "]" +
|
||||
", posX,Y = [" + posX + ", " + posY + "]" +
|
||||
", popupW,H = [" + popupWidth + " x " + popupHeight + "]");
|
||||
}
|
||||
windowManager.updateViewLayout(playerImpl.getRootView(), popupLayoutParams);
|
||||
return true;
|
||||
}
|
||||
|
||||
private void onScrollEnd() {
|
||||
private void onScrollEnd(MotionEvent event) {
|
||||
if (DEBUG) Log.d(TAG, "onScrollEnd() called");
|
||||
if (playerImpl == null) return;
|
||||
if (playerImpl.isControlsVisible() && playerImpl.getCurrentState() == STATE_PLAYING) {
|
||||
playerImpl.hideControls(DEFAULT_CONTROLS_DURATION, DEFAULT_CONTROLS_HIDE_TIME);
|
||||
}
|
||||
|
||||
if (isInsideClosingRadius(event)) {
|
||||
closePopup();
|
||||
} else {
|
||||
animateView(playerImpl.getClosingOverlayView(), false, 0);
|
||||
|
||||
if (!isPopupClosing) {
|
||||
animateView(closeOverlayButton, false, 200);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -799,14 +1027,11 @@ public final class PopupVideoPlayer extends Service {
|
||||
|
||||
final float absVelocityX = Math.abs(velocityX);
|
||||
final float absVelocityY = Math.abs(velocityY);
|
||||
if (absVelocityX > shutdownFlingVelocity) {
|
||||
onClose();
|
||||
return true;
|
||||
} else if (Math.max(absVelocityX, absVelocityY) > tossFlingVelocity) {
|
||||
if (absVelocityX > tossFlingVelocity) windowLayoutParams.x = (int) velocityX;
|
||||
if (absVelocityY > tossFlingVelocity) windowLayoutParams.y = (int) velocityY;
|
||||
checkPositionBounds();
|
||||
windowManager.updateViewLayout(playerImpl.getRootView(), windowLayoutParams);
|
||||
if (Math.max(absVelocityX, absVelocityY) > tossFlingVelocity) {
|
||||
if (absVelocityX > tossFlingVelocity) popupLayoutParams.x = (int) velocityX;
|
||||
if (absVelocityY > tossFlingVelocity) popupLayoutParams.y = (int) velocityY;
|
||||
checkPopupPositionBounds();
|
||||
windowManager.updateViewLayout(playerImpl.getRootView(), popupLayoutParams);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
@@ -814,7 +1039,7 @@ public final class PopupVideoPlayer extends Service {
|
||||
|
||||
@Override
|
||||
public boolean onTouch(View v, MotionEvent event) {
|
||||
gestureDetector.onTouchEvent(event);
|
||||
popupGestureDetector.onTouchEvent(event);
|
||||
if (playerImpl == null) return false;
|
||||
if (event.getPointerCount() == 2 && !isResizing) {
|
||||
if (DEBUG) Log.d(TAG, "onTouch() 2 finger pointer detected, enabling resizing.");
|
||||
@@ -837,7 +1062,7 @@ public final class PopupVideoPlayer extends Service {
|
||||
Log.d(TAG, "onTouch() ACTION_UP > v = [" + v + "], e1.getRaw = [" + event.getRawX() + ", " + event.getRawY() + "]");
|
||||
if (isMoving) {
|
||||
isMoving = false;
|
||||
onScrollEnd();
|
||||
onScrollEnd(event);
|
||||
}
|
||||
|
||||
if (isResizing) {
|
||||
@@ -845,7 +1070,10 @@ public final class PopupVideoPlayer extends Service {
|
||||
animateView(playerImpl.getResizingIndicator(), false, 100, 0);
|
||||
playerImpl.changeState(playerImpl.getCurrentState());
|
||||
}
|
||||
savePositionAndSize();
|
||||
|
||||
if (!isPopupClosing) {
|
||||
savePositionAndSize();
|
||||
}
|
||||
}
|
||||
|
||||
v.performClick();
|
||||
@@ -861,13 +1089,13 @@ public final class PopupVideoPlayer extends Service {
|
||||
final float diff = Math.abs(firstPointerX - secondPointerX);
|
||||
if (firstPointerX > secondPointerX) {
|
||||
// second pointer is the anchor (the leftmost pointer)
|
||||
windowLayoutParams.x = (int) (event.getRawX() - diff);
|
||||
popupLayoutParams.x = (int) (event.getRawX() - diff);
|
||||
} else {
|
||||
// first pointer is the anchor
|
||||
windowLayoutParams.x = (int) event.getRawX();
|
||||
popupLayoutParams.x = (int) event.getRawX();
|
||||
}
|
||||
|
||||
checkPositionBounds();
|
||||
checkPopupPositionBounds();
|
||||
updateScreenSize();
|
||||
|
||||
final int width = (int) Math.min(screenWidth, diff);
|
||||
@@ -875,5 +1103,29 @@ public final class PopupVideoPlayer extends Service {
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Utils
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
private int distanceFromCloseButton(MotionEvent popupMotionEvent) {
|
||||
final int closeOverlayButtonX = closeOverlayButton.getLeft() + closeOverlayButton.getWidth() / 2;
|
||||
final int closeOverlayButtonY = closeOverlayButton.getTop() + closeOverlayButton.getHeight() / 2;
|
||||
|
||||
float fingerX = popupLayoutParams.x + popupMotionEvent.getX();
|
||||
float fingerY = popupLayoutParams.y + popupMotionEvent.getY();
|
||||
|
||||
return (int) Math.sqrt(Math.pow(closeOverlayButtonX - fingerX, 2) + Math.pow(closeOverlayButtonY - fingerY, 2));
|
||||
}
|
||||
|
||||
private float getClosingRadius() {
|
||||
final int buttonRadius = closeOverlayButton.getWidth() / 2;
|
||||
// 20% wider than the button itself
|
||||
return buttonRadius * 1.2f;
|
||||
}
|
||||
|
||||
private boolean isInsideClosingRadius(MotionEvent popupMotionEvent) {
|
||||
return distanceFromCloseButton(popupMotionEvent) <= getClosingRadius();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -16,6 +16,7 @@ import android.util.Log;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.ImageButton;
|
||||
import android.widget.LinearLayout;
|
||||
import android.widget.PopupMenu;
|
||||
@@ -29,17 +30,21 @@ 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.player.helper.PlaybackParameterDialog;
|
||||
import org.schabi.newpipe.playlist.PlayQueueItem;
|
||||
import org.schabi.newpipe.playlist.PlayQueueItemBuilder;
|
||||
import org.schabi.newpipe.playlist.PlayQueueItemHolder;
|
||||
import org.schabi.newpipe.playlist.PlayQueueItemTouchCallback;
|
||||
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;
|
||||
|
||||
@@ -151,7 +156,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);
|
||||
@@ -183,17 +188,11 @@ public abstract class ServicePlayerActivity extends AppCompatActivity
|
||||
this.player.getRepeatMode(),
|
||||
this.player.getPlaybackSpeed(),
|
||||
this.player.getPlaybackPitch(),
|
||||
this.player.getPlaybackSkipSilence(),
|
||||
null
|
||||
).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
|
||||
////////////////////////////////////////////////////////////////////////////
|
||||
@@ -319,7 +318,8 @@ public abstract class ServicePlayerActivity extends AppCompatActivity
|
||||
|
||||
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;
|
||||
|
||||
@@ -328,12 +328,27 @@ 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;
|
||||
});
|
||||
|
||||
final MenuItem share = menu.getMenu().add(RECYCLER_ITEM_POPUP_MENU_GROUP_ID, /*pos=*/3,
|
||||
Menu.NONE, R.string.share);
|
||||
share.setOnMenuItemClickListener(menuItem -> {
|
||||
shareUrl(item.getTitle(), item.getUrl());
|
||||
return true;
|
||||
});
|
||||
|
||||
menu.show();
|
||||
}
|
||||
|
||||
@@ -453,13 +468,16 @@ public abstract class ServicePlayerActivity extends AppCompatActivity
|
||||
|
||||
private void openPlaybackParameterDialog() {
|
||||
if (player == null) return;
|
||||
PlaybackParameterDialog.newInstance(player.getPlaybackSpeed(),
|
||||
player.getPlaybackPitch()).show(getSupportFragmentManager(), getTag());
|
||||
PlaybackParameterDialog.newInstance(player.getPlaybackSpeed(), player.getPlaybackPitch(),
|
||||
player.getPlaybackSkipSilence()).show(getSupportFragmentManager(), getTag());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPlaybackParameterChanged(float playbackTempo, float playbackPitch) {
|
||||
if (player != null) player.setPlaybackParameters(playbackTempo, playbackPitch);
|
||||
public void onPlaybackParameterChanged(float playbackTempo, float playbackPitch,
|
||||
boolean playbackSkipSilence) {
|
||||
if (player != null) {
|
||||
player.setPlaybackParameters(playbackTempo, playbackPitch, playbackSkipSilence);
|
||||
}
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////
|
||||
@@ -488,6 +506,33 @@ public abstract class ServicePlayerActivity extends AppCompatActivity
|
||||
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());
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////
|
||||
// Share
|
||||
////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
private void shareUrl(String subject, String url) {
|
||||
Intent intent = new Intent(Intent.ACTION_SEND);
|
||||
intent.setType("text/plain");
|
||||
intent.putExtra(Intent.EXTRA_SUBJECT, subject);
|
||||
intent.putExtra(Intent.EXTRA_TEXT, url);
|
||||
startActivity(Intent.createChooser(intent, getString(R.string.share_dialog_title)));
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////
|
||||
// Binding Service Listener
|
||||
////////////////////////////////////////////////////////////////////////////
|
||||
@@ -497,6 +542,7 @@ public abstract class ServicePlayerActivity extends AppCompatActivity
|
||||
onStateChanged(state);
|
||||
onPlayModeChanged(repeatMode, shuffled);
|
||||
onPlaybackParameterChanged(parameters);
|
||||
onMaybePlaybackAdapterChanged();
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -517,6 +563,12 @@ public abstract class ServicePlayerActivity extends AppCompatActivity
|
||||
if (player != null) {
|
||||
progressLiveSync.setClickable(!player.isLiveEdge());
|
||||
}
|
||||
|
||||
// this will make shure progressCurrentTime has the same width as progressEndTime
|
||||
final ViewGroup.LayoutParams endTimeParams = progressEndTime.getLayoutParams();
|
||||
final ViewGroup.LayoutParams currentTimeParams = progressCurrentTime.getLayoutParams();
|
||||
currentTimeParams.width = progressEndTime.getWidth();
|
||||
progressCurrentTime.setLayoutParams(currentTimeParams);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -609,4 +661,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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,10 +29,8 @@ import android.content.Intent;
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.Color;
|
||||
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;
|
||||
@@ -48,13 +46,12 @@ import android.widget.SeekBar;
|
||||
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.source.MediaSource;
|
||||
import com.google.android.exoplayer2.source.MergingMediaSource;
|
||||
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;
|
||||
@@ -62,21 +59,17 @@ 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.player.resolver.MediaSourceTag;
|
||||
import org.schabi.newpipe.player.resolver.VideoPlaybackResolver;
|
||||
import org.schabi.newpipe.util.AnimationUtils;
|
||||
import org.schabi.newpipe.util.ListHelper;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import static com.google.android.exoplayer2.C.SELECTION_FLAG_AUTOSELECT;
|
||||
import static com.google.android.exoplayer2.C.TIME_UNSET;
|
||||
import static org.schabi.newpipe.player.helper.PlayerHelper.formatSpeed;
|
||||
import static org.schabi.newpipe.player.helper.PlayerHelper.getTimeString;
|
||||
import static org.schabi.newpipe.util.AnimationUtils.animateView;
|
||||
@@ -105,13 +98,12 @@ public abstract class VideoPlayer extends BasePlayer
|
||||
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;
|
||||
private List<VideoStream> availableStreams;
|
||||
private int selectedStreamIndex;
|
||||
|
||||
protected String playbackQuality;
|
||||
|
||||
protected boolean wasPlaying = false;
|
||||
|
||||
@NonNull final private VideoPlaybackResolver resolver;
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Views
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
@@ -162,6 +154,7 @@ public abstract class VideoPlayer extends BasePlayer
|
||||
public VideoPlayer(String debugTag, Context context) {
|
||||
super(context);
|
||||
this.TAG = debugTag;
|
||||
this.resolver = new VideoPlaybackResolver(context, dataSource, getQualityResolver());
|
||||
}
|
||||
|
||||
public void setup(View rootView) {
|
||||
@@ -189,10 +182,10 @@ public abstract class VideoPlayer extends BasePlayer
|
||||
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()));
|
||||
@@ -214,7 +207,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() {
|
||||
@@ -228,8 +222,8 @@ public abstract class VideoPlayer extends BasePlayer
|
||||
}
|
||||
|
||||
@Override
|
||||
public void initPlayer() {
|
||||
super.initPlayer();
|
||||
public void initPlayer(final boolean playOnReady) {
|
||||
super.initPlayer(playOnReady);
|
||||
|
||||
// Setup video view
|
||||
simpleExoPlayer.setVideoSurfaceView(surfaceView);
|
||||
@@ -240,7 +234,8 @@ public abstract class VideoPlayer extends BasePlayer
|
||||
|
||||
// Setup audio session with onboard equalizer
|
||||
if (Build.VERSION.SDK_INT >= 21) {
|
||||
trackSelector.setTunnelingAudioSessionId(C.generateAudioSessionIdV21(context));
|
||||
trackSelector.setParameters(trackSelector.buildUponParameters()
|
||||
.setTunnelingAudioSessionId(C.generateAudioSessionIdV21(context)));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -296,8 +291,9 @@ public abstract class VideoPlayer extends BasePlayer
|
||||
0, Menu.NONE, R.string.caption_none);
|
||||
captionOffItem.setOnMenuItemClickListener(menuItem -> {
|
||||
final int textRendererIndex = getRendererIndex(C.TRACK_TYPE_TEXT);
|
||||
if (trackSelector != null && textRendererIndex != RENDERER_UNAVAILABLE) {
|
||||
trackSelector.setRendererDisabled(textRendererIndex, true);
|
||||
if (textRendererIndex != RENDERER_UNAVAILABLE) {
|
||||
trackSelector.setParameters(trackSelector.buildUponParameters()
|
||||
.setRendererDisabled(textRendererIndex, true));
|
||||
}
|
||||
return true;
|
||||
});
|
||||
@@ -309,68 +305,61 @@ public abstract class VideoPlayer extends BasePlayer
|
||||
i + 1, Menu.NONE, captionLanguage);
|
||||
captionItem.setOnMenuItemClickListener(menuItem -> {
|
||||
final int textRendererIndex = getRendererIndex(C.TRACK_TYPE_TEXT);
|
||||
if (trackSelector != null && textRendererIndex != RENDERER_UNAVAILABLE) {
|
||||
if (textRendererIndex != RENDERER_UNAVAILABLE) {
|
||||
trackSelector.setPreferredTextLanguage(captionLanguage);
|
||||
trackSelector.setRendererDisabled(textRendererIndex, false);
|
||||
trackSelector.setParameters(trackSelector.buildUponParameters()
|
||||
.setRendererDisabled(textRendererIndex, false));
|
||||
}
|
||||
return true;
|
||||
});
|
||||
}
|
||||
captionPopupMenu.setOnDismissListener(this);
|
||||
}
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Playback Listener
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
protected abstract int getDefaultResolutionIndex(final List<VideoStream> sortedVideos);
|
||||
|
||||
protected abstract int getOverrideResolutionIndex(final List<VideoStream> sortedVideos, final String playbackQuality);
|
||||
private void updateStreamRelatedViews() {
|
||||
if (getCurrentMetadata() == null) return;
|
||||
|
||||
final MediaSourceTag tag = getCurrentMetadata();
|
||||
final StreamInfo metadata = tag.getMetadata();
|
||||
|
||||
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);
|
||||
|
||||
playbackEndTime.setVisibility(View.GONE);
|
||||
playbackLiveSync.setVisibility(View.GONE);
|
||||
|
||||
final StreamType streamType = info == null ? StreamType.NONE : info.getStreamType();
|
||||
|
||||
switch (streamType) {
|
||||
switch (metadata.getStreamType()) {
|
||||
case AUDIO_STREAM:
|
||||
surfaceView.setVisibility(View.GONE);
|
||||
endScreen.setVisibility(View.VISIBLE);
|
||||
playbackEndTime.setVisibility(View.VISIBLE);
|
||||
break;
|
||||
|
||||
case AUDIO_LIVE_STREAM:
|
||||
surfaceView.setVisibility(View.GONE);
|
||||
endScreen.setVisibility(View.VISIBLE);
|
||||
playbackLiveSync.setVisibility(View.VISIBLE);
|
||||
break;
|
||||
|
||||
case LIVE_STREAM:
|
||||
surfaceView.setVisibility(View.VISIBLE);
|
||||
endScreen.setVisibility(View.GONE);
|
||||
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());
|
||||
}
|
||||
if (metadata.getVideoStreams().size() + metadata.getVideoOnlyStreams().size() == 0)
|
||||
break;
|
||||
|
||||
availableStreams = tag.getSortedAvailableVideoStreams();
|
||||
selectedStreamIndex = tag.getSelectedVideoStreamIndex();
|
||||
buildQualityMenu();
|
||||
qualityTextView.setVisibility(View.VISIBLE);
|
||||
|
||||
qualityTextView.setVisibility(View.VISIBLE);
|
||||
surfaceView.setVisibility(View.VISIBLE);
|
||||
default:
|
||||
endScreen.setVisibility(View.GONE);
|
||||
playbackEndTime.setVisibility(View.VISIBLE);
|
||||
break;
|
||||
}
|
||||
@@ -378,69 +367,21 @@ public abstract class VideoPlayer extends BasePlayer
|
||||
buildPlaybackSpeedMenu();
|
||||
playbackSpeedTextView.setVisibility(View.VISIBLE);
|
||||
}
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Playback Listener
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
protected abstract VideoPlaybackResolver.QualityResolver getQualityResolver();
|
||||
|
||||
protected void onMetadataChanged(@NonNull final MediaSourceTag tag) {
|
||||
super.onMetadataChanged(tag);
|
||||
updateStreamRelatedViews();
|
||||
}
|
||||
|
||||
@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.getVideoStreams(), info.getVideoOnlyStreams(), false);
|
||||
final int index;
|
||||
if (videos.isEmpty()) {
|
||||
index = -1;
|
||||
} else if (playbackQuality == null) {
|
||||
index = getDefaultResolutionIndex(videos);
|
||||
} else {
|
||||
index = getOverrideResolutionIndex(videos, getPlaybackQuality());
|
||||
}
|
||||
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);
|
||||
}
|
||||
|
||||
// Create optional audio stream source
|
||||
final List<AudioStream> audioStreams = info.getAudioStreams();
|
||||
final AudioStream audio = audioStreams.isEmpty() ? null : audioStreams.get(
|
||||
ListHelper.getDefaultAudioFormat(context, audioStreams));
|
||||
// Use the audio stream if there is no video stream, or
|
||||
// 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);
|
||||
}
|
||||
|
||||
// If there is no audio or video sources, then this media source cannot be played back
|
||||
if (mediaSources.isEmpty()) return null;
|
||||
// Below are auxiliary media sources
|
||||
|
||||
// Create subtitle sources
|
||||
for (final Subtitles subtitle : info.getSubtitles()) {
|
||||
final String mimeType = PlayerHelper.mimeTypesOf(subtitle.getFileType());
|
||||
if (mimeType == null) continue;
|
||||
|
||||
final Format textFormat = Format.createTextSampleFormat(null, mimeType,
|
||||
SELECTION_FLAG_AUTOSELECT, PlayerHelper.captionLanguageOf(context, subtitle));
|
||||
final MediaSource textSource = dataSource.getSampleMediaSourceFactory()
|
||||
.createMediaSource(Uri.parse(subtitle.getURL()), textFormat, TIME_UNSET);
|
||||
mediaSources.add(textSource);
|
||||
}
|
||||
|
||||
if (mediaSources.size() == 1) {
|
||||
return mediaSources.get(0);
|
||||
} else {
|
||||
return new MergingMediaSource(mediaSources.toArray(
|
||||
new MediaSource[mediaSources.size()]));
|
||||
}
|
||||
return resolver.resolve(info);
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
@@ -459,7 +400,6 @@ public abstract class VideoPlayer extends BasePlayer
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN)
|
||||
playbackSeekBar.getThumb().setColorFilter(Color.RED, PorterDuff.Mode.SRC_IN);
|
||||
|
||||
animateView(endScreen, false, 0);
|
||||
loadingPanel.setBackgroundColor(Color.BLACK);
|
||||
animateView(loadingPanel, true, 0);
|
||||
animateView(surfaceForeground, true, 100);
|
||||
@@ -469,6 +409,8 @@ public abstract class VideoPlayer extends BasePlayer
|
||||
public void onPlaying() {
|
||||
super.onPlaying();
|
||||
|
||||
updateStreamRelatedViews();
|
||||
|
||||
showAndAnimateControl(-1, true);
|
||||
|
||||
playbackSeekBar.setEnabled(true);
|
||||
@@ -479,14 +421,12 @@ public abstract class VideoPlayer extends BasePlayer
|
||||
loadingPanel.setVisibility(View.GONE);
|
||||
|
||||
animateView(currentDisplaySeek, AnimationUtils.Type.SCALE_AND_ALPHA, false, 200);
|
||||
animateView(endScreen, false, 0);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBuffering() {
|
||||
if (DEBUG) Log.d(TAG, "onBuffering() called");
|
||||
loadingPanel.setBackgroundColor(Color.TRANSPARENT);
|
||||
animateView(loadingPanel, true, 500);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -551,8 +491,7 @@ public abstract class VideoPlayer extends BasePlayer
|
||||
final int textRenderer = getRendererIndex(C.TRACK_TYPE_TEXT);
|
||||
|
||||
if (captionTextView == null) return;
|
||||
if (trackSelector == null || trackSelector.getCurrentMappedTrackInfo() == null ||
|
||||
textRenderer == RENDERER_UNAVAILABLE) {
|
||||
if (trackSelector.getCurrentMappedTrackInfo() == null || textRenderer == RENDERER_UNAVAILABLE) {
|
||||
captionTextView.setVisibility(View.GONE);
|
||||
return;
|
||||
}
|
||||
@@ -574,8 +513,8 @@ public abstract class VideoPlayer extends BasePlayer
|
||||
|
||||
// Build UI
|
||||
buildCaptionMenu(availableLanguages);
|
||||
if (trackSelector.getRendererDisabled(textRenderer) || preferredLanguage == null ||
|
||||
!availableLanguages.contains(preferredLanguage)) {
|
||||
if (trackSelector.getParameters().getRendererDisabled(textRenderer) ||
|
||||
preferredLanguage == null || !availableLanguages.contains(preferredLanguage)) {
|
||||
captionTextView.setText(R.string.caption_none);
|
||||
} else {
|
||||
captionTextView.setText(preferredLanguage);
|
||||
@@ -886,16 +825,30 @@ public abstract class VideoPlayer extends BasePlayer
|
||||
() -> 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
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
public void setPlaybackQuality(final String quality) {
|
||||
this.playbackQuality = quality;
|
||||
this.resolver.setPlaybackQuality(quality);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public String getPlaybackQuality() {
|
||||
return playbackQuality;
|
||||
return resolver.getPlaybackQuality();
|
||||
}
|
||||
|
||||
public AspectRatioFrameLayout getAspectRatioFrameLayout() {
|
||||
|
||||
@@ -1,13 +1,16 @@
|
||||
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.DummyPlaybackPreparer;
|
||||
import org.schabi.newpipe.player.mediasession.MediaSessionCallback;
|
||||
import org.schabi.newpipe.player.mediasession.PlayQueueNavigator;
|
||||
import org.schabi.newpipe.player.mediasession.PlayQueuePlaybackController;
|
||||
@@ -15,24 +18,34 @@ import org.schabi.newpipe.player.mediasession.PlayQueuePlaybackController;
|
||||
public class MediaSessionManager {
|
||||
private static final String TAG = "MediaSessionManager";
|
||||
|
||||
private final MediaSessionCompat mediaSession;
|
||||
private final MediaSessionConnector sessionConnector;
|
||||
@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, new DummyPlaybackPreparer());
|
||||
this.sessionConnector.setPlayer(player, null);
|
||||
}
|
||||
|
||||
public MediaSessionCompat getMediaSession() {
|
||||
return mediaSession;
|
||||
@Nullable
|
||||
@SuppressWarnings("UnusedReturnValue")
|
||||
public KeyEvent handleMediaButtonIntent(final Intent intent) {
|
||||
return MediaButtonReceiver.handleIntent(mediaSession, intent);
|
||||
}
|
||||
|
||||
public MediaSessionConnector getSessionConnector() {
|
||||
return sessionConnector;
|
||||
}
|
||||
/**
|
||||
* Should be called on player destruction to prevent leakage.
|
||||
* */
|
||||
public void dispose() {
|
||||
this.sessionConnector.setPlayer(null, null);
|
||||
this.sessionConnector.setQueueNavigator(null);
|
||||
this.mediaSession.setActive(false);
|
||||
this.mediaSession.release();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,25 +21,34 @@ 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;
|
||||
// Minimum allowable range in ExoPlayer
|
||||
public static final double MINIMUM_PLAYBACK_VALUE = 0.10f;
|
||||
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 STEP_ONE_PERCENT_VALUE = 0.01f;
|
||||
public static final double STEP_FIVE_PERCENT_VALUE = 0.05f;
|
||||
public static final double STEP_TEN_PERCENT_VALUE = 0.10f;
|
||||
public static final double STEP_TWENTY_FIVE_PERCENT_VALUE = 0.25f;
|
||||
public static final double STEP_ONE_HUNDRED_PERCENT_VALUE = 1.00f;
|
||||
|
||||
public static final double DEFAULT_TEMPO = 1.00f;
|
||||
public static final double DEFAULT_PITCH = 1.00f;
|
||||
public static final double DEFAULT_STEP = STEP_TWENTY_FIVE_PERCENT_VALUE;
|
||||
public static final boolean DEFAULT_SKIP_SILENCE = false;
|
||||
|
||||
@NonNull private static final String INITIAL_TEMPO_KEY = "initial_tempo_key";
|
||||
@NonNull private static final String INITIAL_PITCH_KEY = "initial_pitch_key";
|
||||
|
||||
@NonNull private static final String TEMPO_KEY = "tempo_key";
|
||||
@NonNull private static final String PITCH_KEY = "pitch_key";
|
||||
@NonNull private static final String STEP_SIZE_KEY = "step_size_key";
|
||||
|
||||
public interface Callback {
|
||||
void onPlaybackParameterChanged(final float playbackTempo, final float playbackPitch);
|
||||
void onPlaybackParameterChanged(final float playbackTempo, final float playbackPitch,
|
||||
final boolean playbackSkipSilence);
|
||||
}
|
||||
|
||||
@Nullable private Callback callback;
|
||||
@@ -50,6 +59,11 @@ public class PlaybackParameterDialog extends DialogFragment {
|
||||
|
||||
private double initialTempo = DEFAULT_TEMPO;
|
||||
private double initialPitch = DEFAULT_PITCH;
|
||||
private boolean initialSkipSilence = DEFAULT_SKIP_SILENCE;
|
||||
|
||||
private double tempo = DEFAULT_TEMPO;
|
||||
private double pitch = DEFAULT_PITCH;
|
||||
private double stepSize = DEFAULT_STEP;
|
||||
|
||||
@Nullable private SeekBar tempoSlider;
|
||||
@Nullable private TextView tempoMinimumText;
|
||||
@@ -65,16 +79,26 @@ public class PlaybackParameterDialog extends DialogFragment {
|
||||
@Nullable private TextView pitchStepDownText;
|
||||
@Nullable private TextView pitchStepUpText;
|
||||
|
||||
@Nullable private CheckBox unhookingCheckbox;
|
||||
@Nullable private TextView stepSizeOnePercentText;
|
||||
@Nullable private TextView stepSizeFivePercentText;
|
||||
@Nullable private TextView stepSizeTenPercentText;
|
||||
@Nullable private TextView stepSizeTwentyFivePercentText;
|
||||
@Nullable private TextView stepSizeOneHundredPercentText;
|
||||
|
||||
@Nullable private TextView nightCorePresetText;
|
||||
@Nullable private TextView resetPresetText;
|
||||
@Nullable private CheckBox unhookingCheckbox;
|
||||
@Nullable private CheckBox skipSilenceCheckbox;
|
||||
|
||||
public static PlaybackParameterDialog newInstance(final double playbackTempo,
|
||||
final double playbackPitch) {
|
||||
final double playbackPitch,
|
||||
final boolean playbackSkipSilence) {
|
||||
PlaybackParameterDialog dialog = new PlaybackParameterDialog();
|
||||
dialog.initialTempo = playbackTempo;
|
||||
dialog.initialPitch = playbackPitch;
|
||||
|
||||
dialog.tempo = playbackTempo;
|
||||
dialog.pitch = playbackPitch;
|
||||
|
||||
dialog.initialSkipSilence = playbackSkipSilence;
|
||||
return dialog;
|
||||
}
|
||||
|
||||
@@ -98,6 +122,10 @@ public class PlaybackParameterDialog extends DialogFragment {
|
||||
if (savedInstanceState != null) {
|
||||
initialTempo = savedInstanceState.getDouble(INITIAL_TEMPO_KEY, DEFAULT_TEMPO);
|
||||
initialPitch = savedInstanceState.getDouble(INITIAL_PITCH_KEY, DEFAULT_PITCH);
|
||||
|
||||
tempo = savedInstanceState.getDouble(TEMPO_KEY, DEFAULT_TEMPO);
|
||||
pitch = savedInstanceState.getDouble(PITCH_KEY, DEFAULT_PITCH);
|
||||
stepSize = savedInstanceState.getDouble(STEP_SIZE_KEY, DEFAULT_STEP);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -106,6 +134,10 @@ public class PlaybackParameterDialog extends DialogFragment {
|
||||
super.onSaveInstanceState(outState);
|
||||
outState.putDouble(INITIAL_TEMPO_KEY, initialTempo);
|
||||
outState.putDouble(INITIAL_PITCH_KEY, initialPitch);
|
||||
|
||||
outState.putDouble(TEMPO_KEY, getCurrentTempo());
|
||||
outState.putDouble(PITCH_KEY, getCurrentPitch());
|
||||
outState.putDouble(STEP_SIZE_KEY, getCurrentStepSize());
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
@@ -123,7 +155,9 @@ public class PlaybackParameterDialog extends DialogFragment {
|
||||
.setView(view)
|
||||
.setCancelable(true)
|
||||
.setNegativeButton(R.string.cancel, (dialogInterface, i) ->
|
||||
setPlaybackParameters(initialTempo, initialPitch))
|
||||
setPlaybackParameters(initialTempo, initialPitch, initialSkipSilence))
|
||||
.setNeutralButton(R.string.playback_reset, (dialogInterface, i) ->
|
||||
setPlaybackParameters(DEFAULT_TEMPO, DEFAULT_PITCH, DEFAULT_SKIP_SILENCE))
|
||||
.setPositiveButton(R.string.finish, (dialogInterface, i) ->
|
||||
setCurrentPlaybackParameters());
|
||||
|
||||
@@ -136,9 +170,13 @@ public class PlaybackParameterDialog extends DialogFragment {
|
||||
|
||||
private void setupControlViews(@NonNull View rootView) {
|
||||
setupHookingControl(rootView);
|
||||
setupSkipSilenceControl(rootView);
|
||||
|
||||
setupTempoControl(rootView);
|
||||
setupPitchControl(rootView);
|
||||
setupPresetControl(rootView);
|
||||
|
||||
changeStepSize(stepSize);
|
||||
setupStepSizeSelector(rootView);
|
||||
}
|
||||
|
||||
private void setupTempoControl(@NonNull View rootView) {
|
||||
@@ -150,31 +188,15 @@ public class PlaybackParameterDialog extends DialogFragment {
|
||||
tempoStepDownText = rootView.findViewById(R.id.tempoStepDown);
|
||||
|
||||
if (tempoCurrentText != null)
|
||||
tempoCurrentText.setText(PlayerHelper.formatSpeed(initialTempo));
|
||||
tempoCurrentText.setText(PlayerHelper.formatSpeed(tempo));
|
||||
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.setProgress(strategy.progressOf(tempo));
|
||||
tempoSlider.setOnSeekBarChangeListener(getOnTempoChangedListener());
|
||||
}
|
||||
}
|
||||
@@ -188,31 +210,15 @@ public class PlaybackParameterDialog extends DialogFragment {
|
||||
pitchStepUpText = rootView.findViewById(R.id.pitchStepUp);
|
||||
|
||||
if (pitchCurrentText != null)
|
||||
pitchCurrentText.setText(PlayerHelper.formatPitch(initialPitch));
|
||||
pitchCurrentText.setText(PlayerHelper.formatPitch(pitch));
|
||||
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.setProgress(strategy.progressOf(pitch));
|
||||
pitchSlider.setOnSeekBarChangeListener(getOnPitchChangedListener());
|
||||
}
|
||||
}
|
||||
@@ -220,7 +226,7 @@ public class PlaybackParameterDialog extends DialogFragment {
|
||||
private void setupHookingControl(@NonNull View rootView) {
|
||||
unhookingCheckbox = rootView.findViewById(R.id.unhookCheckbox);
|
||||
if (unhookingCheckbox != null) {
|
||||
unhookingCheckbox.setChecked(initialPitch != initialTempo);
|
||||
unhookingCheckbox.setChecked(pitch != tempo);
|
||||
unhookingCheckbox.setOnCheckedChangeListener((compoundButton, isChecked) -> {
|
||||
if (isChecked) return;
|
||||
// When unchecked, slide back to the minimum of current tempo or pitch
|
||||
@@ -231,24 +237,84 @@ public class PlaybackParameterDialog extends DialogFragment {
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
private void setupSkipSilenceControl(@NonNull View rootView) {
|
||||
skipSilenceCheckbox = rootView.findViewById(R.id.skipSilenceCheckbox);
|
||||
if (skipSilenceCheckbox != null) {
|
||||
skipSilenceCheckbox.setChecked(initialSkipSilence);
|
||||
skipSilenceCheckbox.setOnCheckedChangeListener((compoundButton, isChecked) ->
|
||||
setCurrentPlaybackParameters());
|
||||
}
|
||||
}
|
||||
|
||||
setTempoSlider(NIGHTCORE_TEMPO);
|
||||
setPitchSlider(randomPitch);
|
||||
private void setupStepSizeSelector(@NonNull final View rootView) {
|
||||
stepSizeOnePercentText = rootView.findViewById(R.id.stepSizeOnePercent);
|
||||
stepSizeFivePercentText = rootView.findViewById(R.id.stepSizeFivePercent);
|
||||
stepSizeTenPercentText = rootView.findViewById(R.id.stepSizeTenPercent);
|
||||
stepSizeTwentyFivePercentText = rootView.findViewById(R.id.stepSizeTwentyFivePercent);
|
||||
stepSizeOneHundredPercentText = rootView.findViewById(R.id.stepSizeOneHundredPercent);
|
||||
|
||||
if (stepSizeOnePercentText != null) {
|
||||
stepSizeOnePercentText.setText(getPercentString(STEP_ONE_PERCENT_VALUE));
|
||||
stepSizeOnePercentText.setOnClickListener(view ->
|
||||
changeStepSize(STEP_ONE_PERCENT_VALUE));
|
||||
}
|
||||
|
||||
if (stepSizeFivePercentText != null) {
|
||||
stepSizeFivePercentText.setText(getPercentString(STEP_FIVE_PERCENT_VALUE));
|
||||
stepSizeFivePercentText.setOnClickListener(view ->
|
||||
changeStepSize(STEP_FIVE_PERCENT_VALUE));
|
||||
}
|
||||
|
||||
if (stepSizeTenPercentText != null) {
|
||||
stepSizeTenPercentText.setText(getPercentString(STEP_TEN_PERCENT_VALUE));
|
||||
stepSizeTenPercentText.setOnClickListener(view ->
|
||||
changeStepSize(STEP_TEN_PERCENT_VALUE));
|
||||
}
|
||||
|
||||
if (stepSizeTwentyFivePercentText != null) {
|
||||
stepSizeTwentyFivePercentText.setText(getPercentString(STEP_TWENTY_FIVE_PERCENT_VALUE));
|
||||
stepSizeTwentyFivePercentText.setOnClickListener(view ->
|
||||
changeStepSize(STEP_TWENTY_FIVE_PERCENT_VALUE));
|
||||
}
|
||||
|
||||
if (stepSizeOneHundredPercentText != null) {
|
||||
stepSizeOneHundredPercentText.setText(getPercentString(STEP_ONE_HUNDRED_PERCENT_VALUE));
|
||||
stepSizeOneHundredPercentText.setOnClickListener(view ->
|
||||
changeStepSize(STEP_ONE_HUNDRED_PERCENT_VALUE));
|
||||
}
|
||||
}
|
||||
|
||||
private void changeStepSize(final double stepSize) {
|
||||
this.stepSize = stepSize;
|
||||
|
||||
if (tempoStepUpText != null) {
|
||||
tempoStepUpText.setText(getStepUpPercentString(stepSize));
|
||||
tempoStepUpText.setOnClickListener(view -> {
|
||||
onTempoSliderUpdated(getCurrentTempo() + stepSize);
|
||||
setCurrentPlaybackParameters();
|
||||
});
|
||||
}
|
||||
|
||||
resetPresetText = rootView.findViewById(R.id.presetReset);
|
||||
if (resetPresetText != null) {
|
||||
resetPresetText.setOnClickListener(view -> {
|
||||
setTempoSlider(DEFAULT_TEMPO);
|
||||
setPitchSlider(DEFAULT_PITCH);
|
||||
if (tempoStepDownText != null) {
|
||||
tempoStepDownText.setText(getStepDownPercentString(stepSize));
|
||||
tempoStepDownText.setOnClickListener(view -> {
|
||||
onTempoSliderUpdated(getCurrentTempo() - stepSize);
|
||||
setCurrentPlaybackParameters();
|
||||
});
|
||||
}
|
||||
|
||||
if (pitchStepUpText != null) {
|
||||
pitchStepUpText.setText(getStepUpPercentString(stepSize));
|
||||
pitchStepUpText.setOnClickListener(view -> {
|
||||
onPitchSliderUpdated(getCurrentPitch() + stepSize);
|
||||
setCurrentPlaybackParameters();
|
||||
});
|
||||
}
|
||||
|
||||
if (pitchStepDownText != null) {
|
||||
pitchStepDownText.setText(getStepDownPercentString(stepSize));
|
||||
pitchStepDownText.setOnClickListener(view -> {
|
||||
onPitchSliderUpdated(getCurrentPitch() - stepSize);
|
||||
setCurrentPlaybackParameters();
|
||||
});
|
||||
}
|
||||
@@ -342,10 +408,11 @@ public class PlaybackParameterDialog extends DialogFragment {
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
private void setCurrentPlaybackParameters() {
|
||||
setPlaybackParameters(getCurrentTempo(), getCurrentPitch());
|
||||
setPlaybackParameters(getCurrentTempo(), getCurrentPitch(), getCurrentSkipSilence());
|
||||
}
|
||||
|
||||
private void setPlaybackParameters(final double tempo, final double pitch) {
|
||||
private void setPlaybackParameters(final double tempo, final double pitch,
|
||||
final boolean skipSilence) {
|
||||
if (callback != null && tempoCurrentText != null && pitchCurrentText != null) {
|
||||
if (DEBUG) Log.d(TAG, "Setting playback parameters to " +
|
||||
"tempo=[" + tempo + "], " +
|
||||
@@ -353,27 +420,40 @@ public class PlaybackParameterDialog extends DialogFragment {
|
||||
|
||||
tempoCurrentText.setText(PlayerHelper.formatSpeed(tempo));
|
||||
pitchCurrentText.setText(PlayerHelper.formatPitch(pitch));
|
||||
callback.onPlaybackParameterChanged((float) tempo, (float) pitch);
|
||||
callback.onPlaybackParameterChanged((float) tempo, (float) pitch, skipSilence);
|
||||
}
|
||||
}
|
||||
|
||||
private double getCurrentTempo() {
|
||||
return tempoSlider == null ? initialTempo : strategy.valueOf(
|
||||
return tempoSlider == null ? tempo : strategy.valueOf(
|
||||
tempoSlider.getProgress());
|
||||
}
|
||||
|
||||
private double getCurrentPitch() {
|
||||
return pitchSlider == null ? initialPitch : strategy.valueOf(
|
||||
return pitchSlider == null ? pitch : strategy.valueOf(
|
||||
pitchSlider.getProgress());
|
||||
}
|
||||
|
||||
private double getCurrentStepSize() {
|
||||
return stepSize;
|
||||
}
|
||||
|
||||
private boolean getCurrentSkipSilence() {
|
||||
return skipSilenceCheckbox != null && skipSilenceCheckbox.isChecked();
|
||||
}
|
||||
|
||||
@NonNull
|
||||
private static String getStepUpPercentString(final double percent) {
|
||||
return STEP_UP_SIGN + PlayerHelper.formatPitch(percent);
|
||||
return STEP_UP_SIGN + getPercentString(percent);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
private static String getStepDownPercentString(final double percent) {
|
||||
return STEP_DOWN_SIGN + PlayerHelper.formatPitch(percent);
|
||||
return STEP_DOWN_SIGN + getPercentString(percent);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
private static String getPercentString(final double percent) {
|
||||
return PlayerHelper.formatPitch(percent);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,12 +2,19 @@ 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.IntDef;
|
||||
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;
|
||||
@@ -18,10 +25,11 @@ 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.playlist.PlayQueue;
|
||||
import org.schabi.newpipe.playlist.PlayQueueItem;
|
||||
import org.schabi.newpipe.playlist.SinglePlayQueue;
|
||||
import org.schabi.newpipe.player.playqueue.PlayQueue;
|
||||
import org.schabi.newpipe.player.playqueue.PlayQueueItem;
|
||||
import org.schabi.newpipe.player.playqueue.SinglePlayQueue;
|
||||
|
||||
import java.lang.annotation.Retention;
|
||||
import java.text.DecimalFormat;
|
||||
import java.text.NumberFormat;
|
||||
import java.util.ArrayList;
|
||||
@@ -31,10 +39,13 @@ import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
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_ZOOM;
|
||||
import static java.lang.annotation.RetentionPolicy.SOURCE;
|
||||
import static org.schabi.newpipe.player.helper.PlayerHelper.MinimizeMode.*;
|
||||
|
||||
public class PlayerHelper {
|
||||
private PlayerHelper() {}
|
||||
@@ -44,6 +55,14 @@ public class PlayerHelper {
|
||||
private static final NumberFormat speedFormatter = new DecimalFormat("0.##x");
|
||||
private static final NumberFormat pitchFormatter = new DecimalFormat("##%");
|
||||
|
||||
@Retention(SOURCE)
|
||||
@IntDef({MINIMIZE_ON_EXIT_MODE_NONE, MINIMIZE_ON_EXIT_MODE_BACKGROUND,
|
||||
MINIMIZE_ON_EXIT_MODE_POPUP})
|
||||
public @interface MinimizeMode {
|
||||
int MINIMIZE_ON_EXIT_MODE_NONE = 0;
|
||||
int MINIMIZE_ON_EXIT_MODE_BACKGROUND = 1;
|
||||
int MINIMIZE_ON_EXIT_MODE_POPUP = 2;
|
||||
}
|
||||
////////////////////////////////////////////////////////////////////////////
|
||||
// Exposed helpers
|
||||
////////////////////////////////////////////////////////////////////////////
|
||||
@@ -166,6 +185,22 @@ public class PlayerHelper {
|
||||
return isAutoQueueEnabled(context, false);
|
||||
}
|
||||
|
||||
@MinimizeMode
|
||||
public static int getMinimizeOnExitAction(@NonNull final Context context) {
|
||||
final String defaultAction = context.getString(R.string.minimize_on_exit_none_key);
|
||||
final String popupAction = context.getString(R.string.minimize_on_exit_popup_key);
|
||||
final String backgroundAction = context.getString(R.string.minimize_on_exit_background_key);
|
||||
|
||||
final String action = getMinimizeOnExitAction(context, defaultAction);
|
||||
if (action.equals(popupAction)) {
|
||||
return MINIMIZE_ON_EXIT_MODE_POPUP;
|
||||
} else if (action.equals(backgroundAction)) {
|
||||
return MINIMIZE_ON_EXIT_MODE_BACKGROUND;
|
||||
} else {
|
||||
return MINIMIZE_ON_EXIT_MODE_NONE;
|
||||
}
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public static SeekParameters getSeekParameters(@NonNull final Context context) {
|
||||
return isUsingInexactSeek(context, false) ?
|
||||
@@ -203,18 +238,61 @@ public class PlayerHelper {
|
||||
return 60000;
|
||||
}
|
||||
|
||||
public static TrackSelection.Factory getQualitySelector(@NonNull final Context context,
|
||||
@NonNull final BandwidthMeter meter) {
|
||||
return new AdaptiveTrackSelection.Factory(meter,
|
||||
/*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) {
|
||||
return 10000;
|
||||
}
|
||||
|
||||
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
|
||||
* */
|
||||
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
|
||||
////////////////////////////////////////////////////////////////////////////
|
||||
@@ -247,4 +325,29 @@ public class PlayerHelper {
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
private static String getMinimizeOnExitAction(@NonNull final Context context,
|
||||
final String key) {
|
||||
return getPreferences(context).getString(context.getString(R.string.minimize_on_exit_key),
|
||||
key);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,45 +0,0 @@
|
||||
package org.schabi.newpipe.player.mediasession;
|
||||
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
import android.os.ResultReceiver;
|
||||
|
||||
import com.google.android.exoplayer2.Player;
|
||||
import com.google.android.exoplayer2.ext.mediasession.MediaSessionConnector;
|
||||
|
||||
public class DummyPlaybackPreparer implements MediaSessionConnector.PlaybackPreparer {
|
||||
@Override
|
||||
public long getSupportedPrepareActions() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPrepare() {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPrepareFromMediaId(String mediaId, Bundle extras) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPrepareFromSearch(String query, Bundle extras) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPrepareFromUri(Uri uri, Bundle extras) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public String[] getCommands() {
|
||||
return new String[0];
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCommand(Player player, String command, Bundle extras, ResultReceiver cb) {
|
||||
|
||||
}
|
||||
}
|
||||
@@ -13,5 +13,4 @@ public interface MediaSessionCallback {
|
||||
|
||||
void onPlay();
|
||||
void onPause();
|
||||
void onSetShuffle(final boolean isShuffled);
|
||||
}
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
package org.schabi.newpipe.player.mediasession;
|
||||
|
||||
import android.support.v4.media.session.PlaybackStateCompat;
|
||||
|
||||
import com.google.android.exoplayer2.Player;
|
||||
import com.google.android.exoplayer2.ext.mediasession.DefaultPlaybackController;
|
||||
|
||||
@@ -22,10 +20,4 @@ public class PlayQueuePlaybackController extends DefaultPlaybackController {
|
||||
public void onPause(Player player) {
|
||||
callback.onPause();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSetShuffleMode(Player player, int shuffleMode) {
|
||||
callback.onSetShuffle(shuffleMode == PlaybackStateCompat.SHUFFLE_MODE_ALL
|
||||
|| shuffleMode == PlaybackStateCompat.SHUFFLE_MODE_GROUP);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,23 +4,46 @@ import android.support.annotation.NonNull;
|
||||
import android.util.Log;
|
||||
|
||||
import com.google.android.exoplayer2.ExoPlayer;
|
||||
import com.google.android.exoplayer2.source.BaseMediaSource;
|
||||
import com.google.android.exoplayer2.source.MediaPeriod;
|
||||
import com.google.android.exoplayer2.upstream.Allocator;
|
||||
|
||||
import org.schabi.newpipe.playlist.PlayQueueItem;
|
||||
import org.schabi.newpipe.player.playqueue.PlayQueueItem;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
public class FailedMediaSource implements ManagedMediaSource {
|
||||
public class FailedMediaSource extends BaseMediaSource implements ManagedMediaSource {
|
||||
private final String TAG = "FailedMediaSource@" + Integer.toHexString(hashCode());
|
||||
|
||||
public static class FailedMediaSourceException extends Exception {
|
||||
FailedMediaSourceException(String message) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
FailedMediaSourceException(Throwable cause) {
|
||||
super(cause);
|
||||
}
|
||||
}
|
||||
|
||||
public static final class MediaSourceResolutionException extends FailedMediaSourceException {
|
||||
public MediaSourceResolutionException(String message) {
|
||||
super(message);
|
||||
}
|
||||
}
|
||||
|
||||
public static final class StreamInfoLoadException extends FailedMediaSourceException {
|
||||
public StreamInfoLoadException(Throwable cause) {
|
||||
super(cause);
|
||||
}
|
||||
}
|
||||
|
||||
private final PlayQueueItem playQueueItem;
|
||||
private final Throwable error;
|
||||
private final FailedMediaSourceException error;
|
||||
|
||||
private final long retryTimestamp;
|
||||
|
||||
public FailedMediaSource(@NonNull final PlayQueueItem playQueueItem,
|
||||
@NonNull final Throwable error,
|
||||
@NonNull final FailedMediaSourceException error,
|
||||
final long retryTimestamp) {
|
||||
this.playQueueItem = playQueueItem;
|
||||
this.error = error;
|
||||
@@ -32,7 +55,7 @@ public class FailedMediaSource implements ManagedMediaSource {
|
||||
* The error will always be propagated to ExoPlayer.
|
||||
* */
|
||||
public FailedMediaSource(@NonNull final PlayQueueItem playQueueItem,
|
||||
@NonNull final Throwable error) {
|
||||
@NonNull final FailedMediaSourceException error) {
|
||||
this.playQueueItem = playQueueItem;
|
||||
this.error = error;
|
||||
this.retryTimestamp = Long.MAX_VALUE;
|
||||
@@ -42,7 +65,7 @@ public class FailedMediaSource implements ManagedMediaSource {
|
||||
return playQueueItem;
|
||||
}
|
||||
|
||||
public Throwable getError() {
|
||||
public FailedMediaSourceException getError() {
|
||||
return error;
|
||||
}
|
||||
|
||||
@@ -50,11 +73,6 @@ public class FailedMediaSource implements ManagedMediaSource {
|
||||
return System.currentTimeMillis() >= retryTimestamp;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void prepareSource(ExoPlayer player, boolean isTopLevelSource, Listener listener) {
|
||||
Log.e(TAG, "Loading failed source: ", error);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void maybeThrowSourceInfoRefreshError() throws IOException {
|
||||
throw new IOException(error);
|
||||
@@ -68,8 +86,14 @@ public class FailedMediaSource implements ManagedMediaSource {
|
||||
@Override
|
||||
public void releasePeriod(MediaPeriod mediaPeriod) {}
|
||||
|
||||
|
||||
@Override
|
||||
public void releaseSource() {}
|
||||
protected void prepareSourceInternal(ExoPlayer player, boolean isTopLevelSource) {
|
||||
Log.e(TAG, "Loading failed source: ", error);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void releaseSourceInternal() {}
|
||||
|
||||
@Override
|
||||
public boolean shouldBeReplacedWith(@NonNull final PlayQueueItem newIdentity,
|
||||
|
||||
@@ -1,13 +1,15 @@
|
||||
package org.schabi.newpipe.player.mediasource;
|
||||
|
||||
import android.os.Handler;
|
||||
import android.support.annotation.NonNull;
|
||||
|
||||
import com.google.android.exoplayer2.ExoPlayer;
|
||||
import com.google.android.exoplayer2.source.MediaPeriod;
|
||||
import com.google.android.exoplayer2.source.MediaSource;
|
||||
import com.google.android.exoplayer2.source.MediaSourceEventListener;
|
||||
import com.google.android.exoplayer2.upstream.Allocator;
|
||||
|
||||
import org.schabi.newpipe.playlist.PlayQueueItem;
|
||||
import org.schabi.newpipe.player.playqueue.PlayQueueItem;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
@@ -34,7 +36,8 @@ public class LoadedMediaSource implements ManagedMediaSource {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void prepareSource(ExoPlayer player, boolean isTopLevelSource, Listener listener) {
|
||||
public void prepareSource(ExoPlayer player, boolean isTopLevelSource,
|
||||
SourceInfoRefreshListener listener) {
|
||||
source.prepareSource(player, isTopLevelSource, listener);
|
||||
}
|
||||
|
||||
@@ -54,8 +57,18 @@ public class LoadedMediaSource implements ManagedMediaSource {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void releaseSource() {
|
||||
source.releaseSource();
|
||||
public void releaseSource(SourceInfoRefreshListener listener) {
|
||||
source.releaseSource(listener);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addEventListener(Handler handler, MediaSourceEventListener eventListener) {
|
||||
source.addEventListener(handler, eventListener);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void removeEventListener(MediaSourceEventListener eventListener) {
|
||||
source.removeEventListener(eventListener);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -4,7 +4,7 @@ import android.support.annotation.NonNull;
|
||||
|
||||
import com.google.android.exoplayer2.source.MediaSource;
|
||||
|
||||
import org.schabi.newpipe.playlist.PlayQueueItem;
|
||||
import org.schabi.newpipe.player.playqueue.PlayQueueItem;
|
||||
|
||||
public interface ManagedMediaSource extends MediaSource {
|
||||
/**
|
||||
|
||||
@@ -0,0 +1,131 @@
|
||||
package org.schabi.newpipe.player.mediasource;
|
||||
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.annotation.Nullable;
|
||||
|
||||
import com.google.android.exoplayer2.source.ConcatenatingMediaSource;
|
||||
import com.google.android.exoplayer2.source.ShuffleOrder;
|
||||
|
||||
public class ManagedMediaSourcePlaylist {
|
||||
@NonNull private final ConcatenatingMediaSource internalSource;
|
||||
|
||||
public ManagedMediaSourcePlaylist() {
|
||||
internalSource = new ConcatenatingMediaSource(/*isPlaylistAtomic=*/false,
|
||||
new ShuffleOrder.UnshuffledShuffleOrder(0));
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// MediaSource Delegations
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
public int size() {
|
||||
return internalSource.getSize();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the {@link ManagedMediaSource} at the given index of the playlist.
|
||||
* If the index is invalid, then null is returned.
|
||||
* */
|
||||
@Nullable
|
||||
public ManagedMediaSource get(final int index) {
|
||||
return (index < 0 || index >= size()) ?
|
||||
null : (ManagedMediaSource) internalSource.getMediaSource(index);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public ConcatenatingMediaSource getParentMediaSource() {
|
||||
return internalSource;
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Playlist Manipulation
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
/**
|
||||
* Expands the {@link ConcatenatingMediaSource} by appending it with a
|
||||
* {@link PlaceholderMediaSource}.
|
||||
*
|
||||
* @see #append(ManagedMediaSource)
|
||||
* */
|
||||
public synchronized void expand() {
|
||||
append(new PlaceholderMediaSource());
|
||||
}
|
||||
|
||||
/**
|
||||
* Appends a {@link ManagedMediaSource} to the end of {@link ConcatenatingMediaSource}.
|
||||
* @see ConcatenatingMediaSource#addMediaSource
|
||||
* */
|
||||
public synchronized void append(@NonNull final ManagedMediaSource source) {
|
||||
internalSource.addMediaSource(source);
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes a {@link ManagedMediaSource} from {@link ConcatenatingMediaSource}
|
||||
* at the given index. If this index is out of bound, then the removal is ignored.
|
||||
* @see ConcatenatingMediaSource#removeMediaSource(int)
|
||||
* */
|
||||
public synchronized void remove(final int index) {
|
||||
if (index < 0 || index > internalSource.getSize()) return;
|
||||
|
||||
internalSource.removeMediaSource(index);
|
||||
}
|
||||
|
||||
/**
|
||||
* Moves a {@link ManagedMediaSource} in {@link ConcatenatingMediaSource}
|
||||
* from the given source index to the target index. If either index is out of bound,
|
||||
* then the call is ignored.
|
||||
* @see ConcatenatingMediaSource#moveMediaSource(int, int)
|
||||
* */
|
||||
public synchronized void move(final int source, final int target) {
|
||||
if (source < 0 || target < 0) return;
|
||||
if (source >= internalSource.getSize() || target >= internalSource.getSize()) return;
|
||||
|
||||
internalSource.moveMediaSource(source, target);
|
||||
}
|
||||
|
||||
/**
|
||||
* Invalidates the {@link ManagedMediaSource} at the given index by replacing it
|
||||
* with a {@link PlaceholderMediaSource}.
|
||||
* @see #update(int, ManagedMediaSource, Runnable)
|
||||
* */
|
||||
public synchronized void invalidate(final int index,
|
||||
@Nullable final Runnable finalizingAction) {
|
||||
if (get(index) instanceof PlaceholderMediaSource) return;
|
||||
update(index, new PlaceholderMediaSource(), finalizingAction);
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the {@link ManagedMediaSource} in {@link ConcatenatingMediaSource}
|
||||
* at the given index with a given {@link ManagedMediaSource}.
|
||||
* @see #update(int, ManagedMediaSource, Runnable)
|
||||
* */
|
||||
public synchronized void update(final int index, @NonNull final ManagedMediaSource source) {
|
||||
update(index, source, /*doNothing=*/null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the {@link ManagedMediaSource} in {@link ConcatenatingMediaSource}
|
||||
* at the given index with a given {@link ManagedMediaSource}. If the index is out of bound,
|
||||
* then the replacement is ignored.
|
||||
* @see ConcatenatingMediaSource#addMediaSource
|
||||
* @see ConcatenatingMediaSource#removeMediaSource(int, Runnable)
|
||||
* */
|
||||
public synchronized void update(final int index, @NonNull final ManagedMediaSource source,
|
||||
@Nullable final Runnable finalizingAction) {
|
||||
if (index < 0 || index >= internalSource.getSize()) return;
|
||||
|
||||
// Add and remove are sequential on the same thread, therefore here, the exoplayer
|
||||
// message queue must receive and process add before remove, effectively treating them
|
||||
// as atomic.
|
||||
|
||||
// Since the finalizing action occurs strictly after the timeline has completed
|
||||
// all its changes on the playback thread, thus, it is possible, in the meantime,
|
||||
// other calls that modifies the playlist media source occur in between. This makes
|
||||
// it unsafe to call remove as the finalizing action of add.
|
||||
internalSource.addMediaSource(index + 1, source);
|
||||
|
||||
// Because of the above race condition, it is thus only safe to synchronize the player
|
||||
// in the finalizing action AFTER the removal is complete and the timeline has changed.
|
||||
internalSource.removeMediaSource(index, finalizingAction);
|
||||
}
|
||||
}
|
||||
@@ -3,20 +3,19 @@ package org.schabi.newpipe.player.mediasource;
|
||||
import android.support.annotation.NonNull;
|
||||
|
||||
import com.google.android.exoplayer2.ExoPlayer;
|
||||
import com.google.android.exoplayer2.source.BaseMediaSource;
|
||||
import com.google.android.exoplayer2.source.MediaPeriod;
|
||||
import com.google.android.exoplayer2.upstream.Allocator;
|
||||
|
||||
import org.schabi.newpipe.playlist.PlayQueueItem;
|
||||
import org.schabi.newpipe.player.playqueue.PlayQueueItem;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
public class PlaceholderMediaSource implements ManagedMediaSource {
|
||||
public class PlaceholderMediaSource extends BaseMediaSource implements ManagedMediaSource {
|
||||
// Do nothing, so this will stall the playback
|
||||
@Override public void prepareSource(ExoPlayer player, boolean isTopLevelSource, Listener listener) {}
|
||||
@Override public void maybeThrowSourceInfoRefreshError() throws IOException {}
|
||||
@Override public void maybeThrowSourceInfoRefreshError() {}
|
||||
@Override public MediaPeriod createPeriod(MediaPeriodId id, Allocator allocator) { return null; }
|
||||
@Override public void releasePeriod(MediaPeriod mediaPeriod) {}
|
||||
@Override public void releaseSource() {}
|
||||
@Override protected void prepareSourceInternal(ExoPlayer player, boolean isTopLevelSource) {}
|
||||
@Override protected void releaseSourceInternal() {}
|
||||
|
||||
@Override
|
||||
public boolean shouldBeReplacedWith(@NonNull PlayQueueItem newIdentity,
|
||||
|
||||
@@ -5,7 +5,7 @@ import android.support.v4.media.MediaDescriptionCompat;
|
||||
|
||||
import org.schabi.newpipe.player.BasePlayer;
|
||||
import org.schabi.newpipe.player.mediasession.MediaSessionCallback;
|
||||
import org.schabi.newpipe.playlist.PlayQueueItem;
|
||||
import org.schabi.newpipe.player.playqueue.PlayQueueItem;
|
||||
|
||||
public class BasePlayerMediaSession implements MediaSessionCallback {
|
||||
private BasePlayer player;
|
||||
@@ -69,9 +69,4 @@ public class BasePlayerMediaSession implements MediaSessionCallback {
|
||||
public void onPause() {
|
||||
player.onPause();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSetShuffle(boolean isShuffled) {
|
||||
player.onShuffleModeEnabledChanged(isShuffled);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,29 +2,28 @@ package org.schabi.newpipe.player.playback;
|
||||
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.support.v4.util.ArraySet;
|
||||
import android.util.Log;
|
||||
|
||||
import com.google.android.exoplayer2.source.DynamicConcatenatingMediaSource;
|
||||
import com.google.android.exoplayer2.source.MediaSource;
|
||||
import com.google.android.exoplayer2.source.ShuffleOrder;
|
||||
|
||||
import org.reactivestreams.Subscriber;
|
||||
import org.reactivestreams.Subscription;
|
||||
import org.schabi.newpipe.extractor.stream.StreamInfo;
|
||||
import org.schabi.newpipe.player.mediasource.FailedMediaSource;
|
||||
import org.schabi.newpipe.player.mediasource.LoadedMediaSource;
|
||||
import org.schabi.newpipe.player.mediasource.ManagedMediaSource;
|
||||
import org.schabi.newpipe.player.mediasource.ManagedMediaSourcePlaylist;
|
||||
import org.schabi.newpipe.player.mediasource.PlaceholderMediaSource;
|
||||
import org.schabi.newpipe.playlist.PlayQueue;
|
||||
import org.schabi.newpipe.playlist.PlayQueueItem;
|
||||
import org.schabi.newpipe.playlist.events.MoveEvent;
|
||||
import org.schabi.newpipe.playlist.events.PlayQueueEvent;
|
||||
import org.schabi.newpipe.playlist.events.RemoveEvent;
|
||||
import org.schabi.newpipe.playlist.events.ReorderEvent;
|
||||
import org.schabi.newpipe.player.playqueue.PlayQueue;
|
||||
import org.schabi.newpipe.player.playqueue.PlayQueueItem;
|
||||
import org.schabi.newpipe.player.playqueue.events.MoveEvent;
|
||||
import org.schabi.newpipe.player.playqueue.events.PlayQueueEvent;
|
||||
import org.schabi.newpipe.player.playqueue.events.RemoveEvent;
|
||||
import org.schabi.newpipe.player.playqueue.events.ReorderEvent;
|
||||
import org.schabi.newpipe.util.ServiceHelper;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
@@ -34,12 +33,13 @@ import io.reactivex.Single;
|
||||
import io.reactivex.android.schedulers.AndroidSchedulers;
|
||||
import io.reactivex.disposables.CompositeDisposable;
|
||||
import io.reactivex.disposables.Disposable;
|
||||
import io.reactivex.disposables.SerialDisposable;
|
||||
import io.reactivex.functions.Consumer;
|
||||
import io.reactivex.internal.subscriptions.EmptySubscription;
|
||||
import io.reactivex.schedulers.Schedulers;
|
||||
import io.reactivex.subjects.PublishSubject;
|
||||
|
||||
import static org.schabi.newpipe.playlist.PlayQueue.DEBUG;
|
||||
import static org.schabi.newpipe.player.mediasource.FailedMediaSource.MediaSourceResolutionException;
|
||||
import static org.schabi.newpipe.player.mediasource.FailedMediaSource.StreamInfoLoadException;
|
||||
import static org.schabi.newpipe.player.playqueue.PlayQueue.DEBUG;
|
||||
|
||||
public class MediaSourceManager {
|
||||
@NonNull private final String TAG = "MediaSourceManager@" + hashCode();
|
||||
@@ -52,7 +52,6 @@ public class MediaSourceManager {
|
||||
* streams before will only be cached for future usage.
|
||||
*
|
||||
* @see #onMediaSourceReceived(PlayQueueItem, ManagedMediaSource)
|
||||
* @see #update(int, MediaSource, Runnable)
|
||||
* */
|
||||
private final static int WINDOW_SIZE = 1;
|
||||
|
||||
@@ -99,11 +98,10 @@ public class MediaSourceManager {
|
||||
private final static int MAXIMUM_LOADER_SIZE = WINDOW_SIZE * 2 + 1;
|
||||
@NonNull private final CompositeDisposable loaderReactor;
|
||||
@NonNull private final Set<PlayQueueItem> loadingItems;
|
||||
@NonNull private final SerialDisposable syncReactor;
|
||||
|
||||
@NonNull private final AtomicBoolean isBlocked;
|
||||
|
||||
@NonNull private DynamicConcatenatingMediaSource sources;
|
||||
@NonNull private ManagedMediaSourcePlaylist playlist;
|
||||
|
||||
public MediaSourceManager(@NonNull final PlaybackListener listener,
|
||||
@NonNull final PlayQueue playQueue) {
|
||||
@@ -139,13 +137,12 @@ public class MediaSourceManager {
|
||||
|
||||
this.playQueueReactor = EmptySubscription.INSTANCE;
|
||||
this.loaderReactor = new CompositeDisposable();
|
||||
this.syncReactor = new SerialDisposable();
|
||||
|
||||
this.isBlocked = new AtomicBoolean(false);
|
||||
|
||||
this.sources = new DynamicConcatenatingMediaSource();
|
||||
this.playlist = new ManagedMediaSourcePlaylist();
|
||||
|
||||
this.loadingItems = Collections.synchronizedSet(new HashSet<>());
|
||||
this.loadingItems = Collections.synchronizedSet(new ArraySet<>());
|
||||
|
||||
playQueue.getBroadcastReceiver()
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
@@ -166,8 +163,6 @@ public class MediaSourceManager {
|
||||
|
||||
playQueueReactor.cancel();
|
||||
loaderReactor.dispose();
|
||||
syncReactor.dispose();
|
||||
sources.releaseSource();
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
@@ -215,17 +210,18 @@ public class MediaSourceManager {
|
||||
break;
|
||||
case REMOVE:
|
||||
final RemoveEvent removeEvent = (RemoveEvent) event;
|
||||
remove(removeEvent.getRemoveIndex());
|
||||
playlist.remove(removeEvent.getRemoveIndex());
|
||||
break;
|
||||
case MOVE:
|
||||
final MoveEvent moveEvent = (MoveEvent) event;
|
||||
move(moveEvent.getFromIndex(), moveEvent.getToIndex());
|
||||
playlist.move(moveEvent.getFromIndex(), moveEvent.getToIndex());
|
||||
break;
|
||||
case REORDER:
|
||||
// Need to move to ensure the playing index from play queue matches that of
|
||||
// the source timeline, and then window correction can take care of the rest
|
||||
final ReorderEvent reorderEvent = (ReorderEvent) event;
|
||||
move(reorderEvent.getFromSelectedIndex(), reorderEvent.getToSelectedIndex());
|
||||
playlist.move(reorderEvent.getFromSelectedIndex(),
|
||||
reorderEvent.getToSelectedIndex());
|
||||
break;
|
||||
case RECOVERY:
|
||||
default:
|
||||
@@ -266,10 +262,11 @@ public class MediaSourceManager {
|
||||
}
|
||||
|
||||
private boolean isPlaybackReady() {
|
||||
if (sources.getSize() != playQueue.size()) return false;
|
||||
if (playlist.size() != playQueue.size()) return false;
|
||||
|
||||
final ManagedMediaSource mediaSource = playlist.get(playQueue.getIndex());
|
||||
if (mediaSource == null) return false;
|
||||
|
||||
final ManagedMediaSource mediaSource =
|
||||
(ManagedMediaSource) sources.getMediaSource(playQueue.getIndex());
|
||||
final PlayQueueItem playQueueItem = playQueue.getItem();
|
||||
return mediaSource.isStreamEqual(playQueueItem);
|
||||
}
|
||||
@@ -288,9 +285,9 @@ public class MediaSourceManager {
|
||||
private void maybeUnblock() {
|
||||
if (DEBUG) Log.d(TAG, "maybeUnblock() called.");
|
||||
|
||||
if (isPlayQueueReady() && isPlaybackReady() && isBlocked.get()) {
|
||||
if (isBlocked.get()) {
|
||||
isBlocked.set(false);
|
||||
playbackListener.onPlaybackUnblock(sources);
|
||||
playbackListener.onPlaybackUnblock(playlist.getParentMediaSource());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -299,45 +296,35 @@ public class MediaSourceManager {
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
private void maybeSync() {
|
||||
if (DEBUG) Log.d(TAG, "onPlaybackSynchronize() called.");
|
||||
if (DEBUG) Log.d(TAG, "maybeSync() called.");
|
||||
|
||||
final PlayQueueItem currentItem = playQueue.getItem();
|
||||
if (isBlocked.get() || !isPlaybackReady() || currentItem == null) return;
|
||||
if (isBlocked.get() || currentItem == null) return;
|
||||
|
||||
final Consumer<StreamInfo> onSuccess = info -> syncInternal(currentItem, info);
|
||||
final Consumer<Throwable> onError = throwable -> syncInternal(currentItem, null);
|
||||
|
||||
final Disposable sync = currentItem.getStream()
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(onSuccess, onError);
|
||||
syncReactor.set(sync);
|
||||
playbackListener.onPlaybackSynchronize(currentItem);
|
||||
}
|
||||
|
||||
private void syncInternal(@NonNull final PlayQueueItem item,
|
||||
@Nullable final StreamInfo info) {
|
||||
// Ensure the current item is up to date with the play queue
|
||||
if (playQueue.getItem() == item) {
|
||||
playbackListener.onPlaybackSynchronize(item, info);
|
||||
private synchronized void maybeSynchronizePlayer() {
|
||||
if (isPlayQueueReady() && isPlaybackReady()) {
|
||||
maybeUnblock();
|
||||
maybeSync();
|
||||
}
|
||||
}
|
||||
|
||||
private void maybeSynchronizePlayer() {
|
||||
maybeUnblock();
|
||||
maybeSync();
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// MediaSource Loading
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
private Observable<Long> getEdgeIntervalSignal() {
|
||||
return Observable.interval(progressUpdateIntervalMillis, TimeUnit.MILLISECONDS)
|
||||
.filter(ignored -> playbackListener.isNearPlaybackEdge(playbackNearEndGapMillis));
|
||||
.filter(ignored ->
|
||||
playbackListener.isApproachingPlaybackEdge(playbackNearEndGapMillis));
|
||||
}
|
||||
|
||||
private Disposable getDebouncedLoader() {
|
||||
return debouncedSignal.mergeWith(nearEndIntervalSignal)
|
||||
.debounce(loadDebounceMillis, TimeUnit.MILLISECONDS)
|
||||
.subscribeOn(Schedulers.single())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(timestamp -> loadImmediate());
|
||||
}
|
||||
@@ -348,42 +335,21 @@ public class MediaSourceManager {
|
||||
|
||||
private void loadImmediate() {
|
||||
if (DEBUG) Log.d(TAG, "MediaSource - loadImmediate() called");
|
||||
// The current item has higher priority
|
||||
final int currentIndex = playQueue.getIndex();
|
||||
final PlayQueueItem currentItem = playQueue.getItem(currentIndex);
|
||||
if (currentItem == null) return;
|
||||
final ItemsToLoad itemsToLoad = getItemsToLoad(playQueue, WINDOW_SIZE);
|
||||
if (itemsToLoad == null) return;
|
||||
|
||||
// Evict the items being loaded to free up memory
|
||||
if (loaderReactor.size() > MAXIMUM_LOADER_SIZE) {
|
||||
loaderReactor.clear();
|
||||
loadingItems.clear();
|
||||
}
|
||||
maybeLoadItem(currentItem);
|
||||
// Evict the previous items being loaded to free up memory, before start loading new ones
|
||||
maybeClearLoaders();
|
||||
|
||||
// The rest are just for seamless playback
|
||||
// Although timeline is not updated prior to the current index, these sources are still
|
||||
// loaded into the cache for faster retrieval at a potentially later time.
|
||||
final int leftBound = Math.max(0, currentIndex - WINDOW_SIZE);
|
||||
final int rightLimit = currentIndex + WINDOW_SIZE + 1;
|
||||
final int rightBound = Math.min(playQueue.size(), rightLimit);
|
||||
final Set<PlayQueueItem> items = new HashSet<>(
|
||||
playQueue.getStreams().subList(leftBound,rightBound));
|
||||
|
||||
// Do a round robin
|
||||
final int excess = rightLimit - playQueue.size();
|
||||
if (excess >= 0) {
|
||||
items.addAll(playQueue.getStreams().subList(0, Math.min(playQueue.size(), excess)));
|
||||
}
|
||||
items.remove(currentItem);
|
||||
|
||||
for (final PlayQueueItem item : items) {
|
||||
maybeLoadItem(itemsToLoad.center);
|
||||
for (final PlayQueueItem item : itemsToLoad.neighbors) {
|
||||
maybeLoadItem(item);
|
||||
}
|
||||
}
|
||||
|
||||
private void maybeLoadItem(@NonNull final PlayQueueItem item) {
|
||||
if (DEBUG) Log.d(TAG, "maybeLoadItem() called.");
|
||||
if (playQueue.indexOf(item) >= sources.getSize()) return;
|
||||
if (playQueue.indexOf(item) >= playlist.size()) return;
|
||||
|
||||
if (!loadingItems.contains(item) && isCorrectionNeeded(item)) {
|
||||
if (DEBUG) Log.d(TAG, "MediaSource - Loading=[" + item.getTitle() +
|
||||
@@ -402,19 +368,19 @@ public class MediaSourceManager {
|
||||
return stream.getStream().map(streamInfo -> {
|
||||
final MediaSource source = playbackListener.sourceOf(stream, streamInfo);
|
||||
if (source == null) {
|
||||
final Exception exception = new IllegalStateException(
|
||||
"Unable to resolve source from stream info." +
|
||||
" URL: " + stream.getUrl() +
|
||||
", audio count: " + streamInfo.getAudioStreams().size() +
|
||||
", video count: " + streamInfo.getVideoOnlyStreams().size() +
|
||||
streamInfo.getVideoStreams().size());
|
||||
return new FailedMediaSource(stream, exception);
|
||||
final String message = "Unable to resolve source from stream info." +
|
||||
" URL: " + stream.getUrl() +
|
||||
", audio count: " + streamInfo.getAudioStreams().size() +
|
||||
", video count: " + streamInfo.getVideoOnlyStreams().size() +
|
||||
streamInfo.getVideoStreams().size();
|
||||
return new FailedMediaSource(stream, new MediaSourceResolutionException(message));
|
||||
}
|
||||
|
||||
final long expiration = System.currentTimeMillis() +
|
||||
ServiceHelper.getCacheExpirationMillis(streamInfo.getServiceId());
|
||||
return new LoadedMediaSource(source, stream, expiration);
|
||||
}).onErrorReturn(throwable -> new FailedMediaSource(stream, throwable));
|
||||
}).onErrorReturn(throwable -> new FailedMediaSource(stream,
|
||||
new StreamInfoLoadException(throwable)));
|
||||
}
|
||||
|
||||
private void onMediaSourceReceived(@NonNull final PlayQueueItem item,
|
||||
@@ -426,15 +392,16 @@ public class MediaSourceManager {
|
||||
|
||||
final int itemIndex = playQueue.indexOf(item);
|
||||
// Only update the playlist timeline for items at the current index or after.
|
||||
if (itemIndex >= playQueue.getIndex() && isCorrectionNeeded(item)) {
|
||||
if (isCorrectionNeeded(item)) {
|
||||
if (DEBUG) Log.d(TAG, "MediaSource - Updating index=[" + itemIndex + "] with " +
|
||||
"title=[" + item.getTitle() + "] at url=[" + item.getUrl() + "]");
|
||||
update(itemIndex, mediaSource, this::maybeSynchronizePlayer);
|
||||
playlist.update(itemIndex, mediaSource, this::maybeSynchronizePlayer);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the corresponding MediaSource in {@link DynamicConcatenatingMediaSource}
|
||||
* Checks if the corresponding MediaSource in
|
||||
* {@link com.google.android.exoplayer2.source.ConcatenatingMediaSource}
|
||||
* for a given {@link PlayQueueItem} needs replacement, either due to gapless playback
|
||||
* readiness or playlist desynchronization.
|
||||
* <br><br>
|
||||
@@ -445,10 +412,8 @@ public class MediaSourceManager {
|
||||
* */
|
||||
private boolean isCorrectionNeeded(@NonNull final PlayQueueItem item) {
|
||||
final int index = playQueue.indexOf(item);
|
||||
if (index == -1 || index >= sources.getSize()) return false;
|
||||
|
||||
final ManagedMediaSource mediaSource = (ManagedMediaSource) sources.getMediaSource(index);
|
||||
return mediaSource.shouldBeReplacedWith(item,
|
||||
final ManagedMediaSource mediaSource = playlist.get(index);
|
||||
return mediaSource != null && mediaSource.shouldBeReplacedWith(item,
|
||||
/*mightBeInProgress=*/index != playQueue.getIndex());
|
||||
}
|
||||
|
||||
@@ -465,10 +430,9 @@ public class MediaSourceManager {
|
||||
* */
|
||||
private void maybeRenewCurrentIndex() {
|
||||
final int currentIndex = playQueue.getIndex();
|
||||
if (sources.getSize() <= currentIndex) return;
|
||||
final ManagedMediaSource currentSource = playlist.get(currentIndex);
|
||||
if (currentSource == null) return;
|
||||
|
||||
final ManagedMediaSource currentSource =
|
||||
(ManagedMediaSource) sources.getMediaSource(currentIndex);
|
||||
final PlayQueueItem currentItem = playQueue.getItem();
|
||||
if (!currentSource.shouldBeReplacedWith(currentItem, /*canInterruptOnRenew=*/true)) {
|
||||
maybeSynchronizePlayer();
|
||||
@@ -477,7 +441,16 @@ public class MediaSourceManager {
|
||||
|
||||
if (DEBUG) Log.d(TAG, "MediaSource - Reloading currently playing, " +
|
||||
"index=[" + currentIndex + "], item=[" + currentItem.getTitle() + "]");
|
||||
update(currentIndex, new PlaceholderMediaSource(), this::loadImmediate);
|
||||
playlist.invalidate(currentIndex, this::loadImmediate);
|
||||
}
|
||||
|
||||
private void maybeClearLoaders() {
|
||||
if (DEBUG) Log.d(TAG, "MediaSource - maybeClearLoaders() called.");
|
||||
if (!loadingItems.contains(playQueue.getItem()) &&
|
||||
loaderReactor.size() > MAXIMUM_LOADER_SIZE) {
|
||||
loaderReactor.clear();
|
||||
loadingItems.clear();
|
||||
}
|
||||
}
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// MediaSource Playlist Helpers
|
||||
@@ -485,73 +458,54 @@ public class MediaSourceManager {
|
||||
|
||||
private void resetSources() {
|
||||
if (DEBUG) Log.d(TAG, "resetSources() called.");
|
||||
|
||||
this.sources.releaseSource();
|
||||
this.sources = new DynamicConcatenatingMediaSource(false,
|
||||
// Shuffling is done on PlayQueue, thus no need to use ExoPlayer's shuffle order
|
||||
new ShuffleOrder.UnshuffledShuffleOrder(0));
|
||||
playlist = new ManagedMediaSourcePlaylist();
|
||||
}
|
||||
|
||||
private void populateSources() {
|
||||
if (DEBUG) Log.d(TAG, "populateSources() called.");
|
||||
if (sources.getSize() >= playQueue.size()) return;
|
||||
|
||||
for (int index = sources.getSize() - 1; index < playQueue.size(); index++) {
|
||||
emplace(index, new PlaceholderMediaSource());
|
||||
while (playlist.size() < playQueue.size()) {
|
||||
playlist.expand();
|
||||
}
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// MediaSource Playlist Manipulation
|
||||
// Manager Helpers
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
@Nullable
|
||||
private static ItemsToLoad getItemsToLoad(@NonNull final PlayQueue playQueue,
|
||||
final int windowSize) {
|
||||
// The current item has higher priority
|
||||
final int currentIndex = playQueue.getIndex();
|
||||
final PlayQueueItem currentItem = playQueue.getItem(currentIndex);
|
||||
if (currentItem == null) return null;
|
||||
|
||||
/**
|
||||
* Places a {@link MediaSource} into the {@link DynamicConcatenatingMediaSource}
|
||||
* with position in respect to the play queue only if no {@link MediaSource}
|
||||
* already exists at the given index.
|
||||
* */
|
||||
private synchronized void emplace(final int index, @NonNull final MediaSource source) {
|
||||
if (index < sources.getSize()) return;
|
||||
// The rest are just for seamless playback
|
||||
// Although timeline is not updated prior to the current index, these sources are still
|
||||
// loaded into the cache for faster retrieval at a potentially later time.
|
||||
final int leftBound = Math.max(0, currentIndex - windowSize);
|
||||
final int rightLimit = currentIndex + windowSize + 1;
|
||||
final int rightBound = Math.min(playQueue.size(), rightLimit);
|
||||
final Set<PlayQueueItem> neighbors = new ArraySet<>(
|
||||
playQueue.getStreams().subList(leftBound,rightBound));
|
||||
|
||||
sources.addMediaSource(index, source);
|
||||
// Do a round robin
|
||||
final int excess = rightLimit - playQueue.size();
|
||||
if (excess >= 0) {
|
||||
neighbors.addAll(playQueue.getStreams().subList(0, Math.min(playQueue.size(), excess)));
|
||||
}
|
||||
neighbors.remove(currentItem);
|
||||
|
||||
return new ItemsToLoad(currentItem, neighbors);
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes a {@link MediaSource} from {@link DynamicConcatenatingMediaSource}
|
||||
* at the given index. If this index is out of bound, then the removal is ignored.
|
||||
* */
|
||||
private synchronized void remove(final int index) {
|
||||
if (index < 0 || index > sources.getSize()) return;
|
||||
private static class ItemsToLoad {
|
||||
@NonNull final private PlayQueueItem center;
|
||||
@NonNull final private Collection<PlayQueueItem> neighbors;
|
||||
|
||||
sources.removeMediaSource(index);
|
||||
}
|
||||
|
||||
/**
|
||||
* Moves a {@link MediaSource} in {@link DynamicConcatenatingMediaSource}
|
||||
* from the given source index to the target index. If either index is out of bound,
|
||||
* then the call is ignored.
|
||||
* */
|
||||
private synchronized void move(final int source, final int target) {
|
||||
if (source < 0 || target < 0) return;
|
||||
if (source >= sources.getSize() || target >= sources.getSize()) return;
|
||||
|
||||
sources.moveMediaSource(source, target);
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the {@link MediaSource} in {@link DynamicConcatenatingMediaSource}
|
||||
* at the given index with a given {@link MediaSource}. If the index is out of bound,
|
||||
* then the replacement is ignored.
|
||||
* <br><br>
|
||||
* Not recommended to use on indices LESS THAN the currently playing index, since
|
||||
* this will modify the playback timeline prior to the index and may cause desynchronization
|
||||
* on the playing item between {@link PlayQueue} and {@link DynamicConcatenatingMediaSource}.
|
||||
* */
|
||||
private synchronized void update(final int index, @NonNull final MediaSource source,
|
||||
@Nullable final Runnable finalizingAction) {
|
||||
if (index < 0 || index >= sources.getSize()) return;
|
||||
|
||||
sources.addMediaSource(index + 1, source, () ->
|
||||
sources.removeMediaSource(index, finalizingAction));
|
||||
ItemsToLoad(@NonNull final PlayQueueItem center,
|
||||
@NonNull final Collection<PlayQueueItem> neighbors) {
|
||||
this.center = center;
|
||||
this.neighbors = neighbors;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,20 +6,20 @@ import android.support.annotation.Nullable;
|
||||
import com.google.android.exoplayer2.source.MediaSource;
|
||||
|
||||
import org.schabi.newpipe.extractor.stream.StreamInfo;
|
||||
import org.schabi.newpipe.playlist.PlayQueueItem;
|
||||
import org.schabi.newpipe.player.playqueue.PlayQueueItem;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public interface PlaybackListener {
|
||||
|
||||
/**
|
||||
* Called to check if the currently playing stream is close to the end of its playback.
|
||||
* Implementation should return true when the current playback position is within
|
||||
* timeToEndMillis or less until its playback completes or transitions.
|
||||
* Called to check if the currently playing stream is approaching the end of its playback.
|
||||
* Implementation should return true when the current playback position is progressing within
|
||||
* timeToEndMillis or less to its playback during.
|
||||
*
|
||||
* May be called at any time.
|
||||
* */
|
||||
boolean isNearPlaybackEdge(final long timeToEndMillis);
|
||||
boolean isApproachingPlaybackEdge(final long timeToEndMillis);
|
||||
|
||||
/**
|
||||
* Called when the stream at the current queue index is not ready yet.
|
||||
@@ -45,7 +45,7 @@ public interface PlaybackListener {
|
||||
*
|
||||
* May be called anytime at any amount once unblock is called.
|
||||
* */
|
||||
void onPlaybackSynchronize(@NonNull final PlayQueueItem item, @Nullable final StreamInfo info);
|
||||
void onPlaybackSynchronize(@NonNull final PlayQueueItem item);
|
||||
|
||||
/**
|
||||
* Requests the listener to resolve a stream info into a media source
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package org.schabi.newpipe.playlist;
|
||||
package org.schabi.newpipe.player.playqueue;
|
||||
|
||||
import android.util.Log;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
package org.schabi.newpipe.playlist;
|
||||
package org.schabi.newpipe.player.playqueue;
|
||||
|
||||
|
||||
import org.schabi.newpipe.extractor.InfoItem;
|
||||
import org.schabi.newpipe.extractor.channel.ChannelInfo;
|
||||
import org.schabi.newpipe.extractor.channel.ChannelInfoItem;
|
||||
import org.schabi.newpipe.extractor.stream.StreamInfoItem;
|
||||
@@ -1,4 +1,4 @@
|
||||
package org.schabi.newpipe.playlist;
|
||||
package org.schabi.newpipe.player.playqueue;
|
||||
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.annotation.Nullable;
|
||||
@@ -6,15 +6,16 @@ import android.util.Log;
|
||||
|
||||
import org.reactivestreams.Subscriber;
|
||||
import org.reactivestreams.Subscription;
|
||||
import org.schabi.newpipe.playlist.events.AppendEvent;
|
||||
import org.schabi.newpipe.playlist.events.ErrorEvent;
|
||||
import org.schabi.newpipe.playlist.events.InitEvent;
|
||||
import org.schabi.newpipe.playlist.events.MoveEvent;
|
||||
import org.schabi.newpipe.playlist.events.PlayQueueEvent;
|
||||
import org.schabi.newpipe.playlist.events.RecoveryEvent;
|
||||
import org.schabi.newpipe.playlist.events.RemoveEvent;
|
||||
import org.schabi.newpipe.playlist.events.ReorderEvent;
|
||||
import org.schabi.newpipe.playlist.events.SelectEvent;
|
||||
import org.schabi.newpipe.BuildConfig;
|
||||
import org.schabi.newpipe.player.playqueue.events.AppendEvent;
|
||||
import org.schabi.newpipe.player.playqueue.events.ErrorEvent;
|
||||
import org.schabi.newpipe.player.playqueue.events.InitEvent;
|
||||
import org.schabi.newpipe.player.playqueue.events.MoveEvent;
|
||||
import org.schabi.newpipe.player.playqueue.events.PlayQueueEvent;
|
||||
import org.schabi.newpipe.player.playqueue.events.RecoveryEvent;
|
||||
import org.schabi.newpipe.player.playqueue.events.RemoveEvent;
|
||||
import org.schabi.newpipe.player.playqueue.events.ReorderEvent;
|
||||
import org.schabi.newpipe.player.playqueue.events.SelectEvent;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.ArrayList;
|
||||
@@ -41,7 +42,7 @@ import io.reactivex.subjects.BehaviorSubject;
|
||||
public abstract class PlayQueue implements Serializable {
|
||||
private final String TAG = "PlayQueue@" + Integer.toHexString(hashCode());
|
||||
|
||||
public static final boolean DEBUG = true;
|
||||
public static final boolean DEBUG = !BuildConfig.BUILD_TYPE.equals("release");
|
||||
|
||||
private ArrayList<PlayQueueItem> backup;
|
||||
private ArrayList<PlayQueueItem> streams;
|
||||
@@ -1,4 +1,4 @@
|
||||
package org.schabi.newpipe.playlist;
|
||||
package org.schabi.newpipe.player.playqueue;
|
||||
|
||||
import android.content.Context;
|
||||
import android.support.v7.widget.RecyclerView;
|
||||
@@ -8,12 +8,13 @@ import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
|
||||
import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.playlist.events.AppendEvent;
|
||||
import org.schabi.newpipe.playlist.events.ErrorEvent;
|
||||
import org.schabi.newpipe.playlist.events.MoveEvent;
|
||||
import org.schabi.newpipe.playlist.events.PlayQueueEvent;
|
||||
import org.schabi.newpipe.playlist.events.RemoveEvent;
|
||||
import org.schabi.newpipe.playlist.events.SelectEvent;
|
||||
import org.schabi.newpipe.player.playqueue.events.AppendEvent;
|
||||
import org.schabi.newpipe.player.playqueue.events.ErrorEvent;
|
||||
import org.schabi.newpipe.player.playqueue.events.MoveEvent;
|
||||
import org.schabi.newpipe.player.playqueue.events.PlayQueueEvent;
|
||||
import org.schabi.newpipe.player.playqueue.events.RemoveEvent;
|
||||
import org.schabi.newpipe.player.playqueue.events.SelectEvent;
|
||||
import org.schabi.newpipe.util.FallbackViewHolder;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@@ -188,7 +189,7 @@ public class PlayQueueAdapter extends RecyclerView.Adapter<RecyclerView.ViewHold
|
||||
return new PlayQueueItemHolder(LayoutInflater.from(parent.getContext()).inflate(R.layout.play_queue_item, parent, false));
|
||||
default:
|
||||
Log.e(TAG, "Attempting to create view holder with undefined type: " + type);
|
||||
return null;
|
||||
return new FallbackViewHolder(new View(parent.getContext()));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package org.schabi.newpipe.playlist;
|
||||
package org.schabi.newpipe.player.playqueue;
|
||||
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.annotation.Nullable;
|
||||
@@ -28,12 +28,12 @@ public class PlayQueueItem implements Serializable {
|
||||
private long recoveryPosition;
|
||||
private Throwable error;
|
||||
|
||||
private transient Single<StreamInfo> stream;
|
||||
|
||||
PlayQueueItem(@NonNull final StreamInfo info) {
|
||||
this(info.getName(), info.getUrl(), info.getServiceId(), info.getDuration(),
|
||||
info.getThumbnailUrl(), info.getUploaderName(), info.getStreamType());
|
||||
this.stream = Single.just(info);
|
||||
|
||||
if (info.getStartPosition() > 0)
|
||||
setRecoveryPosition(info.getStartPosition() * 1000);
|
||||
}
|
||||
|
||||
PlayQueueItem(@NonNull final StreamInfoItem item) {
|
||||
@@ -100,11 +100,6 @@ public class PlayQueueItem implements Serializable {
|
||||
|
||||
@NonNull
|
||||
public Single<StreamInfo> getStream() {
|
||||
return stream == null ? stream = getInfo() : stream;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
private Single<StreamInfo> getInfo() {
|
||||
return ExtractorHelper.getStreamInfo(this.serviceId, this.url, false)
|
||||
.subscribeOn(Schedulers.io())
|
||||
.doOnError(throwable -> error = throwable);
|
||||
@@ -1,4 +1,4 @@
|
||||
package org.schabi.newpipe.playlist;
|
||||
package org.schabi.newpipe.player.playqueue;
|
||||
|
||||
import android.content.Context;
|
||||
import android.text.TextUtils;
|
||||
@@ -1,4 +1,4 @@
|
||||
package org.schabi.newpipe.playlist;
|
||||
package org.schabi.newpipe.player.playqueue;
|
||||
|
||||
import android.support.v7.widget.RecyclerView;
|
||||
import android.view.View;
|
||||
@@ -1,4 +1,4 @@
|
||||
package org.schabi.newpipe.playlist;
|
||||
package org.schabi.newpipe.player.playqueue;
|
||||
|
||||
import android.support.v7.widget.RecyclerView;
|
||||
import android.support.v7.widget.helper.ItemTouchHelper;
|
||||
@@ -1,6 +1,5 @@
|
||||
package org.schabi.newpipe.playlist;
|
||||
package org.schabi.newpipe.player.playqueue;
|
||||
|
||||
import org.schabi.newpipe.extractor.InfoItem;
|
||||
import org.schabi.newpipe.extractor.playlist.PlaylistInfo;
|
||||
import org.schabi.newpipe.extractor.playlist.PlaylistInfoItem;
|
||||
import org.schabi.newpipe.extractor.stream.StreamInfoItem;
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user