mirror of
https://github.com/TeamNewPipe/NewPipe
synced 2026-01-14 21:48:00 +00:00
Compare commits
447 Commits
v0.13.0-be
...
v0.13.4
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 | ||
|
|
7f3bd8aec2 | ||
|
|
4501203a7a | ||
|
|
06292bceb2 | ||
|
|
f94f14ab65 | ||
|
|
7145c68e03 | ||
|
|
bac3825c87 | ||
|
|
fc1d283414 | ||
| c0652daa97 | |||
|
|
553903bd9d | ||
|
|
a43ec25b7e | ||
|
|
bb2a66fd02 | ||
|
|
71ac830bfa | ||
|
|
82bce80c62 | ||
|
|
67ddf78e18 | ||
|
|
8285df0f3f | ||
|
|
bdb45295b9 | ||
|
|
f330ee8f8d | ||
|
|
7331e4a7f2 | ||
|
|
51252d3b61 | ||
|
|
dcdb2c323e | ||
|
|
c4ac901c67 | ||
|
|
a7ce072ca2 | ||
|
|
a9b427b877 | ||
|
|
67a9f3a4ad | ||
|
|
1a4905f36a | ||
|
|
cbb1fde7b0 | ||
|
|
b86bd019a7 | ||
| 6a0bada9d2 | |||
|
|
a708278cf0 | ||
|
|
119462cbc9 | ||
|
|
0324a4928c | ||
|
|
895a2a56b5 | ||
|
|
d9e616beee | ||
|
|
aa5d5d2b6d | ||
|
|
85dc555358 | ||
|
|
15b4a7d055 | ||
|
|
696c94050d | ||
|
|
b222614c4a | ||
|
|
edff694bf3 | ||
|
|
be430a6ac0 | ||
|
|
937d40c5f7 | ||
|
|
d3979676ab | ||
|
|
6716262a28 | ||
|
|
06d8bafce6 | ||
|
|
f814755908 | ||
|
|
f355fd2551 | ||
|
|
ea84c62d76 | ||
|
|
acda71cebb | ||
|
|
67dcd2e5c6 | ||
|
|
c996644613 | ||
|
|
171c3e492d | ||
|
|
8834195cc6 | ||
|
|
ed57e72fa1 | ||
|
|
7e84c3e167 | ||
|
|
4adc33471b | ||
|
|
31d07cc1e2 | ||
|
|
a349a66d5a | ||
|
|
7d427b4cc4 | ||
|
|
6c7e54868d | ||
|
|
9484a5e2ee | ||
|
|
3cbd2057e3 | ||
|
|
71ff1cf713 | ||
|
|
c737d891bc | ||
|
|
82a53343fc | ||
|
|
03fdc60018 | ||
|
|
42d19d98ad | ||
|
|
c9915bba18 | ||
|
|
a275d7ff50 | ||
|
|
74199c8624 | ||
|
|
238bff1fee | ||
|
|
de534b58c5 | ||
|
|
111a0f9f17 | ||
|
|
50392ed67d | ||
|
|
3de9da0528 | ||
|
|
ece93cadd5 | ||
|
|
7219c8d33c | ||
|
|
b0a09c7876 | ||
|
|
1d017d3cbc | ||
|
|
5fa56f2aca | ||
|
|
5ded1b139f | ||
|
|
2abb0db272 | ||
|
|
674c041c13 | ||
|
|
03c89a69a2 | ||
|
|
dbed5e2a62 | ||
|
|
d7519903a1 | ||
|
|
ab14f0d7ac | ||
|
|
3b6e43b7ca | ||
|
|
7d3b7af874 | ||
|
|
2acc348062 | ||
|
|
33ba8acd05 | ||
|
|
0c56af7090 | ||
|
|
4375ecaad7 | ||
|
|
90c538d4b6 | ||
|
|
1ffa8329d0 | ||
|
|
b404b7974a | ||
|
|
de44b580f9 | ||
|
|
2a630cd745 | ||
|
|
1ccccc5e39 | ||
|
|
aec851acfa | ||
|
|
ad5ac479fd | ||
|
|
67b82a404c | ||
|
|
f7f86a2f62 | ||
|
|
6e42b4af27 | ||
|
|
fe9ed8af76 | ||
|
|
c4c3f369c0 | ||
|
|
9b9bf40e47 | ||
|
|
14a888a610 | ||
|
|
f3d777c65c | ||
|
|
d494b6c934 | ||
|
|
7294220727 | ||
|
|
665b9087b1 | ||
|
|
f1691050cd | ||
|
|
169b6acd24 | ||
|
|
35b3bf2edb | ||
|
|
8f73c8c98b | ||
|
|
94ea8c21eb | ||
| b6a19c3e2d | |||
|
|
21455d58a6 | ||
|
|
239e9bd3db | ||
|
|
0832171a2b | ||
|
|
1edfcc629c | ||
|
|
9a2a8698ef | ||
|
|
54c31422d9 | ||
|
|
a33464284c | ||
|
|
4842caf426 | ||
|
|
02b6b4d8eb | ||
|
|
4b0d7c7d88 | ||
|
|
559b53acc3 | ||
|
|
4233c18dbb | ||
|
|
adec2c9fcc | ||
|
|
6ad2406262 | ||
|
|
bfeb8ca104 | ||
|
|
5614edfa2f | ||
|
|
946f7b2305 | ||
|
|
dfcf435f88 | ||
|
|
0e734c267c | ||
|
|
faadcec4d1 | ||
|
|
eb81784818 | ||
|
|
327010f76b | ||
|
|
371669dcb6 | ||
|
|
fb702b93ca | ||
|
|
50c8453785 | ||
|
|
14fb5ee6c4 | ||
|
|
46c2343ec6 | ||
|
|
f5c226362f | ||
|
|
9114c1157d | ||
|
|
a4f4230275 | ||
|
|
46a6147c08 | ||
|
|
0d26923d79 | ||
|
|
b57c93cf03 | ||
|
|
ec19e4bc8f | ||
|
|
cb68a9c2ce | ||
|
|
b51ded1580 | ||
|
|
4b6fb5bfeb | ||
|
|
9b8175b5be | ||
|
|
3ecfc622e2 | ||
|
|
d8d2ee4e09 | ||
|
|
be478cb088 | ||
|
|
c187b4487f | ||
|
|
f6d21e74cb | ||
|
|
bde4c4bd25 | ||
|
|
d9aaceea95 | ||
|
|
676d64a24a | ||
|
|
8ae1768f71 | ||
| 212705e7e3 | |||
|
|
b3f6524e5c | ||
|
|
5ca0a0adf2 | ||
|
|
669d2c44c9 | ||
|
|
604bc5b4c1 | ||
|
|
88268ae569 | ||
|
|
140fb86401 | ||
|
|
2b281c4357 | ||
|
|
7b6dbfb456 | ||
|
|
b3c49ac86b | ||
|
|
d309fd9c97 | ||
|
|
f3a280dcb6 | ||
|
|
43894687a2 | ||
|
|
8dd8928dab | ||
|
|
282a4d15c2 | ||
|
|
83a2ffc67e | ||
|
|
e9a6e3fae0 | ||
|
|
97944b9793 | ||
|
|
0b5d995b6d | ||
|
|
1781a9fe9a | ||
|
|
a0479bf7ca | ||
|
|
2affa31edf | ||
|
|
0bd040a851 | ||
|
|
616c6a1607 | ||
| a5176fbf80 | |||
|
|
f2b76b7582 | ||
|
|
f4847f31ae | ||
|
|
05adcf2c12 | ||
|
|
ea301be3c1 | ||
|
|
88e9785b01 | ||
|
|
78efabfc1c | ||
|
|
9d3e22200c | ||
|
|
183f9701fd | ||
|
|
ebbba134fe | ||
|
|
c257be8176 | ||
|
|
afb0aea660 | ||
|
|
960bf46f44 | ||
|
|
16db799e88 | ||
|
|
f8f2cdcfcc | ||
|
|
a1f7862f96 | ||
|
|
3ff9284f2a | ||
|
|
edf8f27c0f | ||
|
|
a720953ff3 | ||
|
|
f79dd26a82 | ||
|
|
36b5fce4dd | ||
|
|
89537322fd | ||
|
|
2229ce1fe9 | ||
|
|
f257b2177a | ||
|
|
056c1fd43e | ||
|
|
acacd3134f | ||
|
|
1fe0ba1a6e | ||
|
|
e639f4b5de | ||
|
|
5a234bd989 | ||
|
|
661227266e | ||
|
|
e01556fd9a | ||
|
|
5cb53ffc18 | ||
|
|
e7178626ab | ||
|
|
7f0948b0ed | ||
|
|
4baee67781 | ||
|
|
7061f38abe | ||
|
|
2aa259c0b2 | ||
|
|
a0f74e715a | ||
|
|
c13e761c3f | ||
|
|
2837e44bab | ||
|
|
d41e3bb41e | ||
|
|
4160bbb8c4 | ||
|
|
404a20f280 | ||
|
|
2241146b9f | ||
|
|
bcb26c5721 | ||
|
|
a681d8c1ba | ||
|
|
0515b74f75 | ||
|
|
7eeb96d88e | ||
|
|
5b6c73ef06 | ||
|
|
c70b866d16 | ||
|
|
b84f2874dc | ||
|
|
c372b5529b | ||
|
|
a43156c38d | ||
|
|
a15112febc | ||
|
|
1c6a677a39 | ||
|
|
9bf749a2c8 | ||
|
|
f6c9d9df20 | ||
|
|
5386e0ded9 | ||
|
|
8fab405a3a | ||
|
|
ee409f3ca9 | ||
|
|
bda9beacaa | ||
|
|
5d8c7e5733 | ||
|
|
a68c763125 | ||
|
|
31ac89d9d6 | ||
|
|
8b258cbbe4 | ||
|
|
53676fc0fd | ||
|
|
42c561af9e | ||
|
|
fae777c14c | ||
|
|
1af6dc614a | ||
|
|
2986004638 | ||
|
|
d6f7b4706b | ||
|
|
b40dd3e5c0 | ||
|
|
2f4097ca9d | ||
|
|
c8a91fbb25 | ||
|
|
0ec259a5fc | ||
|
|
2fda99bdb7 | ||
|
|
1bb4ac4e9c | ||
|
|
f376b98c09 | ||
|
|
7382fdb2d2 | ||
|
|
aead9592cf | ||
|
|
bedacf29d9 | ||
| 570e9a8307 | |||
|
|
fcb94ec603 | ||
| 69886ed58b | |||
|
|
798235cd21 | ||
|
|
b38b6b6f35 | ||
|
|
2dba13c52e | ||
|
|
ff366cb2c5 | ||
|
|
a87861a993 | ||
|
|
65476356c9 | ||
|
|
8af6667f3e | ||
|
|
b546df7b95 | ||
|
|
20c1f12da8 | ||
|
|
e65ff201af | ||
|
|
4f367a3dcd | ||
|
|
d7093bce4d |
3
.github/CONTRIBUTING.md
vendored
3
.github/CONTRIBUTING.md
vendored
@@ -11,6 +11,7 @@ Do not report crashes in the GitHub issue tracker. NewPipe has an automated cras
|
||||
|
||||
* Search the [existing issues](https://github.com/TeamNewPipe/NewPipe/issues) first to make sure your issue/feature hasn't been reported/requested before
|
||||
* Check whether your issue/feature is already fixed/implemented
|
||||
* Check if the issue still exists in the latest release/beta version
|
||||
* If you are an Android/Java developer, you are always welcome to fix/implement an issue/a feature yourself. PRs welcome!
|
||||
* We use English for development. Issues in other languages will be closed and ignored.
|
||||
* Please only add *one* issue at a time. Do not put multiple issues into one thread.
|
||||
@@ -27,7 +28,7 @@ Do not report crashes in the GitHub issue tracker. NewPipe has an automated cras
|
||||
* Stick to NewPipe's style conventions (well, just look the other code and then do it the same way :))
|
||||
* Do not bring non-free software (e.g., binary blobs) into the project. Also, make sure you do not introduce Google libraries.
|
||||
* Stick to [F-Droid contribution guidelines](https://f-droid.org/wiki/page/Inclusion_Policy)
|
||||
* Make changes on a separate branch, not on the master branch. This is commonly known as *feature branch workflow*. You may then send your changes as a pull request on GitHub. Patches to the email address mentioned in this document might not be considered, GitHub is the primary platform.
|
||||
* Make changes on a separate branch, not on the master branch. This is commonly known as *feature branch workflow*. You may then send your changes as a pull request on GitHub. Patches to the email address mentioned in this document might not be considered, GitHub is the primary platform. (This only affects you if you are a member of TeamNewPipe)
|
||||
* When submitting changes, you confirm that your code is licensed under the terms of the [GNU General Public License v3](https://www.gnu.org/licenses/gpl-3.0.html).
|
||||
* Please test (compile and run) your code before you submit changes! Ideally, provide test feedback in the PR description. Untested code will **not** be merged!
|
||||
* Try to figure out yourself why builds on our CI fail.
|
||||
|
||||
@@ -5,7 +5,7 @@ android:
|
||||
components:
|
||||
# The BuildTools version used by NewPipe
|
||||
- tools
|
||||
- build-tools-27.0.1
|
||||
- build-tools-27.0.3
|
||||
|
||||
# The SDK version used to compile NewPipe
|
||||
- android-27
|
||||
|
||||
Binary file not shown.
@@ -1,170 +0,0 @@
|
||||
import java.io.BufferedReader;
|
||||
import java.io.File;
|
||||
import java.io.FileReader;
|
||||
import java.io.IOException;
|
||||
import java.util.regex.*;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.ArrayList;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.charset.Charset;
|
||||
|
||||
public final class CheckTranslations {
|
||||
|
||||
private static boolean debug = false;
|
||||
private static boolean plurals = false;
|
||||
private static boolean empty = false;
|
||||
private static boolean remove = false;
|
||||
private static int checks = 0;
|
||||
private static int matches = 0;
|
||||
private static int changes = 0;
|
||||
private static Pattern p, pb, pe, e, o;
|
||||
|
||||
/**
|
||||
* Search translated strings.xml files for empty item / plural tags
|
||||
* and remove them.
|
||||
* @param args directories which contain string.xml files (in any subdirectory)
|
||||
* -e option to find all empty string tags
|
||||
* -p option to find all empty plurals and item tags
|
||||
* -r option to remove all occurrences from the files
|
||||
* -d option to see more details
|
||||
*/
|
||||
public static void main(String[] args) {
|
||||
if (args.length < 1 || (args[0].equals("-d") && args.length < 2)) {
|
||||
System.out.println("Not enough arguments");
|
||||
return;
|
||||
}
|
||||
for (int i = 0; i < args.length; i++) {
|
||||
switch (args[i]) {
|
||||
case "-d":
|
||||
debug = true;
|
||||
break;
|
||||
case "-p":
|
||||
plurals = true;
|
||||
break;
|
||||
case "-e":
|
||||
empty = true;
|
||||
break;
|
||||
case "-r":
|
||||
remove = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!plurals && !empty) {
|
||||
plurals = true;
|
||||
empty = true;
|
||||
}
|
||||
|
||||
p = Pattern.compile("(<item quantity=\")(zero|one|two|three|few|many|other)(\"></item>|\"/>)");
|
||||
pb = Pattern.compile("(<plurals[\\sa-zA-Z=\"]*>)");
|
||||
pe = Pattern.compile("(</plurals>)");
|
||||
e = Pattern.compile("(<string[\\sa-z_\\\"=]*)((><\\/string>|\\/>){1})");
|
||||
o = Pattern.compile("(<item quantity=\"other\">)[^</>]*(<\\/item>)");
|
||||
|
||||
for (int i = 0; i < args.length; i++) {
|
||||
if (!args[i].equals("-d") && !args[i].equals("-p") && !args[i].equals("-e") && !args[i].equals("-r")) {
|
||||
File f = new File(args[i]);
|
||||
if (f.exists() && !f.isDirectory()) {
|
||||
checkFile(f);
|
||||
} else if (f.isDirectory()) {
|
||||
checkFiles(f.listFiles());
|
||||
} else {
|
||||
System.out.println("'" + args[i] + "' does not exist!");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
System.out.println(checks + " files were checked.");
|
||||
System.out.println(matches + " corrupt lines detected.");
|
||||
if (remove) {
|
||||
System.out.println(matches + " corrupt lines removed and " + changes + " lines fixed.");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private static void checkFiles(File[] f) {
|
||||
for (int i = 0; i < f.length; i++) {
|
||||
if (f[i].exists() && !f[i].isDirectory()) {
|
||||
if (f[i].toString().contains("strings.xml")) {
|
||||
checkFile(f[i]);
|
||||
}
|
||||
} else if (f[i].isDirectory()) {
|
||||
checkFiles(f[i].listFiles());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void checkFile(File f) {
|
||||
// Do not check our original English strings to cause no unwanted changes
|
||||
// Btw. there should not be empty plural/item tags
|
||||
if (f.toString().contains("values/strings.xml")) {
|
||||
return;
|
||||
}
|
||||
if (debug) System.out.println("Checking " + f.toString());
|
||||
checks++;
|
||||
|
||||
|
||||
List<String> lines = new ArrayList<String>();
|
||||
boolean checkFailed = false;
|
||||
boolean otherDetected = false;
|
||||
boolean inPlurals = false;
|
||||
try (BufferedReader br = new BufferedReader(new FileReader(f))) {
|
||||
String line;
|
||||
int ln = 0;
|
||||
while ((line = br.readLine()) != null) {
|
||||
ln++;
|
||||
if (plurals && p.matcher(line).find()) {
|
||||
matches++;
|
||||
if (debug) System.out.println(" Line " + ln + " was " + ((remove) ? "removed" : "detected") + ": '" + line + "'");
|
||||
checkFailed = true;
|
||||
} else if (empty && e.matcher(line).find()) {
|
||||
matches++;
|
||||
checkFailed = true;
|
||||
if (debug) System.out.println(" Line " + ln + " was " + ((remove) ? "removed" : "detected") + ": '" + line + "'");
|
||||
} else {
|
||||
if (remove) lines.add(line);
|
||||
}
|
||||
}
|
||||
br.close();
|
||||
int pluralsLine = 0;
|
||||
for (int i = 0; i < lines.size(); i++) {
|
||||
if (o.matcher(lines.get(i)).find()) {
|
||||
otherDetected = true;
|
||||
}
|
||||
if (plurals && pb.matcher(lines.get(i)).find()) {
|
||||
inPlurals = true;
|
||||
pluralsLine = i;
|
||||
} else if (plurals && pe.matcher(lines.get(i)).find()) {
|
||||
inPlurals = false;
|
||||
if (!otherDetected) {
|
||||
boolean b = false;
|
||||
check: for(int j = pluralsLine; j < i; j++) {
|
||||
if (lines.get(j).contains("many")) {
|
||||
b = true;
|
||||
pluralsLine = j;
|
||||
break check;
|
||||
}
|
||||
}
|
||||
if (remove && b) {
|
||||
if (debug) System.out.println(" Line " + (pluralsLine + 1) + " was " + ((remove) ? "changed" : "detected") + ": '" + lines.get(pluralsLine) + "'");
|
||||
lines.set(pluralsLine, lines.get(pluralsLine).replace("many", "other"));
|
||||
changes++;
|
||||
checkFailed = true;
|
||||
} else if (debug) {
|
||||
if (debug) System.out.println(" WARNING: Line " + (i + 1) + " - No <item quantity=\"other\"> found!");
|
||||
}
|
||||
}
|
||||
otherDetected = false;
|
||||
}
|
||||
|
||||
}
|
||||
if (remove && checkFailed) {
|
||||
Files.write(f.toPath(), lines, Charset.forName("UTF-8"));
|
||||
}
|
||||
} catch (IOException e) {
|
||||
System.out.println(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -88,7 +88,7 @@ If you like NewPipe we'd be happy about a donation. You can either donate via Bi
|
||||
<tr>
|
||||
<td><a href="https://liberapay.com/TeamNewPipe/"><img src="https://upload.wikimedia.org/wikipedia/commons/2/27/Liberapay_logo_v2_white-on-yellow.svg" alt="Liberapay" width="80px" /></a></td>
|
||||
<td><a href="https://liberapay.com/TeamNewPipe/"><img src="assets/liberapay_qr_code.png" alt="Visit NewPipe at liberapay.com" width="100px"/></a></td>
|
||||
<td><a href="https://liberapay.com/TeamNewPipe/donate/"><img src="assets/liberapay_donate_button.svg" alt="Donate via Liberapay" height="35px" /></a></td>
|
||||
<td><a href="https://liberapay.com/TeamNewPipe/donate"><img src="assets/liberapay_donate_button.svg" alt="Donate via Liberapay" height="35px" /></a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><a href="https://www.bountysource.com/teams/newpipe"><img src="https://upload.wikimedia.org/wikipedia/commons/thumb/2/22/Bountysource.png/320px-Bountysource.png" alt="Bountysource" width="190px" /></a></td>
|
||||
|
||||
@@ -2,14 +2,14 @@ apply plugin: 'com.android.application'
|
||||
|
||||
android {
|
||||
compileSdkVersion 27
|
||||
buildToolsVersion '27.0.1'
|
||||
buildToolsVersion '27.0.3'
|
||||
|
||||
defaultConfig {
|
||||
applicationId "org.schabi.newpipe"
|
||||
minSdkVersion 15
|
||||
targetSdkVersion 27
|
||||
versionCode 49
|
||||
versionName "0.13.0"
|
||||
versionCode 63
|
||||
versionName "0.13.4"
|
||||
|
||||
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
|
||||
vectorDrawables.useSupportLibrary = true
|
||||
@@ -26,13 +26,6 @@ android {
|
||||
debuggable true
|
||||
applicationIdSuffix ".debug"
|
||||
}
|
||||
beta {
|
||||
minifyEnabled true
|
||||
shrinkResources true
|
||||
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
|
||||
|
||||
applicationIdSuffix ".beta"
|
||||
}
|
||||
}
|
||||
|
||||
lintOptions {
|
||||
@@ -48,8 +41,8 @@ android {
|
||||
}
|
||||
|
||||
ext {
|
||||
supportLibVersion = '27.1.0'
|
||||
exoPlayerLibVersion = '2.7.1'
|
||||
supportLibVersion = '27.1.1'
|
||||
exoPlayerLibVersion = '2.7.3'
|
||||
roomDbLibVersion = '1.0.0'
|
||||
leakCanaryLibVersion = '1.5.4'
|
||||
okHttpLibVersion = '1.5.0'
|
||||
@@ -61,7 +54,7 @@ dependencies {
|
||||
exclude module: 'support-annotations'
|
||||
}
|
||||
|
||||
implementation 'com.github.TeamNewPipe:NewPipeExtractor:f787b375e5fb6d'
|
||||
implementation 'com.github.TeamNewPipe:NewPipeExtractor:bf1c771'
|
||||
|
||||
testImplementation 'junit:junit:4.12'
|
||||
testImplementation 'org.mockito:mockito-core:1.10.19'
|
||||
@@ -72,13 +65,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"
|
||||
|
||||
@@ -98,7 +91,6 @@ dependencies {
|
||||
annotationProcessor "frankiesardo:icepick-processor:$icepickLibVersion"
|
||||
|
||||
debugImplementation "com.squareup.leakcanary:leakcanary-android:$leakCanaryLibVersion"
|
||||
betaImplementation "com.squareup.leakcanary:leakcanary-android-no-op:$leakCanaryLibVersion"
|
||||
releaseImplementation "com.squareup.leakcanary:leakcanary-android-no-op:$leakCanaryLibVersion"
|
||||
|
||||
implementation 'com.squareup.okhttp3:okhttp:3.9.1'
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools">
|
||||
|
||||
<application
|
||||
android:label="NewPipe Beta"
|
||||
tools:replace="android:label">
|
||||
</application>
|
||||
|
||||
</manifest>
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 5.8 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 3.0 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 8.1 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 13 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 19 KiB |
@@ -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"
|
||||
@@ -76,8 +80,8 @@
|
||||
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"
|
||||
@@ -132,7 +136,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">
|
||||
|
||||
|
||||
@@ -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,13 @@ package org.schabi.newpipe;
|
||||
|
||||
import android.content.Intent;
|
||||
import android.content.SharedPreferences;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
import android.os.Handler;
|
||||
import android.os.Looper;
|
||||
import android.preference.PreferenceManager;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.design.widget.NavigationView;
|
||||
import android.support.v4.app.Fragment;
|
||||
import android.support.v4.view.GravityCompat;
|
||||
@@ -44,7 +46,6 @@ import android.view.View;
|
||||
import android.widget.Button;
|
||||
import android.widget.ImageButton;
|
||||
import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
|
||||
import org.schabi.newpipe.extractor.NewPipe;
|
||||
import org.schabi.newpipe.extractor.StreamingService;
|
||||
@@ -55,6 +56,7 @@ import org.schabi.newpipe.fragments.list.search.SearchFragment;
|
||||
import org.schabi.newpipe.report.ErrorActivity;
|
||||
import org.schabi.newpipe.util.Constants;
|
||||
import org.schabi.newpipe.util.NavigationHelper;
|
||||
import org.schabi.newpipe.util.PermissionHelper;
|
||||
import org.schabi.newpipe.util.ServiceHelper;
|
||||
import org.schabi.newpipe.util.StateSaver;
|
||||
import org.schabi.newpipe.util.ThemeHelper;
|
||||
@@ -94,44 +96,47 @@ public class MainActivity extends AppCompatActivity {
|
||||
drawer = findViewById(R.id.drawer_layout);
|
||||
drawerItems = findViewById(R.id.navigation);
|
||||
|
||||
//drawerItems.setItemIconTintList(null); // Set null to use the original icon
|
||||
for(StreamingService s : NewPipe.getServices()) {
|
||||
final String title = s.getServiceInfo().getName() +
|
||||
(ServiceHelper.isBeta(s) ? " (beta)" : "");
|
||||
final MenuItem item = drawerItems.getMenu()
|
||||
.add(R.id.menu_services_group, s.getServiceId(), 0, title);
|
||||
item.setIcon(ServiceHelper.getIcon(s.getServiceId()));
|
||||
}
|
||||
|
||||
drawerItems.getMenu().getItem(ServiceHelper.getSelectedServiceId(this)).setChecked(true);
|
||||
|
||||
if (!BuildConfig.BUILD_TYPE.equals("release")) {
|
||||
toggle = new ActionBarDrawerToggle(this, drawer, toolbar, R.string.drawer_open, R.string.drawer_close);
|
||||
toggle.syncState();
|
||||
drawer.addDrawerListener(toggle);
|
||||
drawer.addDrawerListener(new DrawerLayout.SimpleDrawerListener() {
|
||||
private int lastService;
|
||||
toggle = new ActionBarDrawerToggle(this, drawer, toolbar, R.string.drawer_open, R.string.drawer_close);
|
||||
toggle.syncState();
|
||||
drawer.addDrawerListener(toggle);
|
||||
drawer.addDrawerListener(new DrawerLayout.SimpleDrawerListener() {
|
||||
private int lastService;
|
||||
|
||||
@Override
|
||||
public void onDrawerOpened(View drawerView) {
|
||||
lastService = ServiceHelper.getSelectedServiceId(MainActivity.this);
|
||||
@Override
|
||||
public void onDrawerOpened(View drawerView) {
|
||||
lastService = ServiceHelper.getSelectedServiceId(MainActivity.this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDrawerClosed(View drawerView) {
|
||||
if (lastService != ServiceHelper.getSelectedServiceId(MainActivity.this)) {
|
||||
new Handler(Looper.getMainLooper()).post(MainActivity.this::recreate);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@Override
|
||||
public void onDrawerClosed(View drawerView) {
|
||||
if (lastService != ServiceHelper.getSelectedServiceId(MainActivity.this)) {
|
||||
new Handler(Looper.getMainLooper()).post(MainActivity.this::recreate);
|
||||
}
|
||||
}
|
||||
});
|
||||
drawerItems.setNavigationItemSelectedListener(this::changeService);
|
||||
|
||||
drawerItems.setNavigationItemSelectedListener(this::changeService);
|
||||
|
||||
setupDrawerFooter();
|
||||
setupDrawerHeader();
|
||||
} else {
|
||||
drawer.setDrawerLockMode(DrawerLayout.LOCK_MODE_LOCKED_CLOSED);
|
||||
}
|
||||
setupDrawerFooter();
|
||||
setupDrawerHeader();
|
||||
}
|
||||
|
||||
|
||||
private boolean changeService(MenuItem item) {
|
||||
if (item.getGroupId() == R.id.menu_services_group) {
|
||||
drawerItems.getMenu().getItem(ServiceHelper.getSelectedServiceId(this)).setChecked(false);
|
||||
ServiceHelper.setSelectedServiceId(this, item.getTitle().toString());
|
||||
ServiceHelper.setSelectedServiceId(this, item.getItemId());
|
||||
drawerItems.getMenu().getItem(ServiceHelper.getSelectedServiceId(this)).setChecked(true);
|
||||
headerServiceView.setText("gurken");
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
@@ -146,7 +151,8 @@ public class MainActivity extends AppCompatActivity {
|
||||
|
||||
settings.setOnClickListener(view -> NavigationHelper.openSettings(this));
|
||||
downloads.setOnClickListener(view ->NavigationHelper.openDownloads(this));
|
||||
history.setOnClickListener(view -> NavigationHelper.openHistory(this));
|
||||
history.setOnClickListener(view ->
|
||||
NavigationHelper.openStatisticFragment(getSupportFragmentManager()));
|
||||
}
|
||||
|
||||
private void setupDrawerHeader() {
|
||||
@@ -176,11 +182,9 @@ public class MainActivity extends AppCompatActivity {
|
||||
// when the user returns to MainActivity
|
||||
drawer.closeDrawer(Gravity.START, false);
|
||||
try {
|
||||
if(BuildConfig.BUILD_TYPE != "release" ) {
|
||||
String selectedServiceName = NewPipe.getService(
|
||||
ServiceHelper.getSelectedServiceId(this)).getServiceInfo().getName();
|
||||
headerServiceView.setText(selectedServiceName);
|
||||
}
|
||||
String selectedServiceName = NewPipe.getService(
|
||||
ServiceHelper.getSelectedServiceId(this)).getServiceInfo().getName();
|
||||
headerServiceView.setText(selectedServiceName);
|
||||
} catch (Exception e) {
|
||||
ErrorActivity.reportUiError(this, e);
|
||||
}
|
||||
@@ -232,6 +236,26 @@ public class MainActivity extends AppCompatActivity {
|
||||
} else super.onBackPressed();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
|
||||
for (int i: grantResults){
|
||||
if (i == PackageManager.PERMISSION_DENIED){
|
||||
return;
|
||||
}
|
||||
}
|
||||
switch (requestCode) {
|
||||
case PermissionHelper.DOWNLOADS_REQUEST_CODE:
|
||||
NavigationHelper.openDownloads(this);
|
||||
break;
|
||||
case PermissionHelper.DOWNLOAD_DIALOG_REQUEST_CODE:
|
||||
Fragment fragment = getSupportFragmentManager().findFragmentById(R.id.fragment_holder);
|
||||
if (fragment instanceof VideoDetailFragment) {
|
||||
((VideoDetailFragment) fragment).openDownloadDialog();
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Implement the following diagram behavior for the up button:
|
||||
* <pre>
|
||||
@@ -304,16 +328,16 @@ public class MainActivity extends AppCompatActivity {
|
||||
case android.R.id.home:
|
||||
onHomeButtonPressed();
|
||||
return true;
|
||||
case R.id.action_settings:
|
||||
NavigationHelper.openSettings(this);
|
||||
return true;
|
||||
case R.id.action_show_downloads:
|
||||
return NavigationHelper.openDownloads(this);
|
||||
case R.id.action_history:
|
||||
NavigationHelper.openStatisticFragment(getSupportFragmentManager());
|
||||
return true;
|
||||
case R.id.action_about:
|
||||
NavigationHelper.openAbout(this);
|
||||
return true;
|
||||
case R.id.action_history:
|
||||
NavigationHelper.openHistory(this);
|
||||
case R.id.action_settings:
|
||||
NavigationHelper.openSettings(this);
|
||||
return true;
|
||||
default:
|
||||
return super.onOptionsItemSelected(item);
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package org.schabi.newpipe;
|
||||
|
||||
import android.app.IntentService;
|
||||
import android.content.Context;
|
||||
import android.content.DialogInterface;
|
||||
import android.content.Intent;
|
||||
import android.content.SharedPreferences;
|
||||
@@ -24,7 +25,6 @@ import android.widget.Toast;
|
||||
|
||||
import org.schabi.newpipe.extractor.Info;
|
||||
import org.schabi.newpipe.extractor.NewPipe;
|
||||
import org.schabi.newpipe.extractor.ServiceList;
|
||||
import org.schabi.newpipe.extractor.StreamingService;
|
||||
import org.schabi.newpipe.extractor.StreamingService.LinkType;
|
||||
import org.schabi.newpipe.extractor.channel.ChannelInfo;
|
||||
@@ -32,10 +32,10 @@ import org.schabi.newpipe.extractor.exceptions.ExtractionException;
|
||||
import org.schabi.newpipe.extractor.playlist.PlaylistInfo;
|
||||
import org.schabi.newpipe.extractor.stream.StreamInfo;
|
||||
import org.schabi.newpipe.player.helper.PlayerHelper;
|
||||
import org.schabi.newpipe.playlist.ChannelPlayQueue;
|
||||
import org.schabi.newpipe.playlist.PlayQueue;
|
||||
import org.schabi.newpipe.playlist.PlaylistPlayQueue;
|
||||
import org.schabi.newpipe.playlist.SinglePlayQueue;
|
||||
import org.schabi.newpipe.player.playqueue.ChannelPlayQueue;
|
||||
import org.schabi.newpipe.player.playqueue.PlayQueue;
|
||||
import org.schabi.newpipe.player.playqueue.PlaylistPlayQueue;
|
||||
import org.schabi.newpipe.player.playqueue.SinglePlayQueue;
|
||||
import org.schabi.newpipe.report.UserAction;
|
||||
import org.schabi.newpipe.util.ExtractorHelper;
|
||||
import org.schabi.newpipe.util.NavigationHelper;
|
||||
@@ -43,9 +43,11 @@ import org.schabi.newpipe.util.PermissionHelper;
|
||||
import org.schabi.newpipe.util.ThemeHelper;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
|
||||
import icepick.Icepick;
|
||||
import icepick.State;
|
||||
@@ -57,6 +59,8 @@ import io.reactivex.disposables.Disposable;
|
||||
import io.reactivex.functions.Consumer;
|
||||
import io.reactivex.schedulers.Schedulers;
|
||||
|
||||
import static org.schabi.newpipe.extractor.StreamingService.ServiceInfo.MediaCapability.AUDIO;
|
||||
import static org.schabi.newpipe.extractor.StreamingService.ServiceInfo.MediaCapability.VIDEO;
|
||||
import static org.schabi.newpipe.util.ThemeHelper.resolveResourceIdFromAttr;
|
||||
|
||||
/**
|
||||
@@ -64,13 +68,10 @@ import static org.schabi.newpipe.util.ThemeHelper.resolveResourceIdFromAttr;
|
||||
*/
|
||||
public class RouterActivity extends AppCompatActivity {
|
||||
|
||||
@State
|
||||
protected int currentServiceId = -1;
|
||||
@State protected int currentServiceId = -1;
|
||||
private StreamingService currentService;
|
||||
@State
|
||||
protected LinkType currentLinkType;
|
||||
@State
|
||||
protected int selectedRadioPosition = -1;
|
||||
@State protected LinkType currentLinkType;
|
||||
@State protected int selectedRadioPosition = -1;
|
||||
protected int selectedPreviously = -1;
|
||||
|
||||
protected String currentUrl;
|
||||
@@ -91,8 +92,7 @@ public class RouterActivity extends AppCompatActivity {
|
||||
}
|
||||
|
||||
setTheme(ThemeHelper.isLightThemeSelected(this)
|
||||
? R.style.RouterActivityThemeLight
|
||||
: R.style.RouterActivityThemeDark);
|
||||
? R.style.RouterActivityThemeLight : R.style.RouterActivityThemeDark);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -122,7 +122,7 @@ public class RouterActivity extends AppCompatActivity {
|
||||
currentService = NewPipe.getServiceByUrl(url);
|
||||
currentServiceId = currentService.getServiceId();
|
||||
currentLinkType = currentService.getLinkTypeByUrl(url);
|
||||
currentUrl = NavigationHelper.getCleanUrl(currentService, url, currentLinkType);
|
||||
currentUrl = url;
|
||||
} else {
|
||||
currentService = NewPipe.getService(currentServiceId);
|
||||
}
|
||||
@@ -159,57 +159,69 @@ public class RouterActivity extends AppCompatActivity {
|
||||
|
||||
protected void onSuccess() {
|
||||
final SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(this);
|
||||
boolean isExtVideoEnabled = preferences.getBoolean(getString(R.string.use_external_video_player_key), false);
|
||||
boolean isExtAudioEnabled = preferences.getBoolean(getString(R.string.use_external_audio_player_key), false);
|
||||
final String selectedChoiceKey = preferences.getString(getString(R.string.preferred_open_action_key), getString(R.string.preferred_open_action_default));
|
||||
|
||||
if ((isExtAudioEnabled || isExtVideoEnabled) && currentLinkType != LinkType.STREAM) {
|
||||
Toast.makeText(this, R.string.external_player_unsupported_link_type, Toast.LENGTH_LONG).show();
|
||||
finish();
|
||||
return;
|
||||
}
|
||||
|
||||
// TODO: Add some sort of "capabilities" field to services (audio only, video and audio, etc.)
|
||||
if (currentService == ServiceList.SoundCloud) {
|
||||
handleChoice(getString(R.string.background_player_key));
|
||||
return;
|
||||
}
|
||||
|
||||
final String playerChoiceKey = preferences.getString(
|
||||
getString(R.string.preferred_open_action_key),
|
||||
getString(R.string.preferred_open_action_default));
|
||||
final String showInfoKey = getString(R.string.show_info_key);
|
||||
final String videoPlayerKey = getString(R.string.video_player_key);
|
||||
final String backgroundPlayerKey = getString(R.string.background_player_key);
|
||||
final String popupPlayerKey = getString(R.string.popup_player_key);
|
||||
final String alwaysAskKey = getString(R.string.always_ask_open_action_key);
|
||||
|
||||
if (playerChoiceKey.equals(alwaysAskKey)) {
|
||||
showDialog();
|
||||
if (selectedChoiceKey.equals(alwaysAskKey)) {
|
||||
final List<AdapterChoiceItem> choices = getChoicesForService(currentService, currentLinkType);
|
||||
|
||||
if (choices.size() == 1) {
|
||||
handleChoice(choices.get(0).key);
|
||||
} else if (choices.size() == 0) {
|
||||
handleChoice(showInfoKey);
|
||||
} else {
|
||||
showDialog(choices);
|
||||
}
|
||||
} else if (selectedChoiceKey.equals(showInfoKey)) {
|
||||
handleChoice(showInfoKey);
|
||||
} else {
|
||||
handleChoice(playerChoiceKey);
|
||||
final boolean isExtVideoEnabled = preferences.getBoolean(getString(R.string.use_external_video_player_key), false);
|
||||
final boolean isExtAudioEnabled = preferences.getBoolean(getString(R.string.use_external_audio_player_key), false);
|
||||
final boolean isVideoPlayerSelected = selectedChoiceKey.equals(videoPlayerKey) || selectedChoiceKey.equals(popupPlayerKey);
|
||||
final boolean isAudioPlayerSelected = selectedChoiceKey.equals(backgroundPlayerKey);
|
||||
|
||||
if (currentLinkType != LinkType.STREAM) {
|
||||
if (isExtAudioEnabled && isAudioPlayerSelected || isExtVideoEnabled && isVideoPlayerSelected) {
|
||||
Toast.makeText(this, R.string.external_player_unsupported_link_type, Toast.LENGTH_LONG).show();
|
||||
handleChoice(showInfoKey);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
final List<StreamingService.ServiceInfo.MediaCapability> capabilities = currentService.getServiceInfo().getMediaCapabilities();
|
||||
|
||||
boolean serviceSupportsChoice = false;
|
||||
if (isVideoPlayerSelected) {
|
||||
serviceSupportsChoice = capabilities.contains(VIDEO);
|
||||
} else if (selectedChoiceKey.equals(backgroundPlayerKey)) {
|
||||
serviceSupportsChoice = capabilities.contains(AUDIO);
|
||||
}
|
||||
|
||||
if (serviceSupportsChoice) {
|
||||
handleChoice(selectedChoiceKey);
|
||||
} else {
|
||||
handleChoice(showInfoKey);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void showDialog() {
|
||||
SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(this);
|
||||
final ContextThemeWrapper themeWrapper = new ContextThemeWrapper(this,
|
||||
ThemeHelper.isLightThemeSelected(this) ? R.style.LightTheme : R.style.DarkTheme);
|
||||
private void showDialog(final List<AdapterChoiceItem> choices) {
|
||||
final SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(this);
|
||||
final Context themeWrapperContext = getThemeWrapperContext();
|
||||
|
||||
LayoutInflater inflater = LayoutInflater.from(themeWrapper);
|
||||
final LayoutInflater inflater = LayoutInflater.from(themeWrapperContext);
|
||||
final LinearLayout rootLayout = (LinearLayout) inflater.inflate(R.layout.preferred_player_dialog_view, null, false);
|
||||
final RadioGroup radioGroup = rootLayout.findViewById(android.R.id.list);
|
||||
|
||||
final AdapterChoiceItem[] choices = {
|
||||
new AdapterChoiceItem(getString(R.string.show_info_key), getString(R.string.show_info),
|
||||
resolveResourceIdFromAttr(themeWrapper, R.attr.info)),
|
||||
new AdapterChoiceItem(getString(R.string.video_player_key), getString(R.string.video_player),
|
||||
resolveResourceIdFromAttr(themeWrapper, R.attr.play)),
|
||||
new AdapterChoiceItem(getString(R.string.background_player_key), getString(R.string.background_player),
|
||||
resolveResourceIdFromAttr(themeWrapper, R.attr.audio)),
|
||||
new AdapterChoiceItem(getString(R.string.popup_player_key), getString(R.string.popup_player),
|
||||
resolveResourceIdFromAttr(themeWrapper, R.attr.popup))
|
||||
};
|
||||
|
||||
final DialogInterface.OnClickListener dialogButtonsClickListener = (dialog, which) -> {
|
||||
final int indexOfChild = radioGroup.indexOfChild(
|
||||
radioGroup.findViewById(radioGroup.getCheckedRadioButtonId()));
|
||||
final AdapterChoiceItem choice = choices[indexOfChild];
|
||||
final AdapterChoiceItem choice = choices.get(indexOfChild);
|
||||
|
||||
handleChoice(choice.key);
|
||||
|
||||
@@ -218,8 +230,8 @@ public class RouterActivity extends AppCompatActivity {
|
||||
}
|
||||
};
|
||||
|
||||
final AlertDialog alertDialog = new AlertDialog.Builder(themeWrapper)
|
||||
.setTitle(R.string.preferred_player_share_menu_title)
|
||||
final AlertDialog alertDialog = new AlertDialog.Builder(themeWrapperContext)
|
||||
.setTitle(R.string.preferred_open_action_share_menu_title)
|
||||
.setView(radioGroup)
|
||||
.setCancelable(true)
|
||||
.setNegativeButton(R.string.just_once, dialogButtonsClickListener)
|
||||
@@ -227,6 +239,7 @@ public class RouterActivity extends AppCompatActivity {
|
||||
.setOnDismissListener((dialog) -> finish())
|
||||
.create();
|
||||
|
||||
//noinspection CodeBlock2Expr
|
||||
alertDialog.setOnShowListener(dialog -> {
|
||||
setDialogButtonsState(alertDialog, radioGroup.getCheckedRadioButtonId() != -1);
|
||||
});
|
||||
@@ -240,7 +253,7 @@ public class RouterActivity extends AppCompatActivity {
|
||||
selectedRadioPosition = indexOfChild;
|
||||
|
||||
if (selectedPreviously == selectedRadioPosition) {
|
||||
handleChoice(choices[selectedRadioPosition].key);
|
||||
handleChoice(choices.get(selectedRadioPosition).key);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -259,8 +272,8 @@ public class RouterActivity extends AppCompatActivity {
|
||||
if (selectedRadioPosition == -1) {
|
||||
final String lastSelectedPlayer = preferences.getString(getString(R.string.preferred_open_action_last_selected_key), null);
|
||||
if (!TextUtils.isEmpty(lastSelectedPlayer)) {
|
||||
for (int i = 0; i < choices.length; i++) {
|
||||
AdapterChoiceItem c = choices[i];
|
||||
for (int i = 0; i < choices.size(); i++) {
|
||||
AdapterChoiceItem c = choices.get(i);
|
||||
if (lastSelectedPlayer.equals(c.key)) {
|
||||
selectedRadioPosition = i;
|
||||
break;
|
||||
@@ -269,7 +282,7 @@ public class RouterActivity extends AppCompatActivity {
|
||||
}
|
||||
}
|
||||
|
||||
selectedRadioPosition = Math.min(Math.max(-1, selectedRadioPosition), choices.length - 1);
|
||||
selectedRadioPosition = Math.min(Math.max(-1, selectedRadioPosition), choices.size() - 1);
|
||||
if (selectedRadioPosition != -1) {
|
||||
((RadioButton) radioGroup.getChildAt(selectedRadioPosition)).setChecked(true);
|
||||
}
|
||||
@@ -278,6 +291,39 @@ public class RouterActivity extends AppCompatActivity {
|
||||
alertDialog.show();
|
||||
}
|
||||
|
||||
private List<AdapterChoiceItem> getChoicesForService(StreamingService service, LinkType linkType) {
|
||||
final Context context = getThemeWrapperContext();
|
||||
|
||||
final List<AdapterChoiceItem> returnList = new ArrayList<>();
|
||||
final List<StreamingService.ServiceInfo.MediaCapability> capabilities = service.getServiceInfo().getMediaCapabilities();
|
||||
|
||||
final SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(this);
|
||||
boolean isExtVideoEnabled = preferences.getBoolean(getString(R.string.use_external_video_player_key), false);
|
||||
boolean isExtAudioEnabled = preferences.getBoolean(getString(R.string.use_external_audio_player_key), false);
|
||||
|
||||
returnList.add(new AdapterChoiceItem(getString(R.string.show_info_key), getString(R.string.show_info),
|
||||
resolveResourceIdFromAttr(context, R.attr.info)));
|
||||
|
||||
if (capabilities.contains(VIDEO) && !(isExtVideoEnabled && linkType != LinkType.STREAM)) {
|
||||
returnList.add(new AdapterChoiceItem(getString(R.string.video_player_key), getString(R.string.video_player),
|
||||
resolveResourceIdFromAttr(context, R.attr.play)));
|
||||
returnList.add(new AdapterChoiceItem(getString(R.string.popup_player_key), getString(R.string.popup_player),
|
||||
resolveResourceIdFromAttr(context, R.attr.popup)));
|
||||
}
|
||||
|
||||
if (capabilities.contains(AUDIO) && !(isExtAudioEnabled && linkType != LinkType.STREAM)) {
|
||||
returnList.add(new AdapterChoiceItem(getString(R.string.background_player_key), getString(R.string.background_player),
|
||||
resolveResourceIdFromAttr(context, R.attr.audio)));
|
||||
}
|
||||
|
||||
return returnList;
|
||||
}
|
||||
|
||||
private Context getThemeWrapperContext() {
|
||||
return new ContextThemeWrapper(this,
|
||||
ThemeHelper.isLightThemeSelected(this) ? R.style.LightTheme : R.style.DarkTheme);
|
||||
}
|
||||
|
||||
private void setDialogButtonsState(AlertDialog dialog, boolean state) {
|
||||
final Button negativeButton = dialog.getButton(DialogInterface.BUTTON_NEGATIVE);
|
||||
final Button positiveButton = dialog.getButton(DialogInterface.BUTTON_POSITIVE);
|
||||
@@ -287,17 +333,15 @@ public class RouterActivity extends AppCompatActivity {
|
||||
positiveButton.setEnabled(state);
|
||||
}
|
||||
|
||||
private void handleChoice(final String playerChoiceKey) {
|
||||
if (Arrays.asList(getResources()
|
||||
.getStringArray(R.array.preferred_open_action_values_list))
|
||||
.contains(playerChoiceKey)) {
|
||||
private void handleChoice(final String selectedChoiceKey) {
|
||||
final List<String> validChoicesList = Arrays.asList(getResources().getStringArray(R.array.preferred_open_action_values_list));
|
||||
if (validChoicesList.contains(selectedChoiceKey)) {
|
||||
PreferenceManager.getDefaultSharedPreferences(this).edit()
|
||||
.putString(getString(R.string.preferred_open_action_last_selected_key),
|
||||
playerChoiceKey).apply();
|
||||
.putString(getString(R.string.preferred_open_action_last_selected_key), selectedChoiceKey)
|
||||
.apply();
|
||||
}
|
||||
|
||||
if (playerChoiceKey.equals(getString(R.string.popup_player_key))
|
||||
&& !PermissionHelper.isPopupEnabled(this)) {
|
||||
if (selectedChoiceKey.equals(getString(R.string.popup_player_key)) && !PermissionHelper.isPopupEnabled(this)) {
|
||||
PermissionHelper.showPopupEnablementToast(this);
|
||||
finish();
|
||||
return;
|
||||
@@ -305,7 +349,7 @@ public class RouterActivity extends AppCompatActivity {
|
||||
|
||||
// stop and bypass FetcherService if InfoScreen was selected since
|
||||
// StreamDetailFragment can fetch data itself
|
||||
if(playerChoiceKey.equals(getString(R.string.show_info_key))) {
|
||||
if (selectedChoiceKey.equals(getString(R.string.show_info_key))) {
|
||||
disposables.add(Observable
|
||||
.fromCallable(() -> NavigationHelper.getIntentByLink(this, currentUrl))
|
||||
.subscribeOn(Schedulers.io())
|
||||
@@ -322,11 +366,8 @@ public class RouterActivity extends AppCompatActivity {
|
||||
}
|
||||
|
||||
final Intent intent = new Intent(this, FetcherService.class);
|
||||
intent.putExtra(FetcherService.KEY_CHOICE,
|
||||
new Choice(currentService.getServiceId(),
|
||||
currentLinkType,
|
||||
currentUrl,
|
||||
playerChoiceKey));
|
||||
final Choice choice = new Choice(currentService.getServiceId(), currentLinkType, currentUrl, selectedChoiceKey);
|
||||
intent.putExtra(FetcherService.KEY_CHOICE, choice);
|
||||
startService(intent);
|
||||
|
||||
finish();
|
||||
@@ -334,8 +375,7 @@ public class RouterActivity extends AppCompatActivity {
|
||||
|
||||
private static class AdapterChoiceItem {
|
||||
final String description, key;
|
||||
@DrawableRes
|
||||
final int icon;
|
||||
@DrawableRes final int icon;
|
||||
|
||||
AdapterChoiceItem(String key, String description, int icon) {
|
||||
this.description = description;
|
||||
@@ -554,7 +594,7 @@ public class RouterActivity extends AppCompatActivity {
|
||||
* @param sharedText text to scan for URLs.
|
||||
* @return potential URLs
|
||||
*/
|
||||
protected String[] getUris(final String sharedText) {
|
||||
protected String[] getUris(final String sharedText) {
|
||||
final Collection<String> result = new HashSet<>();
|
||||
if (sharedText != null) {
|
||||
final String[] array = sharedText.split("\\p{Space}");
|
||||
|
||||
@@ -32,7 +32,6 @@ public class AboutActivity extends AppCompatActivity {
|
||||
new SoftwareComponent("Giga Get", "2014", "Peter Cai", "https://github.com/PaperAirplane-Dev-Team/GigaGet", StandardLicenses.GPL2),
|
||||
new SoftwareComponent("NewPipe Extractor", "2017", "Christian Schabesberger", "https://github.com/TeamNewPipe/NewPipeExtractor", StandardLicenses.GPL3),
|
||||
new SoftwareComponent("Jsoup", "2017", "Jonathan Hedley", "https://github.com/jhy/jsoup", StandardLicenses.MIT),
|
||||
new SoftwareComponent("Google Gson", "2008", "Google Inc", "https://github.com/google/gson", StandardLicenses.APACHE2),
|
||||
new SoftwareComponent("Rhino", "2015", "Mozilla", "https://www.mozilla.org/rhino/", StandardLicenses.MPL2),
|
||||
new SoftwareComponent("ACRA", "2013", "Kevin Gaudin", "http://www.acra.ch", StandardLicenses.APACHE2),
|
||||
new SoftwareComponent("Universal Image Loader", "2011 - 2015", "Sergey Tarasevich", "https://github.com/nostra13/Android-Universal-Image-Loader", StandardLicenses.APACHE2),
|
||||
@@ -129,47 +128,31 @@ public class AboutActivity extends AppCompatActivity {
|
||||
public View onCreateView(LayoutInflater inflater, ViewGroup container,
|
||||
Bundle savedInstanceState) {
|
||||
View rootView = inflater.inflate(R.layout.fragment_about, container, false);
|
||||
Context context = this.getContext();
|
||||
|
||||
TextView version = rootView.findViewById(R.id.app_version);
|
||||
version.setText(BuildConfig.VERSION_NAME);
|
||||
|
||||
View githubLink = rootView.findViewById(R.id.github_link);
|
||||
githubLink.setOnClickListener(new OnGithubLinkClickListener());
|
||||
githubLink.setOnClickListener(nv -> openWebsite(context.getString(R.string.github_url), context));
|
||||
|
||||
View donationLink = rootView.findViewById(R.id.donation_link);
|
||||
donationLink.setOnClickListener(new OnDonationLinkClickListener());
|
||||
donationLink.setOnClickListener(v -> openWebsite(context.getString(R.string.donation_url), context));
|
||||
|
||||
View websiteLink = rootView.findViewById(R.id.website_link);
|
||||
websiteLink.setOnClickListener(new OnWebsiteLinkClickListener());
|
||||
websiteLink.setOnClickListener(nv -> openWebsite(context.getString(R.string.website_url), context));
|
||||
|
||||
View privacyPolicyLink = rootView.findViewById(R.id.privacy_policy_link);
|
||||
privacyPolicyLink.setOnClickListener(v -> openWebsite(context.getString(R.string.privacy_policy_url), context));
|
||||
|
||||
return rootView;
|
||||
}
|
||||
|
||||
private static class OnGithubLinkClickListener implements View.OnClickListener {
|
||||
@Override
|
||||
public void onClick(final View view) {
|
||||
final Context context = view.getContext();
|
||||
Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(context.getString(R.string.github_url)));
|
||||
context.startActivity(intent);
|
||||
}
|
||||
private void openWebsite(String url, Context context) {
|
||||
Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(url));
|
||||
context.startActivity(intent);
|
||||
}
|
||||
|
||||
private static class OnDonationLinkClickListener implements View.OnClickListener {
|
||||
@Override
|
||||
public void onClick(final View view) {
|
||||
final Context context = view.getContext();
|
||||
Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(context.getString(R.string.donation_url)));
|
||||
context.startActivity(intent);
|
||||
}
|
||||
}
|
||||
|
||||
private static class OnWebsiteLinkClickListener implements View.OnClickListener {
|
||||
@Override
|
||||
public void onClick(final View view) {
|
||||
final Context context = view.getContext();
|
||||
Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(context.getString(R.string.website_url)));
|
||||
context.startActivity(intent);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package org.schabi.newpipe.about;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.net.Uri;
|
||||
@@ -39,7 +40,7 @@ public class LicenseFragment extends Fragment {
|
||||
* @param license the license to show
|
||||
*/
|
||||
public static void showLicense(Context context, License license) {
|
||||
new LicenseFragmentHelper().execute(context, license);
|
||||
new LicenseFragmentHelper((Activity) context).execute(license);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
package org.schabi.newpipe.about;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.Context;
|
||||
import android.content.DialogInterface;
|
||||
import android.os.AsyncTask;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.support.v7.app.AlertDialog;
|
||||
import android.webkit.WebView;
|
||||
import org.schabi.newpipe.R;
|
||||
@@ -10,26 +13,46 @@ import org.schabi.newpipe.util.ThemeHelper;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.InputStreamReader;
|
||||
import java.lang.ref.WeakReference;
|
||||
|
||||
public class LicenseFragmentHelper extends AsyncTask<Object, Void, Integer> {
|
||||
|
||||
private Context context;
|
||||
WeakReference<Activity> weakReference;
|
||||
private License license;
|
||||
|
||||
public LicenseFragmentHelper(@Nullable Activity activity) {
|
||||
weakReference = new WeakReference<>(activity);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private Activity getActivity() {
|
||||
Activity activity = weakReference.get();
|
||||
|
||||
if (activity != null && activity.isFinishing()) {
|
||||
return null;
|
||||
} else {
|
||||
return activity;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Integer doInBackground(Object... objects) {
|
||||
context = (Context) objects[0];
|
||||
license = (License) objects[1];
|
||||
license = (License) objects[0];
|
||||
return 1;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPostExecute(Integer result){
|
||||
String webViewData = getFormattedLicense(context, license);
|
||||
AlertDialog.Builder alert = new AlertDialog.Builder(context);
|
||||
protected void onPostExecute(Integer result) {
|
||||
Activity activity = getActivity();
|
||||
if (activity == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
String webViewData = getFormattedLicense(activity, license);
|
||||
AlertDialog.Builder alert = new AlertDialog.Builder(activity);
|
||||
alert.setTitle(license.getName());
|
||||
|
||||
WebView wv = new WebView(context);
|
||||
WebView wv = new WebView(activity);
|
||||
wv.loadData(webViewData, "text/html; charset=UTF-8", null);
|
||||
|
||||
alert.setView(wv);
|
||||
|
||||
@@ -9,7 +9,7 @@ import android.arch.persistence.room.PrimaryKey;
|
||||
import org.schabi.newpipe.extractor.stream.StreamInfo;
|
||||
import org.schabi.newpipe.extractor.stream.StreamInfoItem;
|
||||
import org.schabi.newpipe.extractor.stream.StreamType;
|
||||
import org.schabi.newpipe.playlist.PlayQueueItem;
|
||||
import org.schabi.newpipe.player.playqueue.PlayQueueItem;
|
||||
import org.schabi.newpipe.util.Constants;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -239,7 +239,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));
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
|
||||
@@ -27,10 +27,10 @@ import org.schabi.newpipe.extractor.ServiceList;
|
||||
import org.schabi.newpipe.extractor.StreamingService;
|
||||
import org.schabi.newpipe.extractor.kiosk.KioskList;
|
||||
import org.schabi.newpipe.fragments.list.channel.ChannelFragment;
|
||||
import org.schabi.newpipe.fragments.list.feed.FeedFragment;
|
||||
import org.schabi.newpipe.local.feed.FeedFragment;
|
||||
import org.schabi.newpipe.fragments.list.kiosk.KioskFragment;
|
||||
import org.schabi.newpipe.fragments.local.bookmark.BookmarkFragment;
|
||||
import org.schabi.newpipe.fragments.subscription.SubscriptionFragment;
|
||||
import org.schabi.newpipe.local.bookmark.BookmarkFragment;
|
||||
import org.schabi.newpipe.local.subscription.SubscriptionFragment;
|
||||
import org.schabi.newpipe.report.ErrorActivity;
|
||||
import org.schabi.newpipe.report.UserAction;
|
||||
import org.schabi.newpipe.util.KioskTranslator;
|
||||
|
||||
@@ -1,74 +0,0 @@
|
||||
package org.schabi.newpipe.fragments.detail;
|
||||
|
||||
import android.content.Context;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.BaseAdapter;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.Spinner;
|
||||
import android.widget.TextView;
|
||||
|
||||
import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.extractor.MediaFormat;
|
||||
import org.schabi.newpipe.extractor.stream.VideoStream;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class SpinnerToolbarAdapter extends BaseAdapter {
|
||||
private final List<VideoStream> videoStreams;
|
||||
private final boolean showIconNoAudio;
|
||||
|
||||
private final Context context;
|
||||
|
||||
public SpinnerToolbarAdapter(Context context, List<VideoStream> videoStreams, boolean showIconNoAudio) {
|
||||
this.context = context;
|
||||
this.videoStreams = videoStreams;
|
||||
this.showIconNoAudio = showIconNoAudio;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getCount() {
|
||||
return videoStreams.size();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object getItem(int position) {
|
||||
return videoStreams.get(position);
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getItemId(int position) {
|
||||
return position;
|
||||
}
|
||||
|
||||
@Override
|
||||
public View getDropDownView(int position, View convertView, ViewGroup parent) {
|
||||
return getCustomView(position, convertView, parent, true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public View getView(int position, View convertView, ViewGroup parent) {
|
||||
return getCustomView(((Spinner) parent).getSelectedItemPosition(), convertView, parent, false);
|
||||
}
|
||||
|
||||
private View getCustomView(int position, View convertView, ViewGroup parent, boolean isDropdownItem) {
|
||||
if (convertView == null) {
|
||||
convertView = LayoutInflater.from(context).inflate(R.layout.resolutions_spinner_item, parent, false);
|
||||
}
|
||||
|
||||
ImageView woSoundIcon = convertView.findViewById(R.id.wo_sound_icon);
|
||||
TextView text = convertView.findViewById(android.R.id.text1);
|
||||
VideoStream item = (VideoStream) getItem(position);
|
||||
text.setText(item.getFormat().getName() + " " + item.getResolution());
|
||||
|
||||
int visibility = !showIconNoAudio ? View.GONE
|
||||
: item.isVideoOnly ? View.VISIBLE
|
||||
: isDropdownItem ? View.INVISIBLE
|
||||
: View.GONE;
|
||||
woSoundIcon.setVisibility(visibility);
|
||||
|
||||
return convertView;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -12,6 +12,7 @@ import android.preference.PreferenceManager;
|
||||
import android.support.annotation.DrawableRes;
|
||||
import android.support.annotation.FloatRange;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.support.v4.content.ContextCompat;
|
||||
import android.support.v4.view.animation.FastOutSlowInInterpolator;
|
||||
import android.support.v7.app.ActionBar;
|
||||
@@ -61,16 +62,17 @@ import org.schabi.newpipe.extractor.stream.StreamType;
|
||||
import org.schabi.newpipe.extractor.stream.VideoStream;
|
||||
import org.schabi.newpipe.fragments.BackPressable;
|
||||
import org.schabi.newpipe.fragments.BaseStateFragment;
|
||||
import org.schabi.newpipe.fragments.local.dialog.PlaylistAppendDialog;
|
||||
import org.schabi.newpipe.util.StreamItemAdapter;
|
||||
import org.schabi.newpipe.util.StreamItemAdapter.StreamSizeWrapper;
|
||||
import org.schabi.newpipe.local.dialog.PlaylistAppendDialog;
|
||||
import org.schabi.newpipe.info_list.InfoItemBuilder;
|
||||
import org.schabi.newpipe.info_list.InfoItemDialog;
|
||||
import org.schabi.newpipe.player.MainVideoPlayer;
|
||||
import org.schabi.newpipe.player.PopupVideoPlayer;
|
||||
import org.schabi.newpipe.player.helper.PlayerHelper;
|
||||
import org.schabi.newpipe.player.old.PlayVideoActivity;
|
||||
import org.schabi.newpipe.playlist.PlayQueue;
|
||||
import org.schabi.newpipe.playlist.SinglePlayQueue;
|
||||
import org.schabi.newpipe.report.ErrorActivity;
|
||||
import org.schabi.newpipe.player.playqueue.PlayQueue;
|
||||
import org.schabi.newpipe.player.playqueue.SinglePlayQueue;
|
||||
import org.schabi.newpipe.report.UserAction;
|
||||
import org.schabi.newpipe.util.Constants;
|
||||
import org.schabi.newpipe.util.ExtractorHelper;
|
||||
@@ -84,9 +86,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 +111,6 @@ public class VideoDetailFragment
|
||||
// Amount of videos to show on start
|
||||
private static final int INITIAL_RELATED_VIDEOS = 8;
|
||||
|
||||
private ArrayList<VideoStream> sortedStreamVideosList;
|
||||
|
||||
private InfoItemBuilder infoItemBuilder = null;
|
||||
|
||||
private int updateFlags = 0;
|
||||
@@ -121,18 +122,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();
|
||||
|
||||
private int selectedVideoStream = -1;
|
||||
private List<VideoStream> sortedVideoStreams;
|
||||
private int selectedVideoStreamIndex = -1;
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Views
|
||||
@@ -205,7 +204,7 @@ public class VideoDetailFragment
|
||||
}
|
||||
|
||||
@Override
|
||||
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
|
||||
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
|
||||
return inflater.inflate(R.layout.fragment_video_detail, container, false);
|
||||
}
|
||||
|
||||
@@ -356,21 +355,8 @@ public class VideoDetailFragment
|
||||
}
|
||||
break;
|
||||
case R.id.detail_controls_download:
|
||||
if (!PermissionHelper.checkStoragePermissions(activity)) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
DownloadDialog downloadDialog =
|
||||
DownloadDialog.newInstance(currentInfo,
|
||||
sortedStreamVideosList,
|
||||
selectedVideoStream);
|
||||
downloadDialog.show(activity.getSupportFragmentManager(), "downloadDialog");
|
||||
} catch (Exception e) {
|
||||
Toast.makeText(activity,
|
||||
R.string.could_not_setup_download_menu,
|
||||
Toast.LENGTH_LONG).show();
|
||||
e.printStackTrace();
|
||||
if (PermissionHelper.checkStoragePermissions(activity, PermissionHelper.DOWNLOAD_DIALOG_REQUEST_CODE)) {
|
||||
this.openDownloadDialog();
|
||||
}
|
||||
break;
|
||||
case R.id.detail_uploader_root_layout:
|
||||
@@ -412,6 +398,9 @@ public class VideoDetailFragment
|
||||
case R.id.detail_controls_popup:
|
||||
openPopupPlayer(true);
|
||||
break;
|
||||
case R.id.detail_controls_download:
|
||||
NavigationHelper.openDownloads(getActivity());
|
||||
break;
|
||||
}
|
||||
|
||||
return true;
|
||||
@@ -533,6 +522,7 @@ public class VideoDetailFragment
|
||||
detailControlsPopup.setOnClickListener(this);
|
||||
detailControlsAddToPlaylist.setOnClickListener(this);
|
||||
detailControlsDownload.setOnClickListener(this);
|
||||
detailControlsDownload.setOnLongClickListener(this);
|
||||
relatedStreamExpandButton.setOnClickListener(this);
|
||||
|
||||
detailControlsBackground.setLongClickable(true);
|
||||
@@ -549,7 +539,8 @@ public class VideoDetailFragment
|
||||
|
||||
final String[] commands = new String[]{
|
||||
context.getResources().getString(R.string.enqueue_on_background),
|
||||
context.getResources().getString(R.string.enqueue_on_popup)
|
||||
context.getResources().getString(R.string.enqueue_on_popup),
|
||||
context.getResources().getString(R.string.append_playlist)
|
||||
};
|
||||
|
||||
final DialogInterface.OnClickListener actions = (DialogInterface dialogInterface, int i) -> {
|
||||
@@ -560,6 +551,12 @@ public class VideoDetailFragment
|
||||
case 1:
|
||||
NavigationHelper.enqueueOnPopupPlayer(getActivity(), new SinglePlayQueue(item));
|
||||
break;
|
||||
case 2:
|
||||
if (getFragmentManager() != null) {
|
||||
PlaylistAppendDialog.fromStreamInfoItems(Collections.singletonList(item))
|
||||
.show(getFragmentManager(), TAG);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
@@ -681,15 +678,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 +719,25 @@ public class VideoDetailFragment
|
||||
|
||||
private void setupActionBar(final StreamInfo info) {
|
||||
if (DEBUG) Log.d(TAG, "setupActionBarHandler() called with: info = [" + info + "]");
|
||||
sortedStreamVideosList = new ArrayList<>(ListHelper.getSortedStreamVideosList(
|
||||
activity, info.getVideoStreams(), info.getVideoOnlyStreams(), false));
|
||||
|
||||
selectedVideoStream = ListHelper.getDefaultResolutionIndex(activity, sortedStreamVideosList);
|
||||
|
||||
boolean isExternalPlayerEnabled = PreferenceManager.getDefaultSharedPreferences(activity)
|
||||
.getBoolean(activity.getString(R.string.use_external_video_player_key), false);
|
||||
spinnerToolbar.setAdapter(new SpinnerToolbarAdapter(activity, sortedStreamVideosList,
|
||||
isExternalPlayerEnabled));
|
||||
spinnerToolbar.setSelection(selectedVideoStream);
|
||||
|
||||
sortedVideoStreams = ListHelper.getSortedStreamVideosList(activity, info.getVideoStreams(), info.getVideoOnlyStreams(), false);
|
||||
selectedVideoStreamIndex = ListHelper.getDefaultResolutionIndex(activity, sortedVideoStreams);
|
||||
|
||||
final StreamItemAdapter<VideoStream> streamsAdapter = new StreamItemAdapter<>(activity, new StreamSizeWrapper<>(sortedVideoStreams), isExternalPlayerEnabled);
|
||||
spinnerToolbar.setAdapter(streamsAdapter);
|
||||
spinnerToolbar.setSelection(selectedVideoStreamIndex);
|
||||
spinnerToolbar.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
|
||||
@Override
|
||||
public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
|
||||
selectedVideoStream = position;
|
||||
selectedVideoStreamIndex = position;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onNothingSelected(AdapterView<?> parent) {
|
||||
}
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
@@ -818,7 +813,7 @@ public class VideoDetailFragment
|
||||
public void prepareAndHandleInfo(final StreamInfo info, boolean scrollToTop) {
|
||||
if (DEBUG) Log.d(TAG, "prepareAndHandleInfo() called with: info = [" + info + "], scrollToTop = [" + scrollToTop + "]");
|
||||
|
||||
setInitialData(info.getServiceId(), info.getUrl(), info.getName());
|
||||
setInitialData(info.getServiceId(), info.getOriginalUrl(), info.getName());
|
||||
pushToStack(serviceId, url, name);
|
||||
showLoading();
|
||||
|
||||
@@ -954,8 +949,9 @@ public class VideoDetailFragment
|
||||
this.autoPlayEnabled = autoplay;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private VideoStream getSelectedVideoStream() {
|
||||
return sortedStreamVideosList.get(selectedVideoStream);
|
||||
return sortedVideoStreams != null ? sortedVideoStreams.get(selectedVideoStreamIndex) : null;
|
||||
}
|
||||
|
||||
private void prepareDescription(final String descriptionHtml) {
|
||||
@@ -1112,7 +1108,7 @@ public class VideoDetailFragment
|
||||
public void handleResult(@NonNull StreamInfo info) {
|
||||
super.handleResult(info);
|
||||
|
||||
setInitialData(info.getServiceId(), info.getUrl(), info.getName());
|
||||
setInitialData(info.getServiceId(), info.getOriginalUrl(), info.getName());
|
||||
pushToStack(serviceId, url, name);
|
||||
|
||||
animateView(thumbnailPlayButton, true, 200);
|
||||
@@ -1192,7 +1188,9 @@ public class VideoDetailFragment
|
||||
toggleExpandRelatedVideos(currentInfo);
|
||||
wasRelatedStreamsExpanded = false;
|
||||
}
|
||||
|
||||
setTitleToUrl(info.getServiceId(), info.getUrl(), info.getName());
|
||||
setTitleToUrl(info.getServiceId(), info.getOriginalUrl(), info.getName());
|
||||
|
||||
if (!info.getErrors().isEmpty()) {
|
||||
showSnackBarError(info.getErrors(),
|
||||
@@ -1226,6 +1224,23 @@ public class VideoDetailFragment
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public void openDownloadDialog() {
|
||||
try {
|
||||
DownloadDialog downloadDialog = DownloadDialog.newInstance(currentInfo);
|
||||
downloadDialog.setVideoStreams(sortedVideoStreams);
|
||||
downloadDialog.setAudioStreams(currentInfo.getAudioStreams());
|
||||
downloadDialog.setSelectedVideoStream(selectedVideoStreamIndex);
|
||||
|
||||
downloadDialog.show(activity.getSupportFragmentManager(), "downloadDialog");
|
||||
} catch (Exception e) {
|
||||
Toast.makeText(activity,
|
||||
R.string.could_not_setup_download_menu,
|
||||
Toast.LENGTH_LONG).show();
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Stream Results
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
@@ -20,10 +20,10 @@ import org.schabi.newpipe.extractor.playlist.PlaylistInfoItem;
|
||||
import org.schabi.newpipe.extractor.stream.StreamInfoItem;
|
||||
import org.schabi.newpipe.fragments.BaseStateFragment;
|
||||
import org.schabi.newpipe.fragments.OnScrollBelowItemsListener;
|
||||
import org.schabi.newpipe.fragments.local.dialog.PlaylistAppendDialog;
|
||||
import org.schabi.newpipe.local.dialog.PlaylistAppendDialog;
|
||||
import org.schabi.newpipe.info_list.InfoItemDialog;
|
||||
import org.schabi.newpipe.info_list.InfoListAdapter;
|
||||
import org.schabi.newpipe.playlist.SinglePlayQueue;
|
||||
import org.schabi.newpipe.player.playqueue.SinglePlayQueue;
|
||||
import org.schabi.newpipe.util.NavigationHelper;
|
||||
import org.schabi.newpipe.util.OnClickGesture;
|
||||
import org.schabi.newpipe.util.StateSaver;
|
||||
@@ -140,9 +140,7 @@ public abstract class BaseListFragment<I, N> extends BaseStateFragment<I> implem
|
||||
infoListAdapter.setOnStreamSelectedListener(new OnClickGesture<StreamInfoItem>() {
|
||||
@Override
|
||||
public void selected(StreamInfoItem selectedItem) {
|
||||
onItemSelected(selectedItem);
|
||||
NavigationHelper.openVideoDetailFragment(useAsFrontPage ? getParentFragment().getFragmentManager() : getFragmentManager(),
|
||||
selectedItem.getServiceId(), selectedItem.getUrl(), selectedItem.getName());
|
||||
onStreamSelected(selectedItem);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -178,6 +176,12 @@ public abstract class BaseListFragment<I, N> extends BaseStateFragment<I> implem
|
||||
});
|
||||
}
|
||||
|
||||
private void onStreamSelected(StreamInfoItem selectedItem) {
|
||||
onItemSelected(selectedItem);
|
||||
NavigationHelper.openVideoDetailFragment(useAsFrontPage ? getParentFragment().getFragmentManager() : getFragmentManager(),
|
||||
selectedItem.getServiceId(), selectedItem.getUrl(), selectedItem.getName());
|
||||
}
|
||||
|
||||
protected void onScrollToBottom() {
|
||||
if (hasMoreItems() && !isLoading.get()) {
|
||||
loadMoreItems();
|
||||
@@ -216,6 +220,7 @@ public abstract class BaseListFragment<I, N> extends BaseStateFragment<I> implem
|
||||
|
||||
new InfoItemDialog(getActivity(), item, commands, actions).show();
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Menu
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
@@ -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.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.local.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;
|
||||
|
||||
@@ -161,6 +161,7 @@ public class ChannelFragment extends BaseListInfoFragment<ChannelInfo> {
|
||||
context.getResources().getString(R.string.start_here_on_main),
|
||||
context.getResources().getString(R.string.start_here_on_background),
|
||||
context.getResources().getString(R.string.start_here_on_popup),
|
||||
context.getResources().getString(R.string.append_playlist)
|
||||
};
|
||||
|
||||
final DialogInterface.OnClickListener actions = new DialogInterface.OnClickListener() {
|
||||
@@ -183,6 +184,12 @@ public class ChannelFragment extends BaseListInfoFragment<ChannelInfo> {
|
||||
case 4:
|
||||
NavigationHelper.playOnPopupPlayer(activity, getPlayQueue(index));
|
||||
break;
|
||||
case 5:
|
||||
if (getFragmentManager() != null) {
|
||||
PlaylistAppendDialog.fromStreamInfoItems(Collections.singletonList(item))
|
||||
.show(getFragmentManager(), TAG);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -77,8 +77,7 @@ public class KioskFragment extends BaseListInfoFragment<KioskInfo> {
|
||||
UrlIdHandler kioskTypeUrlIdHandler = service.getKioskList()
|
||||
.getUrlIdHandlerByType(kioskId);
|
||||
instance.setInitialData(serviceId,
|
||||
kioskTypeUrlIdHandler.getUrl(kioskId),
|
||||
kioskId);
|
||||
kioskTypeUrlIdHandler.getUrl(kioskId), kioskId);
|
||||
instance.kioskId = kioskId;
|
||||
return instance;
|
||||
}
|
||||
@@ -168,7 +167,7 @@ public class KioskFragment extends BaseListInfoFragment<KioskInfo> {
|
||||
|
||||
if (!result.getErrors().isEmpty()) {
|
||||
showSnackBarError(result.getErrors(),
|
||||
UserAction.REQUESTED_PLAYLIST,
|
||||
UserAction.REQUESTED_KIOSK,
|
||||
NewPipe.getNameOfService(result.getServiceId()), result.getUrl(), 0);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,14 +27,13 @@ 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.fragments.list.BaseListInfoFragment;
|
||||
import org.schabi.newpipe.fragments.local.RemotePlaylistManager;
|
||||
import org.schabi.newpipe.local.playlist.RemotePlaylistManager;
|
||||
import org.schabi.newpipe.info_list.InfoItemDialog;
|
||||
import org.schabi.newpipe.playlist.PlayQueue;
|
||||
import org.schabi.newpipe.playlist.PlaylistPlayQueue;
|
||||
import org.schabi.newpipe.playlist.SinglePlayQueue;
|
||||
import org.schabi.newpipe.player.playqueue.PlayQueue;
|
||||
import org.schabi.newpipe.player.playqueue.PlaylistPlayQueue;
|
||||
import org.schabi.newpipe.player.playqueue.SinglePlayQueue;
|
||||
import org.schabi.newpipe.report.UserAction;
|
||||
import org.schabi.newpipe.util.ExtractorHelper;
|
||||
import org.schabi.newpipe.util.ImageDisplayConstants;
|
||||
|
||||
@@ -41,7 +41,7 @@ import org.schabi.newpipe.extractor.search.SearchEngine;
|
||||
import org.schabi.newpipe.extractor.search.SearchResult;
|
||||
import org.schabi.newpipe.fragments.BackPressable;
|
||||
import org.schabi.newpipe.fragments.list.BaseListFragment;
|
||||
import org.schabi.newpipe.history.HistoryRecordManager;
|
||||
import org.schabi.newpipe.local.history.HistoryRecordManager;
|
||||
import org.schabi.newpipe.report.UserAction;
|
||||
import org.schabi.newpipe.util.Constants;
|
||||
import org.schabi.newpipe.util.AnimationUtils;
|
||||
@@ -544,7 +544,7 @@ public class SearchFragment
|
||||
howManyDeleted -> suggestionPublisher
|
||||
.onNext(searchEditText.getText().toString()),
|
||||
throwable -> showSnackBarError(throwable,
|
||||
UserAction.SOMETHING_ELSE, "none",
|
||||
UserAction.DELETE_FROM_HISTORY, "none",
|
||||
"Deleting item failed", R.string.general_error)
|
||||
);
|
||||
disposables.add(onDelete);
|
||||
|
||||
@@ -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();
|
||||
@@ -79,7 +78,9 @@ public final class BookmarkFragment
|
||||
@Override
|
||||
public void setUserVisibleHint(boolean isVisibleToUser) {
|
||||
super.setUserVisibleHint(isVisibleToUser);
|
||||
if (isVisibleToUser) setTitle(getString(R.string.tab_bookmarks));
|
||||
if (activity != null && isVisibleToUser) {
|
||||
setTitle(activity.getString(R.string.tab_bookmarks));
|
||||
}
|
||||
}
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////
|
||||
@@ -91,15 +92,6 @@ public final class BookmarkFragment
|
||||
super.initViews(rootView, savedInstanceState);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected View getListHeader() {
|
||||
final View headerRootLayout = activity.getLayoutInflater()
|
||||
.inflate(R.layout.bookmark_header, itemsList, false);
|
||||
lastPlayedButton = headerRootLayout.findViewById(R.id.lastPlayed);
|
||||
mostPlayedButton = headerRootLayout.findViewById(R.id.mostPlayed);
|
||||
return headerRootLayout;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void initListeners() {
|
||||
super.initListeners();
|
||||
@@ -133,18 +125,6 @@ public final class BookmarkFragment
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
lastPlayedButton.setOnClickListener(view -> {
|
||||
if (getParentFragment() != null) {
|
||||
NavigationHelper.openLastPlayedFragment(getParentFragment().getFragmentManager());
|
||||
}
|
||||
});
|
||||
|
||||
mostPlayedButton.setOnClickListener(view -> {
|
||||
if (getParentFragment() != null) {
|
||||
NavigationHelper.openMostPlayedFragment(getParentFragment().getFragmentManager());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////
|
||||
@@ -177,8 +157,6 @@ public final class BookmarkFragment
|
||||
@Override
|
||||
public void onDestroyView() {
|
||||
super.onDestroyView();
|
||||
if (mostPlayedButton != null) mostPlayedButton.setOnClickListener(null);
|
||||
if (lastPlayedButton != null) lastPlayedButton.setOnClickListener(null);
|
||||
|
||||
if (disposables != null) disposables.clear();
|
||||
if (databaseSubscription != null) databaseSubscription.cancel();
|
||||
@@ -1,4 +1,4 @@
|
||||
package org.schabi.newpipe.fragments.local.dialog;
|
||||
package org.schabi.newpipe.local.dialog;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.os.Bundle;
|
||||
@@ -18,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;
|
||||
@@ -1,4 +1,4 @@
|
||||
package org.schabi.newpipe.history;
|
||||
package org.schabi.newpipe.local.history;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.res.Resources;
|
||||
@@ -1,4 +1,4 @@
|
||||
package org.schabi.newpipe.history;
|
||||
package org.schabi.newpipe.local.history;
|
||||
|
||||
import android.support.annotation.Nullable;
|
||||
|
||||
@@ -1,4 +1,22 @@
|
||||
package org.schabi.newpipe.history;
|
||||
package org.schabi.newpipe.local.history;
|
||||
|
||||
/*
|
||||
* Copyright (C) Mauricio Colli 2018
|
||||
* HistoryRecordManager.java is part of NewPipe.
|
||||
*
|
||||
* NewPipe is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* NewPipe is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with NewPipe. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.SharedPreferences;
|
||||
@@ -27,6 +45,7 @@ import java.util.List;
|
||||
|
||||
import io.reactivex.Flowable;
|
||||
import io.reactivex.Maybe;
|
||||
import io.reactivex.Scheduler;
|
||||
import io.reactivex.Single;
|
||||
import io.reactivex.schedulers.Schedulers;
|
||||
|
||||
@@ -80,6 +99,11 @@ public class HistoryRecordManager {
|
||||
.subscribeOn(Schedulers.io());
|
||||
}
|
||||
|
||||
public Single<Integer> deleteWholeStreamHistory() {
|
||||
return Single.fromCallable(() -> streamHistoryTable.deleteAll())
|
||||
.subscribeOn(Schedulers.io());
|
||||
}
|
||||
|
||||
public Flowable<List<StreamHistoryEntry>> getStreamHistory() {
|
||||
return streamHistoryTable.getHistory().subscribeOn(Schedulers.io());
|
||||
}
|
||||
@@ -114,20 +138,6 @@ public class HistoryRecordManager {
|
||||
// Search History
|
||||
///////////////////////////////////////////////////////
|
||||
|
||||
public Single<List<Long>> insertSearches(final Collection<SearchHistoryEntry> entries) {
|
||||
return Single.fromCallable(() -> searchHistoryTable.insertAll(entries))
|
||||
.subscribeOn(Schedulers.io());
|
||||
}
|
||||
|
||||
public Single<Integer> deleteSearches(final Collection<SearchHistoryEntry> entries) {
|
||||
return Single.fromCallable(() -> searchHistoryTable.delete(entries))
|
||||
.subscribeOn(Schedulers.io());
|
||||
}
|
||||
|
||||
public Flowable<List<SearchHistoryEntry>> getSearchHistory() {
|
||||
return searchHistoryTable.getAll();
|
||||
}
|
||||
|
||||
public Maybe<Long> onSearched(final int serviceId, final String search) {
|
||||
if (!isSearchHistoryEnabled()) return Maybe.empty();
|
||||
|
||||
@@ -150,6 +160,11 @@ public class HistoryRecordManager {
|
||||
.subscribeOn(Schedulers.io());
|
||||
}
|
||||
|
||||
public Single<Integer> deleteWholeSearchHistory() {
|
||||
return Single.fromCallable(() -> searchHistoryTable.deleteAll())
|
||||
.subscribeOn(Schedulers.io());
|
||||
}
|
||||
|
||||
public Flowable<List<SearchHistoryEntry>> getRelatedSearches(final String query,
|
||||
final int similarQueryLimit,
|
||||
final int uniqueQueryLimit) {
|
||||
@@ -1,4 +1,4 @@
|
||||
package org.schabi.newpipe.fragments.local.bookmark;
|
||||
package org.schabi.newpipe.local.history;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.Context;
|
||||
@@ -7,9 +7,13 @@ import android.os.Bundle;
|
||||
import android.os.Parcelable;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.support.design.widget.Snackbar;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
|
||||
import org.reactivestreams.Subscriber;
|
||||
import org.reactivestreams.Subscription;
|
||||
@@ -17,13 +21,14 @@ import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.database.LocalItem;
|
||||
import org.schabi.newpipe.database.stream.StreamStatisticsEntry;
|
||||
import org.schabi.newpipe.extractor.stream.StreamInfoItem;
|
||||
import org.schabi.newpipe.history.HistoryRecordManager;
|
||||
import org.schabi.newpipe.local.BaseLocalListFragment;
|
||||
import org.schabi.newpipe.info_list.InfoItemDialog;
|
||||
import org.schabi.newpipe.playlist.PlayQueue;
|
||||
import org.schabi.newpipe.playlist.SinglePlayQueue;
|
||||
import org.schabi.newpipe.player.playqueue.PlayQueue;
|
||||
import org.schabi.newpipe.player.playqueue.SinglePlayQueue;
|
||||
import org.schabi.newpipe.report.UserAction;
|
||||
import org.schabi.newpipe.util.NavigationHelper;
|
||||
import org.schabi.newpipe.util.OnClickGesture;
|
||||
import org.schabi.newpipe.util.ThemeHelper;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
@@ -31,13 +36,19 @@ import java.util.List;
|
||||
|
||||
import icepick.State;
|
||||
import io.reactivex.android.schedulers.AndroidSchedulers;
|
||||
import io.reactivex.disposables.CompositeDisposable;
|
||||
import io.reactivex.disposables.Disposable;
|
||||
|
||||
public abstract class StatisticsPlaylistFragment
|
||||
public class StatisticsPlaylistFragment
|
||||
extends BaseLocalListFragment<List<StreamStatisticsEntry>, Void> {
|
||||
|
||||
private View headerPlayAllButton;
|
||||
private View headerPopupButton;
|
||||
private View headerBackgroundButton;
|
||||
private View playlistCtrl;
|
||||
private View sortButton;
|
||||
private ImageView sortButtonIcon;
|
||||
private TextView sortButtonText;
|
||||
|
||||
@State
|
||||
protected Parcelable itemsListState;
|
||||
@@ -45,14 +56,28 @@ public abstract class StatisticsPlaylistFragment
|
||||
/* Used for independent events */
|
||||
private Subscription databaseSubscription;
|
||||
private HistoryRecordManager recordManager;
|
||||
private CompositeDisposable disposables = new CompositeDisposable();
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////
|
||||
// Abstracts
|
||||
///////////////////////////////////////////////////////////////////////////
|
||||
private enum StatisticSortMode {
|
||||
LAST_PLAYED,
|
||||
MOST_PLAYED,
|
||||
}
|
||||
|
||||
protected abstract String getName();
|
||||
StatisticSortMode sortMode = StatisticSortMode.LAST_PLAYED;
|
||||
|
||||
protected abstract List<StreamStatisticsEntry> processResult(final List<StreamStatisticsEntry> results);
|
||||
protected List<StreamStatisticsEntry> processResult(final List<StreamStatisticsEntry> results) {
|
||||
switch (sortMode) {
|
||||
case LAST_PLAYED:
|
||||
Collections.sort(results, (left, right) ->
|
||||
right.latestAccessDate.compareTo(left.latestAccessDate));
|
||||
return results;
|
||||
case MOST_PLAYED:
|
||||
Collections.sort(results, (left, right) ->
|
||||
((Long) right.watchCount).compareTo(left.watchCount));
|
||||
return results;
|
||||
default: return null;
|
||||
}
|
||||
}
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////
|
||||
// Fragment LifeCycle - Creation
|
||||
@@ -78,16 +103,20 @@ public abstract class StatisticsPlaylistFragment
|
||||
@Override
|
||||
protected void initViews(View rootView, Bundle savedInstanceState) {
|
||||
super.initViews(rootView, savedInstanceState);
|
||||
setTitle(getName());
|
||||
setTitle(getString(R.string.title_last_played));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected View getListHeader() {
|
||||
final View headerRootLayout = activity.getLayoutInflater().inflate(R.layout.playlist_control,
|
||||
final View headerRootLayout = activity.getLayoutInflater().inflate(R.layout.statistic_playlist_control,
|
||||
itemsList, false);
|
||||
playlistCtrl = headerRootLayout.findViewById(R.id.playlist_control);
|
||||
headerPlayAllButton = headerRootLayout.findViewById(R.id.playlist_ctrl_play_all_button);
|
||||
headerPopupButton = headerRootLayout.findViewById(R.id.playlist_ctrl_play_popup_button);
|
||||
headerBackgroundButton = headerRootLayout.findViewById(R.id.playlist_ctrl_play_bg_button);
|
||||
sortButton = headerRootLayout.findViewById(R.id.sortButton);
|
||||
sortButtonIcon = headerRootLayout.findViewById(R.id.sortButtonIcon);
|
||||
sortButtonText = headerRootLayout.findViewById(R.id.sortButtonText);
|
||||
return headerRootLayout;
|
||||
}
|
||||
|
||||
@@ -193,6 +222,8 @@ public abstract class StatisticsPlaylistFragment
|
||||
super.handleResult(result);
|
||||
if (itemListAdapter == null) return;
|
||||
|
||||
playlistCtrl.setVisibility(View.VISIBLE);
|
||||
|
||||
itemListAdapter.clearStreamItemList();
|
||||
|
||||
if (result.isEmpty()) {
|
||||
@@ -212,6 +243,7 @@ public abstract class StatisticsPlaylistFragment
|
||||
NavigationHelper.playOnPopupPlayer(activity, getPlayQueue()));
|
||||
headerBackgroundButton.setOnClickListener(view ->
|
||||
NavigationHelper.playOnBackgroundPlayer(activity, getPlayQueue()));
|
||||
sortButton.setOnClickListener(view -> toggleSortMode());
|
||||
|
||||
hideLoading();
|
||||
}
|
||||
@@ -238,6 +270,21 @@ public abstract class StatisticsPlaylistFragment
|
||||
// Utils
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
private void toggleSortMode() {
|
||||
if(sortMode == StatisticSortMode.LAST_PLAYED) {
|
||||
sortMode = StatisticSortMode.MOST_PLAYED;
|
||||
setTitle(getString(R.string.title_most_played));
|
||||
sortButtonIcon.setImageResource(ThemeHelper.getIconByAttr(R.attr.history, getContext()));
|
||||
sortButtonText.setText(R.string.title_last_played);
|
||||
} else {
|
||||
sortMode = StatisticSortMode.LAST_PLAYED;
|
||||
setTitle(getString(R.string.title_last_played));
|
||||
sortButtonIcon.setImageResource(ThemeHelper.getIconByAttr(R.attr.filter, getContext()));
|
||||
sortButtonText.setText(R.string.title_most_played);
|
||||
}
|
||||
startLoading(true);
|
||||
}
|
||||
|
||||
private void showStreamDialog(final StreamStatisticsEntry item) {
|
||||
final Context context = getContext();
|
||||
final Activity activity = getActivity();
|
||||
@@ -250,6 +297,7 @@ public abstract class StatisticsPlaylistFragment
|
||||
context.getResources().getString(R.string.start_here_on_main),
|
||||
context.getResources().getString(R.string.start_here_on_background),
|
||||
context.getResources().getString(R.string.start_here_on_popup),
|
||||
context.getResources().getString(R.string.delete),
|
||||
};
|
||||
|
||||
final DialogInterface.OnClickListener actions = (dialogInterface, i) -> {
|
||||
@@ -270,6 +318,9 @@ public abstract class StatisticsPlaylistFragment
|
||||
case 4:
|
||||
NavigationHelper.playOnPopupPlayer(activity, getPlayQueue(index));
|
||||
break;
|
||||
case 5:
|
||||
deleteEntry(index);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
@@ -278,6 +329,32 @@ public abstract class StatisticsPlaylistFragment
|
||||
new InfoItemDialog(getActivity(), infoItem, commands, actions).show();
|
||||
}
|
||||
|
||||
private void deleteEntry(final int index) {
|
||||
final LocalItem infoItem = itemListAdapter.getItemsList()
|
||||
.get(index);
|
||||
if(infoItem instanceof StreamStatisticsEntry) {
|
||||
final StreamStatisticsEntry entry = (StreamStatisticsEntry) infoItem;
|
||||
final Disposable onDelete = recordManager.deleteStreamHistory(entry.streamId)
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(
|
||||
howManyDelted -> {
|
||||
if(getView() != null) {
|
||||
Snackbar.make(getView(), R.string.one_item_deleted,
|
||||
Snackbar.LENGTH_SHORT).show();
|
||||
} else {
|
||||
Toast.makeText(getContext(),
|
||||
R.string.one_item_deleted,
|
||||
Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
},
|
||||
throwable -> showSnackBarError(throwable,
|
||||
UserAction.DELETE_FROM_HISTORY, "none",
|
||||
"Deleting item failed", R.string.general_error));
|
||||
|
||||
disposables.add(onDelete);
|
||||
}
|
||||
}
|
||||
|
||||
private PlayQueue getPlayQueue() {
|
||||
return getPlayQueue(0);
|
||||
}
|
||||
@@ -1,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;
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
@@ -39,9 +39,8 @@ import org.schabi.newpipe.extractor.subscription.SubscriptionExtractor;
|
||||
import org.schabi.newpipe.fragments.BaseStateFragment;
|
||||
import org.schabi.newpipe.info_list.InfoListAdapter;
|
||||
import org.schabi.newpipe.report.UserAction;
|
||||
import org.schabi.newpipe.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 +52,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 +63,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;
|
||||
|
||||
@@ -104,8 +102,8 @@ public class SubscriptionFragment extends BaseStateFragment<List<SubscriptionEnt
|
||||
@Override
|
||||
public void setUserVisibleHint(boolean isVisibleToUser) {
|
||||
super.setUserVisibleHint(isVisibleToUser);
|
||||
if (isVisibleToUser) {
|
||||
setTitle(getString(R.string.tab_subscriptions));
|
||||
if (activity != null && isVisibleToUser) {
|
||||
setTitle(activity.getString(R.string.tab_subscriptions));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
@@ -48,7 +48,7 @@ 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.player.playqueue.PlayQueueItem;
|
||||
import org.schabi.newpipe.util.ListHelper;
|
||||
import org.schabi.newpipe.util.NavigationHelper;
|
||||
import org.schabi.newpipe.util.ThemeHelper;
|
||||
@@ -118,8 +118,12 @@ public final class BackgroundPlayer extends Service {
|
||||
|
||||
@Override
|
||||
public int onStartCommand(Intent intent, int flags, int startId) {
|
||||
if (DEBUG) Log.d(TAG, "onStartCommand() called with: intent = [" + intent + "], flags = [" + flags + "], startId = [" + startId + "]");
|
||||
if (DEBUG) Log.d(TAG, "onStartCommand() called with: intent = [" + intent +
|
||||
"], flags = [" + flags + "], startId = [" + startId + "]");
|
||||
basePlayerImpl.handleIntent(intent);
|
||||
if (basePlayerImpl.mediaSessionManager != null) {
|
||||
basePlayerImpl.mediaSessionManager.handleMediaButtonIntent(intent);
|
||||
}
|
||||
return START_NOT_STICKY;
|
||||
}
|
||||
|
||||
@@ -160,6 +164,11 @@ public final class BackgroundPlayer extends Service {
|
||||
if (DEBUG) Log.d(TAG, "onScreenOnOff() called with: on = [" + on + "]");
|
||||
shouldUpdateOnProgress = on;
|
||||
basePlayerImpl.triggerProgressUpdate();
|
||||
if (on) {
|
||||
basePlayerImpl.startProgressLoop();
|
||||
} else {
|
||||
basePlayerImpl.stopProgressLoop();
|
||||
}
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
@@ -545,7 +554,6 @@ public final class BackgroundPlayer extends Service {
|
||||
super.onPaused();
|
||||
|
||||
updateNotification(R.drawable.ic_play_arrow_white);
|
||||
if (isProgressLoopRunning()) stopProgressLoop();
|
||||
|
||||
lockManager.releaseWifiAndCpu();
|
||||
}
|
||||
|
||||
@@ -46,7 +46,7 @@ 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;
|
||||
@@ -58,19 +58,20 @@ 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.util.SerializedCache;
|
||||
|
||||
import java.io.IOException;
|
||||
@@ -124,7 +125,6 @@ public abstract class BasePlayer implements
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
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;
|
||||
@@ -140,10 +140,10 @@ public abstract class BasePlayer implements
|
||||
// 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 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 CustomTrackSelector trackSelector;
|
||||
protected PlayerDataSource dataSource;
|
||||
@@ -177,11 +177,11 @@ public abstract class BasePlayer implements
|
||||
}
|
||||
|
||||
public void setup() {
|
||||
if (simpleExoPlayer == null) initPlayer();
|
||||
if (simpleExoPlayer == null) initPlayer(/*playOnInit=*/true);
|
||||
initListeners();
|
||||
}
|
||||
|
||||
public void initPlayer() {
|
||||
public void initPlayer(final boolean playOnReady) {
|
||||
if (DEBUG) Log.d(TAG, "initPlayer() called with: context = [" + context + "]");
|
||||
|
||||
if (databaseUpdateReactor != null) databaseUpdateReactor.dispose();
|
||||
@@ -191,15 +191,15 @@ public abstract class BasePlayer implements
|
||||
final DefaultBandwidthMeter bandwidthMeter = new DefaultBandwidthMeter();
|
||||
dataSource = new PlayerDataSource(context, userAgent, bandwidthMeter);
|
||||
|
||||
final AdaptiveTrackSelection.Factory trackSelectionFactory =
|
||||
new AdaptiveTrackSelection.Factory(bandwidthMeter);
|
||||
final TrackSelection.Factory trackSelectionFactory =
|
||||
PlayerHelper.getQualitySelector(context, bandwidthMeter);
|
||||
trackSelector = new CustomTrackSelector(trackSelectionFactory);
|
||||
|
||||
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);
|
||||
@@ -237,15 +237,16 @@ public abstract class BasePlayer implements
|
||||
final float playbackPitch = intent.getFloatExtra(PLAYBACK_PITCH, getPlaybackPitch());
|
||||
|
||||
// Good to go...
|
||||
initPlayback(queue, repeatMode, playbackSpeed, playbackPitch);
|
||||
initPlayback(queue, repeatMode, playbackSpeed, playbackPitch, /*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 playOnReady) {
|
||||
destroyPlayer();
|
||||
initPlayer();
|
||||
initPlayer(playOnReady);
|
||||
setRepeatMode(repeatMode);
|
||||
setPlaybackParameters(playbackSpeed, playbackPitch);
|
||||
|
||||
@@ -518,15 +519,16 @@ public abstract class BasePlayer implements
|
||||
}
|
||||
|
||||
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());
|
||||
}
|
||||
@@ -553,8 +555,8 @@ public abstract class BasePlayer implements
|
||||
// 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);
|
||||
"clamping to default position.");
|
||||
seekToDefault();
|
||||
}
|
||||
break;
|
||||
}
|
||||
@@ -640,12 +642,12 @@ public abstract class BasePlayer implements
|
||||
seekTo(recoveryPositionMillis);
|
||||
playQueue.unsetRecovery(currentSourceIndex);
|
||||
|
||||
} else if (isSynchronizing && simpleExoPlayer.isCurrentWindowDynamic()) {
|
||||
} else if (isSynchronizing && isLive()) {
|
||||
if (DEBUG) Log.d(TAG, "Playback - Synchronizing livestream to default time");
|
||||
// Is still synchronizing?
|
||||
seekToDefault();
|
||||
|
||||
} else if (isSynchronizing && presetStartPositionMillis != 0L) {
|
||||
} else if (isSynchronizing && presetStartPositionMillis > 0L) {
|
||||
if (DEBUG) Log.d(TAG, "Playback - Seeking to preset start " +
|
||||
"position=[" + presetStartPositionMillis + "]");
|
||||
// Has another start position?
|
||||
@@ -700,41 +702,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) {
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -787,9 +771,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();
|
||||
@@ -985,22 +970,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);
|
||||
@@ -1050,7 +1035,9 @@ public abstract class BasePlayer implements
|
||||
}
|
||||
|
||||
public void seekToDefault() {
|
||||
if (simpleExoPlayer != null) simpleExoPlayer.seekToDefaultPosition();
|
||||
if (simpleExoPlayer != null) {
|
||||
simpleExoPlayer.seekToDefaultPosition();
|
||||
}
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
@@ -1091,9 +1078,9 @@ public abstract class BasePlayer implements
|
||||
private void savePlaybackState() {
|
||||
if (simpleExoPlayer == null || currentInfo == null) return;
|
||||
|
||||
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());
|
||||
}
|
||||
}
|
||||
@@ -1127,9 +1114,7 @@ public abstract class BasePlayer implements
|
||||
|
||||
/** Checks if the current playback is a livestream AND is playing at or beyond the live edge */
|
||||
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 +1128,16 @@ 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 =(
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public boolean isPlaying() {
|
||||
final int state = simpleExoPlayer.getPlaybackState();
|
||||
return (state == Player.STATE_READY || state == Player.STATE_BUFFERING)
|
||||
@@ -1170,10 +1165,6 @@ public abstract class BasePlayer implements
|
||||
setPlaybackParameters(speed, getPlaybackPitch());
|
||||
}
|
||||
|
||||
public void setPlaybackPitch(float pitch) {
|
||||
setPlaybackParameters(getPlaybackSpeed(), pitch);
|
||||
}
|
||||
|
||||
public PlaybackParameters getPlaybackParameters() {
|
||||
final PlaybackParameters defaultParameters = new PlaybackParameters(1f, 1f);
|
||||
if (simpleExoPlayer == null) return defaultParameters;
|
||||
|
||||
@@ -25,13 +25,16 @@ import android.content.SharedPreferences;
|
||||
import android.content.pm.ActivityInfo;
|
||||
import android.content.res.Configuration;
|
||||
import android.graphics.Color;
|
||||
import android.graphics.drawable.ColorDrawable;
|
||||
import android.media.AudioManager;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.preference.PreferenceManager;
|
||||
import android.provider.Settings;
|
||||
import android.support.annotation.ColorInt;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.support.v4.app.ActivityCompat;
|
||||
import android.support.v7.app.AppCompatActivity;
|
||||
import android.support.v7.widget.RecyclerView;
|
||||
import android.support.v7.widget.helper.ItemTouchHelper;
|
||||
@@ -50,6 +53,7 @@ import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
|
||||
import com.google.android.exoplayer2.Player;
|
||||
import com.google.android.exoplayer2.text.CaptionStyleCompat;
|
||||
import com.google.android.exoplayer2.ui.AspectRatioFrameLayout;
|
||||
import com.google.android.exoplayer2.ui.SubtitleView;
|
||||
|
||||
@@ -59,11 +63,10 @@ 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.util.AnimationUtils;
|
||||
import org.schabi.newpipe.util.ListHelper;
|
||||
import org.schabi.newpipe.util.NavigationHelper;
|
||||
@@ -95,12 +98,12 @@ 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;
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Activity LifeCycle
|
||||
@@ -112,6 +115,7 @@ 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);
|
||||
|
||||
@@ -135,8 +139,9 @@ 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
|
||||
@@ -148,26 +153,28 @@ public final class MainVideoPlayer extends AppCompatActivity
|
||||
|
||||
@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.wasPlaying());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -180,33 +187,24 @@ 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;
|
||||
}
|
||||
|
||||
@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.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();
|
||||
playerImpl.destroy();
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
@@ -221,48 +219,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 +244,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,6 +319,10 @@ public final class MainVideoPlayer extends AppCompatActivity
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isInMultiWindow() {
|
||||
return Build.VERSION.SDK_INT >= Build.VERSION_CODES.N && isInMultiWindowMode();
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////
|
||||
// Playback Parameters Listener
|
||||
////////////////////////////////////////////////////////////////////////////
|
||||
@@ -411,15 +392,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,20 +400,15 @@ 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
|
||||
@@ -727,7 +694,7 @@ public final class MainVideoPlayer extends AppCompatActivity
|
||||
animatePlayButtons(true, 200);
|
||||
});
|
||||
|
||||
changeSystemUi();
|
||||
showSystemUi();
|
||||
getRootView().setKeepScreenOn(false);
|
||||
}
|
||||
|
||||
@@ -882,10 +849,12 @@ public final class MainVideoPlayer extends AppCompatActivity
|
||||
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 +869,7 @@ public final class MainVideoPlayer extends AppCompatActivity
|
||||
playerImpl.hideControls(150, 0);
|
||||
} else {
|
||||
playerImpl.showControlsThenHide();
|
||||
changeSystemUi();
|
||||
showSystemUi();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
68
app/src/main/java/org/schabi/newpipe/player/PlayerState.java
Normal file
68
app/src/main/java/org/schabi/newpipe/player/PlayerState.java
Normal file
@@ -0,0 +1,68 @@
|
||||
package org.schabi.newpipe.player;
|
||||
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.annotation.Nullable;
|
||||
|
||||
import org.schabi.newpipe.player.playqueue.PlayQueue;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
public class PlayerState implements Serializable {
|
||||
|
||||
@NonNull private final PlayQueue playQueue;
|
||||
private final int repeatMode;
|
||||
private final float playbackSpeed;
|
||||
private final float playbackPitch;
|
||||
@Nullable private final String playbackQuality;
|
||||
private final boolean wasPlaying;
|
||||
|
||||
PlayerState(@NonNull final PlayQueue playQueue, final int repeatMode,
|
||||
final float playbackSpeed, final float playbackPitch, final boolean wasPlaying) {
|
||||
this(playQueue, repeatMode, playbackSpeed, playbackPitch, null, wasPlaying);
|
||||
}
|
||||
|
||||
PlayerState(@NonNull final PlayQueue playQueue, final int repeatMode,
|
||||
final float playbackSpeed, final float playbackPitch,
|
||||
@Nullable final String playbackQuality, final boolean wasPlaying) {
|
||||
this.playQueue = playQueue;
|
||||
this.repeatMode = repeatMode;
|
||||
this.playbackSpeed = playbackSpeed;
|
||||
this.playbackPitch = playbackPitch;
|
||||
this.playbackQuality = playbackQuality;
|
||||
this.wasPlaying = wasPlaying;
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Serdes
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Getters
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
@NonNull
|
||||
public PlayQueue getPlayQueue() {
|
||||
return playQueue;
|
||||
}
|
||||
|
||||
public int getRepeatMode() {
|
||||
return repeatMode;
|
||||
}
|
||||
|
||||
public float getPlaybackSpeed() {
|
||||
return playbackSpeed;
|
||||
}
|
||||
|
||||
public float getPlaybackPitch() {
|
||||
return playbackPitch;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public String getPlaybackQuality() {
|
||||
return playbackQuality;
|
||||
}
|
||||
|
||||
public boolean wasPlaying() {
|
||||
return wasPlaying;
|
||||
}
|
||||
}
|
||||
@@ -44,6 +44,7 @@ import android.view.MotionEvent;
|
||||
import android.view.View;
|
||||
import android.view.WindowManager;
|
||||
import android.widget.ImageButton;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.PopupMenu;
|
||||
import android.widget.RemoteViews;
|
||||
import android.widget.SeekBar;
|
||||
@@ -52,6 +53,7 @@ import android.widget.TextView;
|
||||
import com.google.android.exoplayer2.C;
|
||||
import com.google.android.exoplayer2.PlaybackParameters;
|
||||
import com.google.android.exoplayer2.Player;
|
||||
import com.google.android.exoplayer2.text.CaptionStyleCompat;
|
||||
import com.google.android.exoplayer2.ui.AspectRatioFrameLayout;
|
||||
import com.google.android.exoplayer2.ui.SubtitleView;
|
||||
|
||||
@@ -63,7 +65,7 @@ import org.schabi.newpipe.player.event.PlayerEventListener;
|
||||
import org.schabi.newpipe.player.helper.LockManager;
|
||||
import org.schabi.newpipe.player.helper.PlayerHelper;
|
||||
import org.schabi.newpipe.player.old.PlayVideoActivity;
|
||||
import org.schabi.newpipe.playlist.PlayQueueItem;
|
||||
import org.schabi.newpipe.player.playqueue.PlayQueueItem;
|
||||
import org.schabi.newpipe.util.ListHelper;
|
||||
import org.schabi.newpipe.util.NavigationHelper;
|
||||
import org.schabi.newpipe.util.ThemeHelper;
|
||||
@@ -369,6 +371,7 @@ public final class PopupVideoPlayer extends Service {
|
||||
protected class VideoPlayerImpl extends VideoPlayer implements View.OnLayoutChangeListener {
|
||||
private TextView resizingIndicator;
|
||||
private ImageButton fullScreenButton;
|
||||
private ImageView videoPlayPause;
|
||||
|
||||
private View extraOptionsView;
|
||||
|
||||
@@ -390,6 +393,8 @@ public final class PopupVideoPlayer extends Service {
|
||||
resizingIndicator = rootView.findViewById(R.id.resizing_indicator);
|
||||
fullScreenButton = rootView.findViewById(R.id.fullScreenButton);
|
||||
fullScreenButton.setOnClickListener(v -> onFullScreenButtonClicked());
|
||||
videoPlayPause = rootView.findViewById(R.id.videoPlayPause);
|
||||
videoPlayPause.setOnClickListener(this::onPlayPauseButtonPressed);
|
||||
|
||||
extraOptionsView = rootView.findViewById(R.id.extraOptionsView);
|
||||
rootView.addOnLayoutChangeListener(this);
|
||||
@@ -397,14 +402,16 @@ public final class PopupVideoPlayer extends Service {
|
||||
|
||||
@Override
|
||||
protected void setupSubtitleView(@NonNull SubtitleView view,
|
||||
@NonNull String captionSizeKey) {
|
||||
float captionRatio = SubtitleView.DEFAULT_TEXT_SIZE_FRACTION;
|
||||
if (captionSizeKey.equals(getString(R.string.smaller_caption_size_key))) {
|
||||
captionRatio *= 0.9;
|
||||
} else if (captionSizeKey.equals(getString(R.string.larger_caption_size_key))) {
|
||||
captionRatio *= 1.1;
|
||||
}
|
||||
view.setFractionalTextSize(captionRatio);
|
||||
final float captionScale,
|
||||
@NonNull final CaptionStyleCompat captionStyle) {
|
||||
float captionRatio = (captionScale - 1f) / 5f + 1f;
|
||||
view.setFractionalTextSize(SubtitleView.DEFAULT_TEXT_SIZE_FRACTION * captionRatio);
|
||||
view.setApplyEmbeddedStyles(captionStyle.equals(CaptionStyleCompat.DEFAULT));
|
||||
view.setStyle(captionStyle);
|
||||
}
|
||||
|
||||
private void onPlayPauseButtonPressed(View ib) {
|
||||
onPlayPause();
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -652,6 +659,7 @@ public final class PopupVideoPlayer extends Service {
|
||||
public void onPlaying() {
|
||||
super.onPlaying();
|
||||
updateNotification(R.drawable.ic_pause_white);
|
||||
videoPlayPause.setBackgroundResource(R.drawable.ic_pause_white);
|
||||
lockManager.acquireWifiAndCpu();
|
||||
|
||||
hideControls(DEFAULT_CONTROLS_DURATION, DEFAULT_CONTROLS_HIDE_TIME);
|
||||
@@ -667,13 +675,14 @@ public final class PopupVideoPlayer extends Service {
|
||||
public void onPaused() {
|
||||
super.onPaused();
|
||||
updateNotification(R.drawable.ic_play_arrow_white);
|
||||
showAndAnimateControl(R.drawable.ic_play_arrow_white, false);
|
||||
videoPlayPause.setBackgroundResource(R.drawable.ic_play_arrow_white);
|
||||
lockManager.releaseWifiAndCpu();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPausedSeek() {
|
||||
super.onPausedSeek();
|
||||
videoPlayPause.setBackgroundResource(R.drawable.ic_pause_white);
|
||||
updateNotification(R.drawable.ic_play_arrow_white);
|
||||
}
|
||||
|
||||
@@ -681,10 +690,27 @@ public final class PopupVideoPlayer extends Service {
|
||||
public void onCompleted() {
|
||||
super.onCompleted();
|
||||
updateNotification(R.drawable.ic_replay_white);
|
||||
showAndAnimateControl(R.drawable.ic_replay_white, false);
|
||||
videoPlayPause.setBackgroundResource(R.drawable.ic_replay_white);
|
||||
lockManager.releaseWifiAndCpu();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void showControlsThenHide() {
|
||||
videoPlayPause.setVisibility(View.VISIBLE);
|
||||
super.showControlsThenHide();
|
||||
}
|
||||
|
||||
public void showControls(long duration) {
|
||||
videoPlayPause.setVisibility(View.VISIBLE);
|
||||
super.showControls(duration);
|
||||
}
|
||||
|
||||
public void hideControls(final long duration, long delay) {
|
||||
super.hideControlsAndButton(duration, delay, videoPlayPause);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Utils
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
@@ -718,6 +744,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,7 +759,12 @@ public final class PopupVideoPlayer extends Service {
|
||||
public boolean onSingleTapConfirmed(MotionEvent e) {
|
||||
if (DEBUG) Log.d(TAG, "onSingleTapConfirmed() called with: e = [" + e + "]");
|
||||
if (playerImpl == null || playerImpl.getPlayer() == null) return false;
|
||||
playerImpl.onPlayPause();
|
||||
if (playerImpl.isControlsVisible()) {
|
||||
playerImpl.hideControls(100, 100);
|
||||
} else {
|
||||
playerImpl.showControlsThenHide();
|
||||
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@@ -29,17 +29,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 +155,7 @@ public abstract class ServicePlayerActivity extends AppCompatActivity
|
||||
finish();
|
||||
return true;
|
||||
case R.id.action_append_playlist:
|
||||
appendToPlaylist();
|
||||
appendAllToPlaylist();
|
||||
return true;
|
||||
case R.id.action_settings:
|
||||
NavigationHelper.openSettings(this);
|
||||
@@ -187,13 +191,6 @@ public abstract class ServicePlayerActivity extends AppCompatActivity
|
||||
).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||
}
|
||||
|
||||
private void appendToPlaylist() {
|
||||
if (this.player != null && this.player.getPlayQueue() != null) {
|
||||
PlaylistAppendDialog.fromPlayQueueItems(this.player.getPlayQueue().getStreams())
|
||||
.show(getSupportFragmentManager(), getTag());
|
||||
}
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////
|
||||
// Service Connection
|
||||
////////////////////////////////////////////////////////////////////////////
|
||||
@@ -319,7 +316,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 +326,20 @@ public abstract class ServicePlayerActivity extends AppCompatActivity
|
||||
return true;
|
||||
});
|
||||
|
||||
final MenuItem detail = menu.getMenu().add(RECYCLER_ITEM_POPUP_MENU_GROUP_ID, 1, Menu.NONE, R.string.play_queue_stream_detail);
|
||||
final MenuItem detail = menu.getMenu().add(RECYCLER_ITEM_POPUP_MENU_GROUP_ID, /*pos=*/1,
|
||||
Menu.NONE, R.string.play_queue_stream_detail);
|
||||
detail.setOnMenuItemClickListener(menuItem -> {
|
||||
onOpenDetail(item.getServiceId(), item.getUrl(), item.getTitle());
|
||||
return true;
|
||||
});
|
||||
|
||||
final MenuItem append = menu.getMenu().add(RECYCLER_ITEM_POPUP_MENU_GROUP_ID, /*pos=*/2,
|
||||
Menu.NONE, R.string.append_playlist);
|
||||
append.setOnMenuItemClickListener(menuItem -> {
|
||||
openPlaylistAppendDialog(Collections.singletonList(item));
|
||||
return true;
|
||||
});
|
||||
|
||||
menu.show();
|
||||
}
|
||||
|
||||
@@ -488,6 +494,21 @@ 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());
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////
|
||||
// Binding Service Listener
|
||||
////////////////////////////////////////////////////////////////////////////
|
||||
@@ -497,6 +518,7 @@ public abstract class ServicePlayerActivity extends AppCompatActivity
|
||||
onStateChanged(state);
|
||||
onPlayModeChanged(repeatMode, shuffled);
|
||||
onPlaybackParameterChanged(parameters);
|
||||
onMaybePlaybackAdapterChanged();
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -609,4 +631,12 @@ public abstract class ServicePlayerActivity extends AppCompatActivity
|
||||
playbackPitchButton.setText(formatPitch(parameters.pitch));
|
||||
}
|
||||
}
|
||||
|
||||
private void onMaybePlaybackAdapterChanged() {
|
||||
if (itemsList == null || player == null) return;
|
||||
final PlayQueueAdapter maybeNewAdapter = player.getPlayQueueAdapter();
|
||||
if (maybeNewAdapter != null && itemsList.getAdapter() != maybeNewAdapter) {
|
||||
itemsList.setAdapter(maybeNewAdapter);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,7 +32,6 @@ import android.graphics.PorterDuff;
|
||||
import android.net.Uri;
|
||||
import android.os.Build;
|
||||
import android.os.Handler;
|
||||
import android.preference.PreferenceManager;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.support.v4.content.ContextCompat;
|
||||
@@ -55,6 +54,7 @@ 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;
|
||||
@@ -68,7 +68,7 @@ import org.schabi.newpipe.extractor.stream.StreamInfo;
|
||||
import org.schabi.newpipe.extractor.stream.StreamType;
|
||||
import org.schabi.newpipe.extractor.stream.VideoStream;
|
||||
import org.schabi.newpipe.player.helper.PlayerHelper;
|
||||
import org.schabi.newpipe.playlist.PlayQueueItem;
|
||||
import org.schabi.newpipe.player.playqueue.PlayQueueItem;
|
||||
import org.schabi.newpipe.util.AnimationUtils;
|
||||
import org.schabi.newpipe.util.ListHelper;
|
||||
|
||||
@@ -189,10 +189,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 +214,8 @@ public abstract class VideoPlayer extends BasePlayer
|
||||
}
|
||||
|
||||
protected abstract void setupSubtitleView(@NonNull SubtitleView view,
|
||||
@NonNull String captionSizeKey);
|
||||
final float captionScale,
|
||||
@NonNull final CaptionStyleCompat captionStyle);
|
||||
|
||||
@Override
|
||||
public void initListeners() {
|
||||
@@ -228,8 +229,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);
|
||||
@@ -886,6 +887,19 @@ 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
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
@@ -1,8 +1,12 @@
|
||||
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;
|
||||
@@ -15,8 +19,8 @@ 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,
|
||||
@@ -28,11 +32,9 @@ public class MediaSessionManager {
|
||||
this.sessionConnector.setPlayer(player, new DummyPlaybackPreparer());
|
||||
}
|
||||
|
||||
public MediaSessionCompat getMediaSession() {
|
||||
return mediaSession;
|
||||
}
|
||||
|
||||
public MediaSessionConnector getSessionConnector() {
|
||||
return sessionConnector;
|
||||
@Nullable
|
||||
@SuppressWarnings("UnusedReturnValue")
|
||||
public KeyEvent handleMediaButtonIntent(final Intent intent) {
|
||||
return MediaButtonReceiver.handleIntent(mediaSession, intent);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,12 +2,18 @@ package org.schabi.newpipe.player.helper;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.SharedPreferences;
|
||||
import android.os.Build;
|
||||
import android.preference.PreferenceManager;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.view.accessibility.CaptioningManager;
|
||||
|
||||
import com.google.android.exoplayer2.SeekParameters;
|
||||
import com.google.android.exoplayer2.trackselection.AdaptiveTrackSelection;
|
||||
import com.google.android.exoplayer2.trackselection.TrackSelection;
|
||||
import com.google.android.exoplayer2.text.CaptionStyleCompat;
|
||||
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,9 +24,9 @@ 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.text.DecimalFormat;
|
||||
import java.text.NumberFormat;
|
||||
@@ -203,6 +209,16 @@ public class PlayerHelper {
|
||||
return 60000;
|
||||
}
|
||||
|
||||
public static TrackSelection.Factory getQualitySelector(@NonNull final Context context,
|
||||
@NonNull final BandwidthMeter meter) {
|
||||
return new AdaptiveTrackSelection.Factory(meter,
|
||||
AdaptiveTrackSelection.DEFAULT_MAX_INITIAL_BITRATE,
|
||||
/*bufferDurationRequiredForQualityIncrease=*/1000,
|
||||
AdaptiveTrackSelection.DEFAULT_MAX_DURATION_FOR_QUALITY_DECREASE_MS,
|
||||
AdaptiveTrackSelection.DEFAULT_MIN_DURATION_TO_RETAIN_AFTER_DISCARD_MS,
|
||||
AdaptiveTrackSelection.DEFAULT_BANDWIDTH_FRACTION);
|
||||
}
|
||||
|
||||
public static boolean isUsingDSP(@NonNull final Context context) {
|
||||
return true;
|
||||
}
|
||||
@@ -215,6 +231,35 @@ public class PlayerHelper {
|
||||
return 2500;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public static CaptionStyleCompat getCaptionStyle(@NonNull final Context context) {
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT) return CaptionStyleCompat.DEFAULT;
|
||||
|
||||
final CaptioningManager captioningManager = (CaptioningManager)
|
||||
context.getSystemService(Context.CAPTIONING_SERVICE);
|
||||
if (captioningManager == null || !captioningManager.isEnabled()) {
|
||||
return CaptionStyleCompat.DEFAULT;
|
||||
}
|
||||
|
||||
return CaptionStyleCompat.createFromCaptionStyle(captioningManager.getUserStyle());
|
||||
}
|
||||
|
||||
/**
|
||||
* System font scaling:
|
||||
* Very small - 0.25f, Small - 0.5f, Normal - 1.0f, Large - 1.5f, Very Large - 2.0f
|
||||
* */
|
||||
@NonNull
|
||||
public static float getCaptionScale(@NonNull final Context context) {
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT) return 1f;
|
||||
|
||||
final CaptioningManager captioningManager = (CaptioningManager)
|
||||
context.getSystemService(Context.CAPTIONING_SERVICE);
|
||||
if (captioningManager == null || !captioningManager.isEnabled()) {
|
||||
return 1f;
|
||||
}
|
||||
|
||||
return captioningManager.getFontScale();
|
||||
}
|
||||
////////////////////////////////////////////////////////////////////////////
|
||||
// Private helpers
|
||||
////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
@@ -7,20 +7,42 @@ import com.google.android.exoplayer2.ExoPlayer;
|
||||
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 {
|
||||
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 +54,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 +64,7 @@ public class FailedMediaSource implements ManagedMediaSource {
|
||||
return playQueueItem;
|
||||
}
|
||||
|
||||
public Throwable getError() {
|
||||
public FailedMediaSourceException getError() {
|
||||
return error;
|
||||
}
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ import com.google.android.exoplayer2.source.MediaPeriod;
|
||||
import com.google.android.exoplayer2.source.MediaSource;
|
||||
import com.google.android.exoplayer2.upstream.Allocator;
|
||||
|
||||
import org.schabi.newpipe.playlist.PlayQueueItem;
|
||||
import org.schabi.newpipe.player.playqueue.PlayQueueItem;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
|
||||
@@ -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,135 @@
|
||||
package org.schabi.newpipe.player.mediasource;
|
||||
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.annotation.Nullable;
|
||||
|
||||
import com.google.android.exoplayer2.source.DynamicConcatenatingMediaSource;
|
||||
import com.google.android.exoplayer2.source.ShuffleOrder;
|
||||
|
||||
public class ManagedMediaSourcePlaylist {
|
||||
@NonNull private final DynamicConcatenatingMediaSource internalSource;
|
||||
|
||||
public ManagedMediaSourcePlaylist() {
|
||||
internalSource = new DynamicConcatenatingMediaSource(/*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);
|
||||
}
|
||||
|
||||
public void dispose() {
|
||||
internalSource.releaseSource();
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public DynamicConcatenatingMediaSource getParentMediaSource() {
|
||||
return internalSource;
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Playlist Manipulation
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
/**
|
||||
* Expands the {@link DynamicConcatenatingMediaSource} 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 DynamicConcatenatingMediaSource}.
|
||||
* @see DynamicConcatenatingMediaSource#addMediaSource
|
||||
* */
|
||||
public synchronized void append(@NonNull final ManagedMediaSource source) {
|
||||
internalSource.addMediaSource(source);
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes a {@link ManagedMediaSource} from {@link DynamicConcatenatingMediaSource}
|
||||
* at the given index. If this index is out of bound, then the removal is ignored.
|
||||
* @see DynamicConcatenatingMediaSource#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 DynamicConcatenatingMediaSource}
|
||||
* from the given source index to the target index. If either index is out of bound,
|
||||
* then the call is ignored.
|
||||
* @see DynamicConcatenatingMediaSource#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 DynamicConcatenatingMediaSource}
|
||||
* 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 DynamicConcatenatingMediaSource}
|
||||
* at the given index with a given {@link ManagedMediaSource}. If the index is out of bound,
|
||||
* then the replacement is ignored.
|
||||
* @see DynamicConcatenatingMediaSource#addMediaSource
|
||||
* @see DynamicConcatenatingMediaSource#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);
|
||||
}
|
||||
}
|
||||
@@ -6,7 +6,7 @@ import com.google.android.exoplayer2.ExoPlayer;
|
||||
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;
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -2,11 +2,11 @@ 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;
|
||||
@@ -14,17 +14,20 @@ 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.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
@@ -37,9 +40,12 @@ 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 +58,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;
|
||||
|
||||
@@ -103,7 +108,7 @@ public class MediaSourceManager {
|
||||
|
||||
@NonNull private final AtomicBoolean isBlocked;
|
||||
|
||||
@NonNull private DynamicConcatenatingMediaSource sources;
|
||||
@NonNull private ManagedMediaSourcePlaylist playlist;
|
||||
|
||||
public MediaSourceManager(@NonNull final PlaybackListener listener,
|
||||
@NonNull final PlayQueue playQueue) {
|
||||
@@ -143,9 +148,9 @@ public class MediaSourceManager {
|
||||
|
||||
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())
|
||||
@@ -167,7 +172,7 @@ public class MediaSourceManager {
|
||||
playQueueReactor.cancel();
|
||||
loaderReactor.dispose();
|
||||
syncReactor.dispose();
|
||||
sources.releaseSource();
|
||||
playlist.dispose();
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
@@ -215,17 +220,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 +272,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 +295,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,10 +306,10 @@ 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);
|
||||
@@ -321,9 +328,11 @@ public class MediaSourceManager {
|
||||
}
|
||||
}
|
||||
|
||||
private void maybeSynchronizePlayer() {
|
||||
maybeUnblock();
|
||||
maybeSync();
|
||||
private synchronized void maybeSynchronizePlayer() {
|
||||
if (isPlayQueueReady() && isPlaybackReady()) {
|
||||
maybeUnblock();
|
||||
maybeSync();
|
||||
}
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
@@ -332,12 +341,14 @@ public class MediaSourceManager {
|
||||
|
||||
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 +359,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 +392,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,10 +416,10 @@ 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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -445,10 +435,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 +453,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 +464,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
|
||||
@@ -486,72 +482,55 @@ 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.dispose();
|
||||
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.
|
||||
|
||||
@@ -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,15 @@ 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.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;
|
||||
@@ -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;
|
||||
@@ -1,4 +1,4 @@
|
||||
package org.schabi.newpipe.playlist;
|
||||
package org.schabi.newpipe.player.playqueue;
|
||||
|
||||
import org.schabi.newpipe.extractor.stream.StreamInfo;
|
||||
import org.schabi.newpipe.extractor.stream.StreamInfoItem;
|
||||
@@ -1,4 +1,4 @@
|
||||
package org.schabi.newpipe.playlist.events;
|
||||
package org.schabi.newpipe.player.playqueue.events;
|
||||
|
||||
|
||||
public class AppendEvent implements PlayQueueEvent {
|
||||
@@ -1,4 +1,4 @@
|
||||
package org.schabi.newpipe.playlist.events;
|
||||
package org.schabi.newpipe.player.playqueue.events;
|
||||
|
||||
|
||||
public class ErrorEvent implements PlayQueueEvent {
|
||||
@@ -1,4 +1,4 @@
|
||||
package org.schabi.newpipe.playlist.events;
|
||||
package org.schabi.newpipe.player.playqueue.events;
|
||||
|
||||
public class InitEvent implements PlayQueueEvent {
|
||||
@Override
|
||||
@@ -1,4 +1,4 @@
|
||||
package org.schabi.newpipe.playlist.events;
|
||||
package org.schabi.newpipe.player.playqueue.events;
|
||||
|
||||
public class MoveEvent implements PlayQueueEvent {
|
||||
final private int fromIndex;
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user