1
0
mirror of https://github.com/TeamNewPipe/NewPipe synced 2026-01-13 18:22:41 +00:00

Compare commits

..

399 Commits

Author SHA1 Message Date
Christian Schabesberger
7d427b4cc4 move on to v0.13.2 2018-04-14 11:22:26 +02:00
Christian Schabesberger
6c7e54868d Merge branch 'playlist_to_next' of https://github.com/Shan11812/NewPipe into test 2018-04-14 11:04:08 +02:00
Christian Schabesberger
9484a5e2ee fix conflict 2018-04-14 11:02:31 +02:00
Christian Schabesberger
3cbd2057e3 Merge branch 'qol-follow-ups' of https://github.com/karyogamy/NewPipe into test 2018-04-14 10:53:09 +02:00
Stanisław Sikorski
71ff1cf713 Translated using Weblate (Polish)
Currently translated at 100.0% (355 of 355 strings)
2018-04-14 00:27:38 +02:00
Rintaro matsuo
c737d891bc Translated using Weblate (Japanese)
Currently translated at 81.1% (288 of 355 strings)
2018-04-13 04:39:41 +02:00
ScratchBuild
82a53343fc Translated using Weblate (Japanese)
Currently translated at 81.1% (288 of 355 strings)
2018-04-13 04:39:36 +02:00
Andrea Troiano
03fdc60018 Translated using Weblate (Italian)
Currently translated at 100.0% (355 of 355 strings)
2018-04-12 12:15:57 +02:00
John Zhen Mo
42d19d98ad -Changed media source manager near edge loading to no longer load while player position is not progressing.
-Changed main video player to always self-destruct on stop.
-Extracted main video player lifecycle states into separate data class.
-Fixed play queue in full repeat mode does not load first item after expiring.
2018-04-11 20:27:39 -07:00
John Zhen Mo
c9915bba18 -Reversed special seek logic for short buffer livestreams.
-Fixed loader cleaning potentially canceling existing correct loading items.
-Updated ExoPlayer to 2.7.3.
2018-04-11 20:27:39 -07:00
John Zhen Mo
a275d7ff50 -Rollbacks the original main player UI to display nav and status bar on click.
-Changed system UI color to translucent on Lollipop and above.
2018-04-11 20:27:39 -07:00
John Zhen Mo
74199c8624 -Designated background player as default media button receiver.
-Fixed media button intent causing illegal state exception when sent from external apps.
2018-04-11 20:27:38 -07:00
John Zhen Mo
238bff1fee -Modified adaptive track selection to upgrade with 1 second of buffer.
-Modified dynamic track updates to seek to default time instead of clamping to 0 time when negative progress is reached.
2018-04-11 20:27:38 -07:00
John Zhen Mo
de534b58c5 -Removed playlist cleaning. 2018-04-11 20:27:38 -07:00
John Zhen Mo
111a0f9f17 -Modified playback parameter dialog to use translucent background. (reverted from commit 0d25254d4831aca92ad6cf6c0c772279b32b4a07) 2018-04-11 20:27:38 -07:00
John Zhen Mo
50392ed67d -Changed failed media source exception to use cause instead of top level exception. 2018-04-11 20:27:38 -07:00
John Zhen Mo
3de9da0528 -Bump exoplayer dependency to 2.7.2. 2018-04-11 20:27:38 -07:00
John Zhen Mo
ece93cadd5 -Added better player exception handling to player.
-Added expired media source cleaning to media source manager.
2018-04-11 20:27:38 -07:00
John Zhen Mo
7219c8d33c -Fixed main player multiwindow pauses when focus changes. 2018-04-11 20:27:38 -07:00
John Zhen Mo
b0a09c7876 -Added "add to playlist" button to service player play queue item drop down.
-Refactored playlist manipulations out from media source manager.
-Fixed potential ArrayOutOfBound exception when checking if player window is live.
-Fixed service player play queue potentially not refreshing when current play queue is replaced.
2018-04-11 20:27:38 -07:00
John Zhen Mo
1d017d3cbc -Modified LIVE button to be untranslatable on all players. 2018-04-11 20:27:38 -07:00
John Zhen Mo
5fa56f2aca -Modified playback parameter dialog to use translucent background. 2018-04-11 20:27:37 -07:00
Freddy Morán Jr
5ded1b139f Translated using Weblate (Spanish)
Currently translated at 99.7% (354 of 355 strings)
2018-04-11 18:41:45 +02:00
r2308145
2abb0db272 Translated using Weblate (Czech)
Currently translated at 100.0% (355 of 355 strings)
2018-04-11 18:35:42 +02:00
Marc Riera
674c041c13 Translated using Weblate (Catalan)
Currently translated at 100.0% (354 of 354 strings)
2018-04-11 10:09:56 +02:00
Marc Riera
03c89a69a2 Translated using Weblate (Catalan)
Currently translated at 95.7% (339 of 354 strings)
2018-04-11 09:30:46 +02:00
Marc Riera
dbed5e2a62 Translated using Weblate (Catalan)
Currently translated at 76.8% (272 of 354 strings)
2018-04-11 09:30:08 +02:00
Sérgio Marques
d7519903a1 Translated using Weblate (Portuguese)
Currently translated at 80.0% (284 of 355 strings)
2018-04-11 00:40:00 +02:00
E T
ab14f0d7ac Translated using Weblate (Turkish)
Currently translated at 100.0% (355 of 355 strings)
2018-04-10 23:03:32 +02:00
Marc Riera
3b6e43b7ca Translated using Weblate (Catalan)
Currently translated at 76.8% (272 of 354 strings)
2018-04-10 22:41:59 +02:00
Marc Riera
7d3b7af874 Translated using Weblate (Catalan)
Currently translated at 32.2% (114 of 354 strings)
2018-04-10 19:28:24 +02:00
r2308145
2acc348062 Translated using Weblate (Czech)
Currently translated at 100.0% (355 of 355 strings)
2018-04-10 18:01:41 +02:00
Freddy Morán Jr
33ba8acd05 Translated using Weblate (Spanish)
Currently translated at 100.0% (355 of 355 strings)
2018-04-10 17:49:03 +02:00
ezjerry liao
0c56af7090 Translated using Weblate (Chinese (Traditional))
Currently translated at 100.0% (355 of 355 strings)
2018-04-10 15:01:23 +02:00
Marc Riera
4375ecaad7 Translated using Weblate (Catalan)
Currently translated at 32.2% (114 of 354 strings)
2018-04-10 13:52:48 +02:00
nagizade
90c538d4b6 Translated using Weblate (Azerbaijani)
Currently translated at 12.9% (46 of 355 strings)
2018-04-10 11:34:35 +02:00
Dante
1ffa8329d0 Translated using Weblate (Chinese (Simplified))
Currently translated at 100.0% (355 of 355 strings)
2018-04-10 05:32:24 +02:00
Dante
b404b7974a Translated using Weblate (Chinese (Simplified))
Currently translated at 100.0% (355 of 355 strings)
2018-04-10 05:20:16 +02:00
E T
de44b580f9 Translated using Weblate (Turkish)
Currently translated at 100.0% (355 of 355 strings)
2018-04-09 22:53:10 +02:00
Roger
2a630cd745 Added translation using Weblate (Chinese (Mandarin)) 2018-04-09 22:31:57 +02:00
ssantos
1ccccc5e39 Translated using Weblate (German)
Currently translated at 100.0% (355 of 355 strings)
2018-04-09 22:31:35 +02:00
Roger
aec851acfa Added translation using Weblate (Urdu) 2018-04-09 22:28:18 +02:00
nagizade
ad5ac479fd Added translation using Weblate (Azerbaijani) 2018-04-09 10:55:09 +02:00
Heimen Stoffels
67b82a404c Translated using Weblate (Dutch)
Currently translated at 100.0% (355 of 355 strings)
2018-04-09 10:45:45 +02:00
Weblate
f7f86a2f62 Merge remote-tracking branch 'origin/dev' into dev 2018-04-09 00:13:42 +02:00
Anton Shestakov
6e42b4af27 Translated using Weblate (Russian)
Currently translated at 98.3% (348 of 354 strings)
2018-04-09 00:13:42 +02:00
Filip Sebastian
fe9ed8af76 Translated using Weblate (Romanian)
Currently translated at 74.8% (265 of 354 strings)
2018-04-09 00:13:40 +02:00
Eduardo Caron
c4c3f369c0 Translated using Weblate (Portuguese (Brazil))
Currently translated at 98.8% (350 of 354 strings)
2018-04-09 00:13:40 +02:00
M1ck
9b9bf40e47 Translated using Weblate (French)
Currently translated at 97.1% (344 of 354 strings)
2018-04-09 00:13:38 +02:00
Marc Riera
14a888a610 Added translation using Weblate (Catalan) 2018-04-09 00:13:35 +02:00
John Zhen Mo
f3d777c65c -Added accessibility caption style and text color resolver.
-Added button to open captioning settings activity from appearance settings.
-Modified player captions to use custom caption style when captioning manager is enabled.
-Removed caption size settings from appearance settings.
2018-04-08 13:58:55 -07:00
Mauricio Colli
d494b6c934 Long-click to open the downloads activity
- Closes #1263
2018-04-08 16:53:22 -03:00
Mauricio Colli
7294220727 Revert removal of menu items 2018-04-08 16:26:47 -03:00
Christian Schabesberger
665b9087b1 fix conflict 2018-04-08 18:49:27 +02:00
Christian Schabesberger
f1691050cd Merge branch 'download-permissions-screen' of https://github.com/arispoloway/NewPipe into perm 2018-04-08 18:21:27 +02:00
Aris Poloway
169b6acd24 Clean up download dialog opening 2018-04-08 11:47:11 -04:00
Mauricio Colli
35b3bf2edb Show download size preview 2018-04-08 08:45:15 -03:00
Christian Schabesberger
8f73c8c98b Merge pull request #1288 from jaller94/shot-to-short
Rename shot_description.txt to short_description.txt
2018-04-08 12:33:37 +02:00
Christian Schabesberger
94ea8c21eb Merge pull request #1289 from jaller94/german-translation
Add German translation of the description for fastlane
2018-04-08 12:32:53 +02:00
b6a19c3e2d Translated using Weblate (French)
Currently translated at 97.1% (344 of 354 strings)
2018-04-08 12:06:00 +02:00
M1ck
21455d58a6 Translated using Weblate (French)
Currently translated at 97.1% (344 of 354 strings)
2018-04-08 12:05:55 +02:00
Christian Paul
239e9bd3db Rename shot_description.txt to short_description.txt 2018-04-08 01:46:17 -07:00
Christian Paul
0832171a2b Add German translation of the description for fastlane 2018-04-08 01:38:25 -07:00
Anton Shestakov
1edfcc629c Translated using Weblate (Russian)
Currently translated at 97.7% (346 of 354 strings)
2018-04-08 09:34:00 +02:00
ezjerry liao
9a2a8698ef Translated using Weblate (Chinese (Traditional))
Currently translated at 100.0% (354 of 354 strings)
2018-04-08 03:28:46 +02:00
Nathan Follens
54c31422d9 Translated using Weblate (Dutch)
Currently translated at 100.0% (354 of 354 strings)
2018-04-08 01:49:32 +02:00
Filip Sebastian
a33464284c Translated using Weblate (Romanian)
Currently translated at 74.8% (265 of 354 strings)
2018-04-08 00:57:40 +02:00
Filip Sebastian
4842caf426 Translated using Weblate (Romanian)
Currently translated at 74.8% (265 of 354 strings)
2018-04-08 00:53:04 +02:00
Filip Sebastian
02b6b4d8eb Translated using Weblate (Romanian)
Currently translated at 74.2% (263 of 354 strings)
2018-04-08 00:18:05 +02:00
Filip Sebastian
4b0d7c7d88 Translated using Weblate (Romanian)
Currently translated at 74.2% (263 of 354 strings)
2018-04-08 00:17:57 +02:00
Filip Sebastian
559b53acc3 Translated using Weblate (Romanian)
Currently translated at 74.2% (263 of 354 strings)
2018-04-08 00:17:51 +02:00
Filip Sebastian
4233c18dbb Translated using Weblate (Romanian)
Currently translated at 70.3% (249 of 354 strings)
2018-04-08 00:11:00 +02:00
Filip Sebastian
adec2c9fcc Translated using Weblate (Romanian)
Currently translated at 70.0% (248 of 354 strings)
2018-04-08 00:10:48 +02:00
Filip Sebastian
6ad2406262 Translated using Weblate (Romanian)
Currently translated at 70.0% (248 of 354 strings)
2018-04-08 00:10:43 +02:00
Filip Sebastian
bfeb8ca104 Translated using Weblate (Romanian)
Currently translated at 69.7% (247 of 354 strings)
2018-04-08 00:10:34 +02:00
Filip Sebastian
5614edfa2f Translated using Weblate (Romanian)
Currently translated at 68.9% (244 of 354 strings)
2018-04-08 00:00:14 +02:00
Filip Sebastian
946f7b2305 Translated using Weblate (Romanian)
Currently translated at 68.9% (244 of 354 strings)
2018-04-07 23:59:49 +02:00
Filip Sebastian
dfcf435f88 Translated using Weblate (Romanian)
Currently translated at 67.7% (240 of 354 strings)
2018-04-07 23:53:12 +02:00
Filip Sebastian
0e734c267c Translated using Weblate (Romanian)
Currently translated at 66.9% (237 of 354 strings)
2018-04-07 23:49:06 +02:00
Filip Sebastian
faadcec4d1 Translated using Weblate (Romanian)
Currently translated at 66.6% (236 of 354 strings)
2018-04-07 23:47:13 +02:00
Filip Sebastian
eb81784818 Translated using Weblate (Romanian)
Currently translated at 66.3% (235 of 354 strings)
2018-04-07 23:45:01 +02:00
Filip Sebastian
327010f76b Translated using Weblate (Romanian)
Currently translated at 66.3% (235 of 354 strings)
2018-04-07 23:44:26 +02:00
Filip Sebastian
371669dcb6 Translated using Weblate (Romanian)
Currently translated at 66.3% (235 of 354 strings)
2018-04-07 23:44:19 +02:00
Filip Sebastian
fb702b93ca Translated using Weblate (Romanian)
Currently translated at 66.3% (235 of 354 strings)
2018-04-07 23:37:57 +02:00
Filip Sebastian
50c8453785 Translated using Weblate (Romanian)
Currently translated at 66.3% (235 of 354 strings)
2018-04-07 23:37:43 +02:00
Filip Sebastian
14fb5ee6c4 Translated using Weblate (Romanian)
Currently translated at 66.3% (235 of 354 strings)
2018-04-07 23:37:29 +02:00
Filip Sebastian
46c2343ec6 Translated using Weblate (Romanian)
Currently translated at 66.3% (235 of 354 strings)
2018-04-07 23:35:42 +02:00
Filip Sebastian
f5c226362f Translated using Weblate (Romanian)
Currently translated at 65.8% (233 of 354 strings)
2018-04-07 23:24:43 +02:00
Filip Sebastian
9114c1157d Translated using Weblate (Romanian)
Currently translated at 64.9% (230 of 354 strings)
2018-04-07 23:22:38 +02:00
Filip Sebastian
a4f4230275 Translated using Weblate (Romanian)
Currently translated at 64.9% (230 of 354 strings)
2018-04-07 23:22:21 +02:00
Filip Sebastian
46a6147c08 Translated using Weblate (Romanian)
Currently translated at 64.9% (230 of 354 strings)
2018-04-07 23:21:19 +02:00
Filip Sebastian
0d26923d79 Translated using Weblate (Romanian)
Currently translated at 64.9% (230 of 354 strings)
2018-04-07 23:21:09 +02:00
Filip Sebastian
b57c93cf03 Translated using Weblate (Romanian)
Currently translated at 64.6% (229 of 354 strings)
2018-04-07 23:20:04 +02:00
Filip Sebastian
ec19e4bc8f Translated using Weblate (Romanian)
Currently translated at 64.6% (229 of 354 strings)
2018-04-07 23:19:36 +02:00
Filip Sebastian
cb68a9c2ce Translated using Weblate (Romanian)
Currently translated at 64.1% (227 of 354 strings)
2018-04-07 23:16:44 +02:00
Filip Sebastian
b51ded1580 Translated using Weblate (Romanian)
Currently translated at 64.1% (227 of 354 strings)
2018-04-07 23:16:38 +02:00
Filip Sebastian
4b6fb5bfeb Translated using Weblate (Romanian)
Currently translated at 63.8% (226 of 354 strings)
2018-04-07 23:08:51 +02:00
Filip Sebastian
9b8175b5be Translated using Weblate (Romanian)
Currently translated at 63.5% (225 of 354 strings)
2018-04-07 23:07:58 +02:00
Filip Sebastian
3ecfc622e2 Translated using Weblate (Romanian)
Currently translated at 63.5% (225 of 354 strings)
2018-04-07 23:07:35 +02:00
Filip Sebastian
d8d2ee4e09 Translated using Weblate (Romanian)
Currently translated at 63.5% (225 of 354 strings)
2018-04-07 23:06:38 +02:00
Filip Sebastian
be478cb088 Translated using Weblate (Romanian)
Currently translated at 62.7% (222 of 354 strings)
2018-04-07 22:57:24 +02:00
Filip Sebastian
c187b4487f Translated using Weblate (Romanian)
Currently translated at 62.7% (222 of 354 strings)
2018-04-07 22:54:09 +02:00
Weblate
f6d21e74cb Merge remote-tracking branch 'origin/dev' into dev 2018-04-07 22:50:27 +02:00
Filip Sebastian
bde4c4bd25 Translated using Weblate (Romanian)
Currently translated at 62.4% (221 of 354 strings)
2018-04-07 22:50:25 +02:00
Mauricio Colli
d9aaceea95 Fix naming inconsistency 2018-04-07 16:09:34 -03:00
Aris Poloway
676d64a24a Open download dialog if video download is clicked instead 2018-04-07 13:32:31 -04:00
Weblate
8ae1768f71 Merge remote-tracking branch 'origin/dev' into dev 2018-04-07 13:37:23 +02:00
212705e7e3 Translated using Weblate (German)
Currently translated at 99.4% (352 of 354 strings)
2018-04-07 13:37:22 +02:00
Andreas Kromke
b3f6524e5c Translated using Weblate (German)
Currently translated at 99.4% (352 of 354 strings)
2018-04-07 13:37:20 +02:00
Aris Poloway
5ca0a0adf2 Open downloads after permission granted 2018-04-06 17:29:58 -04:00
Mauricio Colli
669d2c44c9 Dynamically create the opening choices
- Fixes TODO
2018-04-06 06:02:51 -03:00
Mauricio Colli
604bc5b4c1 Merge pull request #1255 from Poussinou/patch-1
Add distributionSha256Sum property to gradle-wrapper.properties
2018-04-06 04:50:19 -03:00
Mauricio Colli
88268ae569 Merge pull request #1268 from arispoloway/dev
Handle links with time specified (ex. &t=xxx)
2018-04-06 04:49:02 -03:00
Mauricio Colli
140fb86401 Fix Info's start time when using VideoDetailFragment
- Update extractor dependency
2018-04-06 04:35:44 -03:00
Aris Poloway
2b281c4357 Don't save stream and store recovery position from StreamInfo 2018-04-05 23:47:20 -04:00
MohammedSR Vevo
7b6dbfb456 Translated using Weblate (Kurdish)
Currently translated at 18.0% (64 of 354 strings)
2018-04-06 00:37:32 +02:00
Aris Poloway
b3c49ac86b Revert unnecessary url clean 2018-04-05 16:45:00 -04:00
Dalibor Klobučarić
d309fd9c97 Translated using Weblate (Croatian)
Currently translated at 77.4% (274 of 354 strings)
2018-04-05 11:35:13 +02:00
Aris Poloway
f3a280dcb6 Don't preemptively clean url and save StreamInfo with PlayQueueItem 2018-04-04 20:43:14 -04:00
MORTI
43894687a2 Translated using Weblate (French)
Currently translated at 96.3% (341 of 354 strings)
2018-04-05 01:40:08 +02:00
MohammedSR Vevo
8dd8928dab Translated using Weblate (Kurdish)
Currently translated at 4.2% (15 of 354 strings)
2018-04-04 23:26:31 +02:00
MohammedSR Vevo
282a4d15c2 Translated using Weblate (Kurdish)
Currently translated at 3.3% (12 of 354 strings)
2018-04-04 23:25:25 +02:00
MohammedSR Vevo
83a2ffc67e Added translation using Weblate (Kurdish) 2018-04-04 23:13:05 +02:00
Eduardo Caron
e9a6e3fae0 Translated using Weblate (Portuguese (Brazil))
Currently translated at 98.0% (347 of 354 strings)
2018-04-04 17:55:55 +02:00
Eduardo Caron
97944b9793 Translated using Weblate (Portuguese (Brazil))
Currently translated at 96.6% (342 of 354 strings)
2018-04-04 17:43:44 +02:00
Çağdaş Tatar
0b5d995b6d Translated using Weblate (Turkish)
Currently translated at 100.0% (354 of 354 strings)
2018-04-04 16:48:18 +02:00
Çağdaş Tatar
1781a9fe9a Translated using Weblate (Turkish)
Currently translated at 99.4% (352 of 354 strings)
2018-04-04 15:53:38 +02:00
Çağdaş Tatar
a0479bf7ca Translated using Weblate (Turkish)
Currently translated at 99.4% (352 of 354 strings)
2018-04-04 15:48:54 +02:00
r2308145
2affa31edf Translated using Weblate (Czech)
Currently translated at 100.0% (354 of 354 strings)
2018-04-04 13:35:50 +02:00
Poussinou
0bd040a851 Update gradle to 4.6 (and add corresponding distributionSha256Sum) 2018-04-04 13:33:21 +02:00
Tobias Groza
616c6a1607 Translated using Weblate (German)
Currently translated at 99.4% (352 of 354 strings)
2018-04-04 12:38:10 +02:00
a5176fbf80 Translated using Weblate (German)
Currently translated at 99.4% (352 of 354 strings)
2018-04-04 12:38:01 +02:00
Nick Undnick
f2b76b7582 Translated using Weblate (German)
Currently translated at 99.4% (352 of 354 strings)
2018-04-04 12:37:56 +02:00
ssantos
f4847f31ae Translated using Weblate (German)
Currently translated at 99.4% (352 of 354 strings)
2018-04-04 12:37:51 +02:00
Dalibor Klobučarić
05adcf2c12 Translated using Weblate (Croatian)
Currently translated at 76.2% (270 of 354 strings)
2018-04-04 10:39:56 +02:00
Dalibor Klobučarić
ea301be3c1 Translated using Weblate (Croatian)
Currently translated at 76.2% (270 of 354 strings)
2018-04-04 10:39:45 +02:00
Dalibor Klobučarić
88e9785b01 Translated using Weblate (Croatian)
Currently translated at 72.8% (258 of 354 strings)
2018-04-04 10:36:04 +02:00
Dual Natan
78efabfc1c Translated using Weblate (Macedonian)
Currently translated at 100.0% (354 of 354 strings)
2018-04-04 05:38:53 +02:00
Poussinou
9d3e22200c Update gradle-wrapper.properties 2018-04-03 21:59:42 +02:00
ssantos
183f9701fd Translated using Weblate (German)
Currently translated at 99.4% (352 of 354 strings)
2018-04-03 12:16:27 +02:00
ssantos
ebbba134fe Translated using Weblate (German)
Currently translated at 98.8% (350 of 354 strings)
2018-04-03 12:07:17 +02:00
ssantos
c257be8176 Translated using Weblate (German)
Currently translated at 98.3% (348 of 354 strings)
2018-04-03 12:00:19 +02:00
Dual Natan
afb0aea660 Translated using Weblate (Macedonian)
Currently translated at 100.0% (354 of 354 strings)
2018-04-03 05:17:57 +02:00
Dual Natan
960bf46f44 Translated using Weblate (Macedonian)
Currently translated at 77.9% (276 of 354 strings)
2018-04-03 04:42:16 +02:00
Dual Natan
16db799e88 Translated using Weblate (Macedonian)
Currently translated at 57.0% (202 of 354 strings)
2018-04-03 04:12:55 +02:00
Dual Natan
f8f2cdcfcc Translated using Weblate (Macedonian)
Currently translated at 42.0% (149 of 354 strings)
2018-04-03 03:40:03 +02:00
Dual Natan
a1f7862f96 Translated using Weblate (Macedonian)
Currently translated at 7.9% (28 of 354 strings)
2018-04-03 02:29:01 +02:00
Dual Natan
3ff9284f2a Added translation using Weblate (Macedonian) 2018-04-03 02:07:39 +02:00
Weblate
edf8f27c0f Merge remote-tracking branch 'origin/dev' into dev 2018-04-02 23:39:25 +02:00
horyzont
a720953ff3 Translated using Weblate (Polish)
Currently translated at 100.0% (354 of 354 strings)
2018-04-02 23:39:23 +02:00
Christian Schabesberger
f79dd26a82 remove outdated ccc slides 2018-04-02 20:54:26 +02:00
Christian Schabesberger
36b5fce4dd remove translation stuff 2018-04-02 20:53:56 +02:00
Christian Schabesberger
89537322fd fix email button not working 2018-04-02 20:53:32 +02:00
r2308145
2229ce1fe9 Translated using Weblate (Czech)
Currently translated at 100.0% (354 of 354 strings)
2018-04-02 16:36:12 +02:00
r2308145
f257b2177a Translated using Weblate (Czech)
Currently translated at 98.5% (349 of 354 strings)
2018-04-02 16:33:35 +02:00
r2308145
056c1fd43e Translated using Weblate (Czech)
Currently translated at 97.7% (346 of 354 strings)
2018-04-02 14:59:50 +02:00
r2308145
acacd3134f Translated using Weblate (Czech)
Currently translated at 96.8% (343 of 354 strings)
2018-04-02 14:58:11 +02:00
r2308145
1fe0ba1a6e Translated using Weblate (Czech)
Currently translated at 96.3% (341 of 354 strings)
2018-04-02 14:43:01 +02:00
r2308145
e639f4b5de Translated using Weblate (Czech)
Currently translated at 95.4% (338 of 354 strings)
2018-04-02 14:35:57 +02:00
r2308145
5a234bd989 Translated using Weblate (Czech)
Currently translated at 94.6% (335 of 354 strings)
2018-04-02 14:34:55 +02:00
r2308145
661227266e Translated using Weblate (Czech)
Currently translated at 88.9% (315 of 354 strings)
2018-04-02 14:22:33 +02:00
horyzont
e01556fd9a Translated using Weblate (Polish)
Currently translated at 100.0% (354 of 354 strings)
2018-04-01 23:03:18 +02:00
horyzont
5cb53ffc18 Translated using Weblate (Polish)
Currently translated at 100.0% (354 of 354 strings)
2018-04-01 23:01:44 +02:00
horyzont
e7178626ab Translated using Weblate (Polish)
Currently translated at 100.0% (354 of 354 strings)
2018-04-01 22:56:07 +02:00
horyzont
7f0948b0ed Translated using Weblate (Polish)
Currently translated at 99.7% (353 of 354 strings)
2018-04-01 22:50:49 +02:00
Weblate
4baee67781 Merge remote-tracking branch 'origin/dev' into dev 2018-04-01 22:41:06 +02:00
Heimen Stoffels
7061f38abe Translated using Weblate (Dutch)
Currently translated at 100.0% (354 of 354 strings)
2018-04-01 22:41:06 +02:00
horyzont
2aa259c0b2 Translated using Weblate (Polish)
Currently translated at 98.3% (348 of 354 strings)
2018-04-01 22:41:02 +02:00
Christian Schabesberger
a0f74e715a fix bugreport for kiosk 2018-04-01 21:54:00 +02:00
Christian Schabesberger
c13e761c3f remove global ip range label 2018-04-01 19:58:07 +02:00
shan11812
2837e44bab Adds 'Add to Playlist' option to Related and Channel lists 2018-04-01 20:47:24 +05:30
Heimen Stoffels
d41e3bb41e Translated using Weblate (Dutch)
Currently translated at 100.0% (354 of 354 strings)
2018-04-01 16:43:09 +02:00
Weblate
4160bbb8c4 Merge remote-tracking branch 'origin/dev' into dev 2018-04-01 15:38:23 +02:00
Veli Tasalı
404a20f280 Translated using Weblate (Turkish)
Currently translated at 99.4% (352 of 354 strings)
2018-04-01 15:38:23 +02:00
se7entime
2241146b9f Translated using Weblate (Indonesian)
Currently translated at 89.2% (316 of 354 strings)
2018-04-01 15:38:20 +02:00
Christian Schabesberger
bcb26c5721 move on to version 0.13.1 2018-04-01 15:29:57 +02:00
Christian Schabesberger
a681d8c1ba Merge pull request #1227 from TeamNewPipe/removebeta
remove beta
2018-04-01 15:28:49 +02:00
cozyplanes
0515b74f75 Translated using Weblate (Korean)
Currently translated at 100.0% (354 of 354 strings)
2018-04-01 13:40:13 +02:00
Veli Tasalı
7eeb96d88e Translated using Weblate (Turkish)
Currently translated at 99.1% (351 of 354 strings)
2018-03-31 18:01:21 +02:00
se7entime
5b6c73ef06 Translated using Weblate (Indonesian)
Currently translated at 89.5% (317 of 354 strings)
2018-03-31 15:30:26 +02:00
se7entime
c70b866d16 Translated using Weblate (Indonesian)
Currently translated at 87.2% (309 of 354 strings)
2018-03-31 15:28:10 +02:00
se7entime
b84f2874dc Translated using Weblate (Indonesian)
Currently translated at 87.2% (309 of 354 strings)
2018-03-31 15:28:03 +02:00
se7entime
c372b5529b Translated using Weblate (Indonesian)
Currently translated at 80.5% (285 of 354 strings)
2018-03-31 15:25:16 +02:00
se7entime
a43156c38d Translated using Weblate (Indonesian)
Currently translated at 74.5% (264 of 354 strings)
2018-03-31 15:22:37 +02:00
se7entime
a15112febc Translated using Weblate (Indonesian)
Currently translated at 68.9% (244 of 354 strings)
2018-03-31 15:19:45 +02:00
se7entime
1c6a677a39 Translated using Weblate (Indonesian)
Currently translated at 67.7% (240 of 354 strings)
2018-03-31 15:18:34 +02:00
se7entime
9bf749a2c8 Translated using Weblate (Indonesian)
Currently translated at 65.5% (232 of 354 strings)
2018-03-31 15:15:16 +02:00
se7entime
f6c9d9df20 Translated using Weblate (Indonesian)
Currently translated at 57.3% (203 of 354 strings)
2018-03-31 15:04:54 +02:00
se7entime
5386e0ded9 Translated using Weblate (Indonesian)
Currently translated at 56.7% (201 of 354 strings)
2018-03-31 15:04:27 +02:00
se7entime
8fab405a3a Translated using Weblate (Indonesian)
Currently translated at 55.0% (195 of 354 strings)
2018-03-31 15:03:32 +02:00
se7entime
ee409f3ca9 Translated using Weblate (Indonesian)
Currently translated at 49.7% (176 of 354 strings)
2018-03-31 15:00:23 +02:00
Weblate
bda9beacaa Merge remote-tracking branch 'origin/dev' into dev 2018-03-31 14:59:25 +02:00
Veli Tasalı
5d8c7e5733 Translated using Weblate (Turkish)
Currently translated at 96.8% (343 of 354 strings)
2018-03-31 14:59:24 +02:00
Stnby
a68c763125 Translated using Weblate (Lithuanian)
Currently translated at 87.8% (311 of 354 strings)
2018-03-31 14:59:23 +02:00
se7entime
31ac89d9d6 Translated using Weblate (Indonesian)
Currently translated at 49.4% (175 of 354 strings)
2018-03-31 14:59:22 +02:00
se7entime
8b258cbbe4 Translated using Weblate (Indonesian)
Currently translated at 47.7% (169 of 354 strings)
2018-03-31 14:55:56 +02:00
se7entime
53676fc0fd Translated using Weblate (Indonesian)
Currently translated at 47.1% (167 of 354 strings)
2018-03-31 14:53:54 +02:00
Christian Schabesberger
42c561af9e update build tools to 27.0.3 2018-03-30 18:45:27 +02:00
Christian Schabesberger
fae777c14c make service items in drawer be dynamicly generated 2018-03-30 18:41:11 +02:00
Christian Schabesberger
1af6dc614a remove global ip range label 2018-03-30 14:46:26 +02:00
Christian Schabesberger
2986004638 remove entry points in three dot menu in main screen 2018-03-30 11:52:05 +02:00
Christian Schabesberger
d6f7b4706b remove beta build 2018-03-30 11:09:07 +02:00
Christian Schabesberger
b40dd3e5c0 update gradle 2018-03-30 10:58:32 +02:00
Schabi
2f4097ca9d Merge branch 'dev' of github.com:teamnewpipe/NewPipe into dev 2018-03-28 09:58:18 +02:00
Schabi
c8a91fbb25 add note to check if bugs exist in latest version 2018-03-28 09:58:09 +02:00
Víctor Manuel Tapia Ramírez
0ec259a5fc Translated using Weblate (Spanish)
Currently translated at 100.0% (354 of 354 strings)
2018-03-28 09:50:20 +02:00
Weblate
2fda99bdb7 Merge remote-tracking branch 'origin/dev' into dev 2018-03-28 05:36:47 +02:00
nautilusx
1bb4ac4e9c Translated using Weblate (German)
Currently translated at 95.7% (339 of 354 strings)
2018-03-28 05:36:45 +02:00
MORTI
f376b98c09 Translated using Weblate (French)
Currently translated at 96.3% (341 of 354 strings)
2018-03-28 05:36:41 +02:00
Christian Schabesberger
7382fdb2d2 Merge pull request #1217 from Grammost/patch-1
Fix broken Librepay link in README.md
2018-03-27 11:53:08 +02:00
Edwar Tikhonov
aead9592cf Translated using Weblate (Russian)
Currently translated at 94.9% (336 of 354 strings)
2018-03-27 11:39:53 +02:00
MORTI
bedacf29d9 Translated using Weblate (French)
Currently translated at 96.0% (340 of 354 strings)
2018-03-27 04:49:28 +02:00
570e9a8307 Translated using Weblate (French)
Currently translated at 89.2% (316 of 354 strings)
2018-03-27 01:15:17 +02:00
justanidea
fcb94ec603 Translated using Weblate (French)
Currently translated at 88.9% (315 of 354 strings)
2018-03-27 01:13:44 +02:00
69886ed58b Translated using Weblate (French)
Currently translated at 88.7% (314 of 354 strings)
2018-03-27 01:13:05 +02:00
MORTI
798235cd21 Translated using Weblate (French)
Currently translated at 88.4% (313 of 354 strings)
2018-03-27 01:12:25 +02:00
Grammost
b38b6b6f35 Update README.md 2018-03-26 15:10:03 +00:00
Grammost
2dba13c52e Fix broken Librepay link in README.md 2018-03-26 14:49:59 +00:00
ezjerry liao
ff366cb2c5 Translated using Weblate (Chinese (Traditional))
Currently translated at 100.0% (354 of 354 strings)
2018-03-26 11:33:45 +02:00
Weblate
a87861a993 Merge remote-tracking branch 'origin/dev' into dev 2018-03-25 23:34:16 +02:00
AB
65476356c9 Translated using Weblate (Ukrainian)
Currently translated at 100.0% (354 of 354 strings)
2018-03-25 23:34:12 +02:00
Christian Schabesberger
8af6667f3e Merge pull request #1212 from karyogamy/sub-quick-fix
Subscription Front Page Crash Fix
2018-03-25 20:31:36 +02:00
John Zhen Mo
b546df7b95 -Fixed activity not available exception when setting title on subscription fragments when used as front pager. 2018-03-25 10:29:37 -07:00
Emanuele Petriglia
20c1f12da8 Translated using Weblate (Italian)
Currently translated at 100.0% (354 of 354 strings)
2018-03-25 13:21:40 +02:00
Weblate
e65ff201af Merge remote-tracking branch 'origin/dev' into dev 2018-03-25 11:36:18 +02:00
Tobias Groza
4f367a3dcd Translated using Weblate (German)
Currently translated at 89.8% (318 of 354 strings)
2018-03-25 11:36:16 +02:00
Heimen Stoffels
d7093bce4d Translated using Weblate (Dutch)
Currently translated at 100.0% (354 of 354 strings)
2018-03-25 11:36:13 +02:00
Christian Schabesberger
79e3e59cae move on to version 0.13.0 2018-03-25 11:18:51 +02:00
f1b292df93 Translated using Weblate (German)
Currently translated at 89.8% (318 of 354 strings)
2018-03-25 00:41:30 +01:00
Tobias Groza
0cd383b8dd Translated using Weblate (German)
Currently translated at 88.9% (315 of 354 strings)
2018-03-25 00:40:01 +01:00
Weblate
22c404a667 Merge remote-tracking branch 'origin/dev' into dev 2018-03-24 18:40:13 +01:00
Oleh Ilnytskyi
ba11a59d89 Translated using Weblate (Polish)
Currently translated at 93.0% (319 of 343 strings)
2018-03-24 18:40:13 +01:00
Freddy Morán Jr
06020851a9 Translated using Weblate (Spanish)
Currently translated at 100.0% (343 of 343 strings)
2018-03-24 18:40:07 +01:00
Christian Schabesberger
fd5cbde18c fix readme conflict 2018-03-24 10:56:31 +01:00
Christian Schabesberger
384398a1e2 Merge branch 'qol-updates' of https://github.com/karyogamy/NewPipe into ui 2018-03-24 10:50:17 +01:00
Christian Schabesberger
2bdba4ba8b fix somehow broken librepay donate link 2018-03-24 10:35:43 +01:00
Christian Schabesberger
db4179a530 fix weird librepay link broken 2018-03-24 10:25:42 +01:00
Christian Schabesberger
b905b74dc2 Merge pull request #1201 from TobiGr/readme
Update README.md
2018-03-24 10:21:58 +01:00
Mohammad Hassan
ff05c36856 Translated using Weblate (Arabic)
Currently translated at 87.4% (300 of 343 strings)
2018-03-24 02:34:15 +01:00
TobiGr
40ea5eb53d Update README.md
Add compressed screenshots
Add liberapay to donation section
2018-03-23 16:15:50 +01:00
John Zhen Mo
02f48ccc7f -Removed duplicate dialog open instances in service player activity. 2018-03-22 18:44:03 -07:00
John Zhen Mo
72eaff148c -Fixed main player paused video not abandoning audio focus after navigating away from activity during interruption, when resume on focus regain is enabled.
-Separated onPause and onPlay functions from onPlayPause.
-Renamed onVideoPlayPause to onPlayPause.
2018-03-22 18:12:11 -07:00
John Zhen Mo
8b60397f06 -Changed detail fragment thumbnail failure to produce a snackbar error rather than a full error activity. 2018-03-22 18:11:59 -07:00
John Zhen Mo
18d019c62a -Added quadratic slider strategy implementation and tests.
-Modified playback speed control to use quadratic sliders instead of linear.
-Modified number formatters in player helper to use double instead of float.
-Simplified slider behavior in playback parameter dialog.
-Fixed potential NPE in base local fragment.
2018-03-21 20:08:33 -07:00
Weblate
27527b18e1 Merge remote-tracking branch 'origin/dev' into dev 2018-03-21 23:37:21 +01:00
Tobias Groza
ae9aa2662a Translated using Weblate (German)
Currently translated at 93.2% (320 of 343 strings)
2018-03-21 23:37:17 +01:00
Christian Schabesberger
3bfb593b21 Merge pull request #1197 from TeamNewPipe/remove-ip-range
Remove IP range
2018-03-21 20:59:41 +01:00
wb9688
8556e99241 Remove IP range
This would close #1148. I haven't tested it myself yet. @TheAssassin: Could you update the Sentry part?
2018-03-21 10:55:22 +01:00
John Zhen Mo
e885822a34 -Added playback speed control dialog to allow full user control over player tempo and pitch parameters.
-Changed tempo and pitch button in service player activity and tempo button in main video player to open speed control dialog.
-Changed LIVE button to be no longer clickable when player position is at or beyond default position.
-Changed main video player to use AppCompatActivity rather than Activity.
-Fixed video player tempo button not updating when player speed parameters change.
-Fixed player crashing on lower sdk versions due to no MediaButtonReceiver, added intent back to manifest.
-Fixed inconsistent gradle library naming.
-Fixed stetho dependencies incorrect version.
2018-03-21 00:11:54 -07:00
Weblate
8ebb564a79 Merge remote-tracking branch 'origin/dev' into dev 2018-03-20 16:36:52 +01:00
Tobias Groza
7dc176edcb Translated using Weblate (German)
Currently translated at 93.2% (320 of 343 strings)
2018-03-20 16:36:47 +01:00
John Zhen Mo
5167fe078b -Refactored synchronization checks out from MediaSourceManager to ManagedMediaSource.
-Fixed null input causing potential NPE on PlayQueueItem.
2018-03-19 16:44:18 -07:00
John Zhen Mo
bc7188c8a8 -Added media session implementation for all players.
-Extracted version numbers in gradle dependencies.
-Updated ExoPlayer to 2.7.1.
-Updated RxJava to 2.1.10, RxAndroid to 2.0.2 and RxBinding to 2.1.1.
-Removed deprecated implementation of media buttons.
2018-03-19 16:44:18 -07:00
John Zhen Mo
5a05cb96be -Changed start position seek to occur after media source window has been prepared.
-Fixed livestream not seeking to live when started from play queue.
-Fixed media source manager synchronization to only occur after timeline change has completed.
-Fixed auto queue not working when last item is replayed after the auto-queued item is removed.
-Updated ExoPlayer to 2.7.1.
2018-03-19 16:44:17 -07:00
John Zhen Mo
0258726f0a -Changed thumbnail toggle in disabled mode to load dark dummy image.
-Changed play queue items to display service names.
-Fixed Soundcloud playlist not fitting thumbnail.
-Refactored image display options to follow uniform behavior.
-Refactoring and style changes on audio reactor and media button receiver.
2018-03-19 16:44:17 -07:00
John Zhen Mo
2fa9aa04f4 -Bump support library and multidex version. 2018-03-19 16:40:40 -07:00
John Zhen Mo
a5f9927459 -Fixed main player animations not working on first call. 2018-03-19 16:40:40 -07:00
John Zhen Mo
61b422502b -[#1060] Added toggle to disable thumbnail loading.
-Added button to wipe metadata cache.
-Added more paddings on player buttons.
-Added new animations to main player secondary controls and play queue expand/collapse.
-Refactored play queue item touch callback for use in all players.
-Improved MediaSourceManager to better handle expired stream reloading.
-[#1186] Changed live sync button text to "LIVE".
-Removed MediaSourceManager loader coupling on main players.
-Moved service dependent expiry resolution to ServiceHelper.
-[#1186] Fixed livestream timeline updates causing negative time position.
-[#1186] Fixed livestream not starting from live-edge.
-Fixed main player system UI not retracting on playback start.
2018-03-19 16:40:40 -07:00
Christian Schabesberger
1e57b5ea49 Merge pull request #1193 from TeamNewPipe/fix_page
made frontend combatible to latest extractor refactorings
2018-03-19 23:22:28 +01:00
Allan Nordhøy
12ce915e8e Translated using Weblate (Norwegian Bokmål)
Currently translated at 93.2% (320 of 343 strings)
2018-03-19 22:44:34 +01:00
ezjerry liao
a32273af91 Translated using Weblate (Chinese (Traditional))
Currently translated at 100.0% (343 of 343 strings)
2018-03-19 12:40:50 +01:00
Christian Schabesberger
96a327af17 made frontend combatible to latest extractor refactorings 2018-03-18 16:37:49 +01:00
AB
1910e81ad9 Translated using Weblate (Ukrainian)
Currently translated at 100.0% (343 of 343 strings)
2018-03-18 14:40:32 +01:00
ezjerry liao
80593e774c Translated using Weblate (Chinese (Traditional))
Currently translated at 99.7% (342 of 343 strings)
2018-03-18 13:35:17 +01:00
AB
c092fc8e18 Translated using Weblate (Ukrainian)
Currently translated at 100.0% (343 of 343 strings)
2018-03-17 13:40:27 +01:00
E T
579efa15c7 Translated using Weblate (Turkish)
Currently translated at 100.0% (343 of 343 strings)
2018-03-17 12:40:00 +01:00
E T
37ff4e9aeb Translated using Weblate (Turkish)
Currently translated at 100.0% (343 of 343 strings)
2018-03-16 12:21:00 +01:00
Weblate
4e4a7de5d4 Merge remote-tracking branch 'origin/dev' into dev 2018-03-15 15:35:15 +01:00
Olexandr Nesterenko
2d1bc6436a Translated using Weblate (Ukrainian)
Currently translated at 98.8% (339 of 343 strings)
2018-03-15 15:35:15 +01:00
Edwar Tikhonov
65726d75cc Translated using Weblate (Russian)
Currently translated at 100.0% (343 of 343 strings)
2018-03-15 15:35:13 +01:00
Yaron Shahrabani
d63c0a32eb Translated using Weblate (Hebrew)
Currently translated at 75.8% (260 of 343 strings)
2018-03-15 15:35:12 +01:00
ezjerry liao
e49c4162e5 Translated using Weblate (Chinese (Traditional))
Currently translated at 98.5% (338 of 343 strings)
2018-03-15 15:35:07 +01:00
Christian Schabesberger
ffc5ad5ce7 Merge branch 'media_buttons' of https://github.com/alexandrepa/NewPipe into media 2018-03-14 18:27:17 +01:00
Edwar Tikhonov
2dbfc28d69 Translated using Weblate (Russian)
Currently translated at 100.0% (343 of 343 strings)
2018-03-14 16:34:36 +01:00
ezjerry liao
1ac7b2b8cb Translated using Weblate (Chinese (Traditional))
Currently translated at 98.2% (337 of 343 strings)
2018-03-14 13:35:30 +01:00
cozyplanes
7d047e612e Translated using Weblate (Korean)
Currently translated at 100.0% (343 of 343 strings)
2018-03-14 12:38:22 +01:00
alexandre patelli
24f2999669 Handling play/pause button from different headsets 2018-03-13 22:57:59 +01:00
Eduardo Caron
b08728b645 Translated using Weblate (Portuguese (Brazil))
Currently translated at 98.8% (339 of 343 strings)
2018-03-13 18:38:49 +01:00
cozyplanes
00dee43a1e Translated using Weblate (Korean)
Currently translated at 100.0% (343 of 343 strings)
2018-03-13 12:23:55 +01:00
Freddy Morán Jr
21e300b9f3 Translated using Weblate (Spanish)
Currently translated at 100.0% (343 of 343 strings)
2018-03-12 21:18:01 +01:00
Emanuele Petriglia
158f0aa2d9 Translated using Weblate (Italian)
Currently translated at 100.0% (343 of 343 strings)
2018-03-12 17:53:29 +01:00
Weblate
10fb763d66 Merge remote-tracking branch 'origin/dev' into dev 2018-03-12 17:00:19 +01:00
cozyplanes
4aa2c1c2c2 Translated using Weblate (Korean)
Currently translated at 95.0% (326 of 343 strings)
2018-03-12 17:00:18 +01:00
Heimen Stoffels
bdf044d264 Translated using Weblate (Dutch)
Currently translated at 100.0% (343 of 343 strings)
2018-03-12 17:00:08 +01:00
Christian Schabesberger
6049a1f2f5 fix playlist banner foo 2018-03-12 16:18:03 +01:00
Christian Schabesberger
a9fea9f606 fix release crash because of setting sidebar header 2018-03-12 16:00:38 +01:00
Weblate
11002e9d45 Merge remote-tracking branch 'origin/dev' into dev 2018-03-12 15:50:38 +01:00
cozyplanes
e7a0b850df Translated using Weblate (Korean)
Currently translated at 100.0% (324 of 324 strings)
2018-03-12 15:50:33 +01:00
Christian Schabesberger
736c902f3c fix weblate conflict 2018-03-11 21:38:19 +01:00
ButterflyOfFire
a67ff564d0 Translated using Weblate (Arabic)
Currently translated at 91.6% (297 of 324 strings)
2018-03-11 20:36:51 +01:00
Andreas Kleinert
2a778383b1 Translated using Weblate (German)
Currently translated at 93.2% (302 of 324 strings)
2018-03-11 20:36:47 +01:00
alexandre patelli
36457400e7 Review Fixes 2018-03-11 19:23:00 +01:00
alexandre patelli
1d629e7b2e Direct use of AudioManager from AudioReactor 2018-03-11 15:09:11 +01:00
alexandre patelli
5f764ab8f5 Media Button Play/Pause, Previous and Next in Background Player 2018-03-10 18:25:20 +01:00
Pablo Hinojosa
a8c9edbc3f Translated using Weblate (Spanish)
Currently translated at 100.0% (324 of 324 strings)
2018-03-10 17:38:49 +01:00
Emanuele Petriglia
0a5bffe826 Translated using Weblate (Italian)
Currently translated at 100.0% (324 of 324 strings)
2018-03-10 17:38:39 +01:00
Mauricio Colli
9c9b6bc0d6 Change thumbnail's scale strategy
- Closes #1054
2018-03-10 13:36:50 -03:00
Mauricio Colli
562f7e7e41 Add duration view to video detail fragment
- Add "textAllCaps" to the mini stream layout
- Closes #609
2018-03-10 13:20:10 -03:00
Mauricio Colli
594c55afa6 Merge pull request #1145 from mauriciocolli/dev
Implement subscriptions import/export
2018-03-10 12:11:13 -03:00
Pablo Hinojosa
0abf97e999 Translated using Weblate (Spanish)
Currently translated at 100.0% (324 of 324 strings)
2018-03-10 16:07:50 +01:00
Freddy Morán Jr
b811aec773 Translated using Weblate (Spanish)
Currently translated at 98.7% (320 of 324 strings)
2018-03-10 15:47:34 +01:00
AB
ca47f566dc Translated using Weblate (Ukrainian)
Currently translated at 100.0% (324 of 324 strings)
2018-03-10 14:41:06 +01:00
Yaron Shahrabani
65674f7fd4 Translated using Weblate (Hebrew)
Currently translated at 79.6% (258 of 324 strings)
2018-03-10 09:36:40 +01:00
Eduardo Caron
dcc510ff6c Translated using Weblate (Portuguese (Brazil))
Currently translated at 98.7% (320 of 324 strings)
2018-03-10 00:39:26 +01:00
Emanuele Petriglia
a4fe43a964 Translated using Weblate (Italian)
Currently translated at 100.0% (324 of 324 strings)
2018-03-09 16:44:18 +01:00
Kotoba Murasaki
3dab4c07cf Translated using Weblate (Ukrainian)
Currently translated at 100.0% (324 of 324 strings)
2018-03-09 13:52:18 +01:00
E T
ee3248ea5d Translated using Weblate (Turkish)
Currently translated at 100.0% (324 of 324 strings)
2018-03-09 12:28:47 +01:00
7be5ec0521 Translated using Weblate (Hebrew)
Currently translated at 79.3% (257 of 324 strings)
2018-03-09 09:25:05 +01:00
Yaron Shahrabani
04e90cc279 Translated using Weblate (Hebrew)
Currently translated at 78.7% (255 of 324 strings)
2018-03-09 09:24:48 +01:00
ec8d488249 Translated using Weblate (Hebrew)
Currently translated at 78.0% (253 of 324 strings)
2018-03-09 09:24:03 +01:00
Yaron Shahrabani
41fdafac45 Translated using Weblate (Hebrew)
Currently translated at 76.8% (249 of 324 strings)
2018-03-09 09:23:19 +01:00
d2e2622279 Translated using Weblate (Hebrew)
Currently translated at 75.9% (246 of 324 strings)
2018-03-09 09:22:41 +01:00
Yaron Shahrabani
9476bd6527 Translated using Weblate (Hebrew)
Currently translated at 75.6% (245 of 324 strings)
2018-03-09 09:22:32 +01:00
24a06ea6f6 Translated using Weblate (Hebrew)
Currently translated at 75.6% (245 of 324 strings)
2018-03-09 09:22:09 +01:00
Yaron Shahrabani
2567f8eefb Translated using Weblate (Hebrew)
Currently translated at 75.3% (244 of 324 strings)
2018-03-09 09:21:54 +01:00
ezjerry liao
fa5f5ce251 Translated using Weblate (Chinese (Traditional))
Currently translated at 100.0% (324 of 324 strings)
2018-03-09 02:58:02 +01:00
Heimen Stoffels
728a61756a Translated using Weblate (Dutch)
Currently translated at 100.0% (324 of 324 strings)
2018-03-08 21:25:11 +01:00
Kotoba Murasaki
736ccbe376 Translated using Weblate (Ukrainian)
Currently translated at 100.0% (324 of 324 strings)
2018-03-08 20:38:45 +01:00
Weblate
fa2b226b9e Merge remote-tracking branch 'origin/dev' into dev 2018-03-08 20:24:24 +01:00
Kotoba Murasaki
e8c5ae194d Translated using Weblate (Ukrainian)
Currently translated at 100.0% (318 of 318 strings)
2018-03-08 20:24:19 +01:00
Mauricio Colli
cc2feab37e Implement UI for subscriptions import/export
- Nice and easy to use import/export options in the subscriptions fragment
- Includes instructions for each service (in the import fragment/screen)
2018-03-08 11:50:46 -03:00
Mauricio Colli
83b084a90b Implement subscriptions import/export
- Import subscriptions from YouTube and SoundCloud (all services that the extractor support)
- Import/export a JSON representation of the subscriptions
- [Minor] Remove some javax annotations in favor of the one provided by the android support library
2018-03-08 10:39:24 -03:00
Christian Schabesberger
e2ac0722c8 Merge pull request #1155 from TeamNewPipe/update-readme
Update README.md
2018-03-06 19:46:55 +01:00
Schabi
c3efb40b8e move to latest version of extractor 2018-03-06 19:44:17 +01:00
Schabi
03d7a416f3 add live lable to toolbar 2018-03-06 19:24:58 +01:00
Schabi
e6e812fdb0 Merge branch 'exoplayer-update' of https://github.com/karyogamy/NewPipe into live 2018-03-06 17:23:37 +01:00
John Zhen Mo
b34160eeec -Fixed main video player losing state when killed in background.
-Disabled auto queuing when repeating is enabled.
-Added method to use startForegroundService instead of startService in sdk 26 and up.
2018-03-05 19:03:49 -08:00
John Zhen Mo
d01aeab242 -Added auto-queuing to allow next or related streams to queue up when the last item on play queue is playing.
-Added toggle to enable auto-queuing.
-Modified main video player to only pause the video onPause.
-Fixed main video player not saving play queue state onStop.
2018-03-04 20:16:38 -08:00
9904e01252 Translated using Weblate (Serbian)
Currently translated at 78.6% (250 of 318 strings)
2018-03-04 14:41:36 +01:00
John Zhen Mo
7f068b691b -Removed system ui on main player for Kitkat or above.
-[#1151] Hide video player UI on playing to avoid unnecessary interruptions after pause, seek and resize.
2018-03-03 20:58:53 -08:00
John Zhen Mo
59558efed1 -Added seamless shuffling.
-Reenabled full window loading in MediaSourceManager.
2018-03-03 20:58:06 -08:00
John Zhen Mo
a88e19a8ed -Added toggle to enable fast inexact seek in players.
-Improved player sync calls to recognize different metadata updates.
-Changed MediaSourceManager to synchronize only after timeline changes and reenabled multiple sync calls to player.
-Renamed listener and synchronization methods related to MediaSourceManager.
2018-03-03 14:24:21 -08:00
John Zhen Mo
0c17f0825b -Added loader eviction to avoid spawning too many threads in MediaSourceManager.
-Added nonnull and final constraints to variables in MediaSourceManager.
-Added nonnull and final constraints on context related objects in BasePlayer.
-Fixed Hls livestreams crashing player when behind live window for too long.
-Fixed cache miss when InfoCache key mismatch between StreamInfo and StreamInfoItem.
2018-03-03 11:42:23 -08:00
Mladen Pejaković
9384d2523a Translated using Weblate (Serbian)
Currently translated at 78.3% (249 of 318 strings)
2018-03-03 14:03:02 +01:00
0e13172a89 Translated using Weblate (Serbian)
Currently translated at 78.3% (249 of 318 strings)
2018-03-03 14:02:34 +01:00
Mladen Pejaković
92f34452b5 Translated using Weblate (Serbian)
Currently translated at 77.9% (248 of 318 strings)
2018-03-03 14:02:17 +01:00
db54929584 Translated using Weblate (Serbian)
Currently translated at 77.9% (248 of 318 strings)
2018-03-03 14:01:59 +01:00
Mladen Pejaković
ba23cafb18 Translated using Weblate (Serbian)
Currently translated at 77.6% (247 of 318 strings)
2018-03-03 14:01:43 +01:00
a2e189767b Translated using Weblate (Serbian)
Currently translated at 77.3% (246 of 318 strings)
2018-03-03 14:01:30 +01:00
0d236fd678 Translated using Weblate (Serbian)
Currently translated at 76.4% (243 of 318 strings)
2018-03-03 14:00:48 +01:00
Mladen Pejaković
9979b160c7 Translated using Weblate (Serbian)
Currently translated at 76.1% (242 of 318 strings)
2018-03-03 14:00:39 +01:00
65b332c395 Translated using Weblate (Serbian)
Currently translated at 76.1% (242 of 318 strings)
2018-03-03 13:59:43 +01:00
Mladen Pejaković
b867ac8cc7 Translated using Weblate (Serbian)
Currently translated at 75.7% (241 of 318 strings)
2018-03-03 13:59:38 +01:00
Enol P
9a0ff24ffe Translated using Weblate (Asturian)
Currently translated at 97.4% (310 of 318 strings)
2018-03-03 03:34:28 +01:00
Eduardo Caron
9540a12b6f Translated using Weblate (Portuguese (Brazil))
Currently translated at 99.0% (315 of 318 strings)
2018-03-03 01:41:17 +01:00
wb9688
fdfb53c05c Update README.md 2018-03-02 10:10:15 +01:00
HardLight
3ec979cc40 Translated using Weblate (Lithuanian)
Currently translated at 100.0% (318 of 318 strings)
2018-03-01 22:38:41 +01:00
John Zhen Mo
9ea08c8a4b -Re-added loading for items prior to current index in MediaSourceManager to allow faster access time.
-Added some null checks annotation.
2018-02-28 23:25:45 -08:00
Digiwizkid
629eada5c3 Translated using Weblate (Bengali (Bangladesh))
Currently translated at 40.5% (129 of 318 strings)
2018-03-01 04:34:43 +01:00
John Zhen Mo
a1220c77da -Added serialized cache for transferring serializable objects too large for intent transactions.
-Fixed potential transaction too large exceptions for player intents.
2018-02-28 17:47:12 -08:00
John Zhen Mo
b4668367c6 -Added better assertions and documentations to new mechanism in MediaSourceManager.
-Modified LoadController to allow fast playback start and increased buffer zigzag window.
-Removed unnecessary loading on timeline changes.
-Changed select message in MediaSourceManager to cause immediate load.
-Reduced default expiration time in MediaSourceManager.
-Fixed main video player not showing end time on audio-only streams.
-Fixed live stream has player view disabled after transitioning from audio stream.
-Fixed inconsistent progress bar height between live and non-live video on main player.
2018-02-28 17:45:05 -08:00
John Zhen Mo
77da40e507 -Added perpetual extractor source loading on network failures.
-Fixed play queue playlist desynchronization caused by media source manager window loading expansion on sublist prior to current item.
-Fixed failed media source not treated as ready for playback.
2018-02-26 22:37:19 -08:00
John Zhen Mo
b3b2748bb7 -Improved player queue stability by using more aggressive synchronization policy.
-Added sync buttons on live streams to allow seeking to live edge.
-Added custom cache key for extractor sources to allow more persistent reuse.
-Refactored player data source factories into own class and separating live and non-live data sources.
2018-02-26 19:57:59 -08:00
ezjerry liao
26e8143616 Translated using Weblate (Chinese (Traditional))
Currently translated at 100.0% (318 of 318 strings)
2018-02-26 14:30:19 +01:00
John Zhen Mo
1444fe5468 -Fixed potential NPE when obtaining broadcast receiver.
-Extracted expiration time in media source manager.
-Re-enabled long click on live stream info items.
-Fixed dash source building to use mpd instead of extractor.
2018-02-25 20:12:30 -08:00
John Zhen Mo
ac431e3ece -Fixed failed media source not treated as ready. 2018-02-25 15:32:25 -08:00
John Zhen Mo
563a4137bd -Fixed inconsistent audio focus state when audio becomes noisy (e.g. headset unplugged).
-Fixed live media sources failing when using cached data source by introducing
cacheless data sources.
-Added custom track selector to circumvent ExoPlayer's language normalization NPE.
-Updated Extractor to correctly load live streams.
-Removed deprecated deferred media source and media source manager.
-Removed Livestream exceptions.
2018-02-25 15:10:11 -08:00
John Zhen Mo
19cbcd0c1d -Fixed media source update index check.
-Fixed media source manager excessive loading.
-Remove unneeded fields in loaded media source.
2018-02-24 21:54:47 -08:00
John Zhen Mo
8803b60b28 -Updated Exoplayer to 2.7.0.
-PoC for new seamless stream loading mechanism.
2018-02-24 21:54:47 -08:00
Weblate
e9f59ae769 Merge remote-tracking branch 'origin/dev' into dev 2018-02-24 23:37:04 +01:00
Matej U
5cf3bee336 Translated using Weblate (Slovenian)
Currently translated at 77.3% (246 of 318 strings)
2018-02-24 23:37:04 +01:00
Maciej Gamrat
060fe835c7 Translated using Weblate (Polish)
Currently translated at 100.0% (318 of 318 strings)
2018-02-24 23:37:02 +01:00
Toldi Balázs
2688ea8f59 Translated using Weblate (Hungarian)
Currently translated at 44.6% (142 of 318 strings)
2018-02-24 23:37:01 +01:00
Arun Negi
8eb61cf752 Translated using Weblate (Hindi)
Currently translated at 99.6% (317 of 318 strings)
2018-02-24 23:36:56 +01:00
Tobias Groza
6628901d46 Translated using Weblate (German)
Currently translated at 94.6% (301 of 318 strings)
2018-02-24 23:36:52 +01:00
Christian Schabesberger
5a31882be3 Merge branch 'dev' of github.com:teamnewpipe/NewPipe into dev 2018-02-24 22:57:34 +01:00
Christian Schabesberger
a42da09d6c make NewPipe compatible with latest Extractor refactorings 2018-02-24 22:57:25 +01:00
nieko-nera
658cf5c873 Translated using Weblate (Lithuanian)
Currently translated at 100.0% (318 of 318 strings)
2018-02-24 19:38:45 +01:00
Coffeemaker
b8c752b740 Translated using Weblate (Polish)
Currently translated at 100.0% (318 of 318 strings)
2018-02-24 18:51:39 +01:00
Maciej Gamrat
9151ae7081 Translated using Weblate (Polish)
Currently translated at 100.0% (318 of 318 strings)
2018-02-24 18:51:28 +01:00
a1a894f722 Translated using Weblate (Polish)
Currently translated at 100.0% (318 of 318 strings)
2018-02-24 18:50:22 +01:00
Maciej Gamrat
3352ee3151 Translated using Weblate (Polish)
Currently translated at 100.0% (318 of 318 strings)
2018-02-24 18:49:38 +01:00
Maciej Gamrat
da3533a430 Translated using Weblate (Polish)
Currently translated at 100.0% (318 of 318 strings)
2018-02-24 18:48:41 +01:00
0bb7f9becf Translated using Weblate (Polish)
Currently translated at 83.0% (264 of 318 strings)
2018-02-24 18:02:21 +01:00
Maciej Gamrat
40c64ee2d8 Translated using Weblate (Polish)
Currently translated at 83.0% (264 of 318 strings)
2018-02-24 18:01:21 +01:00
r2308145
66651f7111 Translated using Weblate (Czech)
Currently translated at 100.0% (318 of 318 strings)
2018-02-24 13:43:02 +01:00
Freddy Morán Jr
c7d0bd5dec Translated using Weblate (Spanish)
Currently translated at 100.0% (318 of 318 strings)
2018-02-24 03:40:58 +01:00
9ce0a9d49d Translated using Weblate (Hungarian)
Currently translated at 30.5% (97 of 318 strings)
2018-02-23 22:54:03 +01:00
Toldi Balázs
2070b353c2 Translated using Weblate (Hungarian)
Currently translated at 30.1% (96 of 318 strings)
2018-02-23 22:52:47 +01:00
1d91f3b91b Translated using Weblate (Hungarian)
Currently translated at 29.5% (94 of 318 strings)
2018-02-23 22:52:04 +01:00
Toldi Balázs
8b0aebfddb Translated using Weblate (Hungarian)
Currently translated at 29.2% (93 of 318 strings)
2018-02-23 22:51:13 +01:00
467905d7b0 Translated using Weblate (Hungarian)
Currently translated at 29.2% (93 of 318 strings)
2018-02-23 22:50:48 +01:00
Toldi Balázs
cc81921bcb Translated using Weblate (Hungarian)
Currently translated at 28.9% (92 of 318 strings)
2018-02-23 22:50:36 +01:00
8600c04ff3 Translated using Weblate (Hungarian)
Currently translated at 28.6% (91 of 318 strings)
2018-02-23 22:50:00 +01:00
Toldi Balázs
3f31445f12 Translated using Weblate (Hungarian)
Currently translated at 27.3% (87 of 318 strings)
2018-02-23 22:49:15 +01:00
8a33371f37 Translated using Weblate (Hungarian)
Currently translated at 26.7% (85 of 318 strings)
2018-02-23 22:48:01 +01:00
Toldi Balázs
1164bd7183 Translated using Weblate (Hungarian)
Currently translated at 25.7% (82 of 318 strings)
2018-02-23 22:40:53 +01:00
589fcd09c0 Translated using Weblate (Hungarian)
Currently translated at 25.4% (81 of 318 strings)
2018-02-23 22:40:37 +01:00
Weblate
c5d49016d4 Merge remote-tracking branch 'origin/dev' into dev 2018-02-23 18:48:55 +01:00
Freddy Morán Jr
8886b12151 Translated using Weblate (Spanish)
Currently translated at 99.0% (315 of 318 strings)
2018-02-23 18:48:54 +01:00
nieko-nera
336ffd7cf0 Translated using Weblate (Lithuanian)
Currently translated at 100.0% (318 of 318 strings)
2018-02-23 18:48:50 +01:00
Christian Schabesberger
3088778a9f Merge pull request #1136 from wb9688/okhttp-uil
Use OkHttp with UIL
2018-02-23 18:06:13 +01:00
E T
4c1de83b24 Translated using Weblate (Turkish)
Currently translated at 100.0% (318 of 318 strings)
2018-02-23 11:43:24 +01:00
Emanuele Petriglia
ee2fbfc2d1 Translated using Weblate (Italian)
Currently translated at 100.0% (318 of 318 strings)
2018-02-23 09:46:40 +01:00
Heimen Stoffels
dff7fe722b Translated using Weblate (Dutch)
Currently translated at 100.0% (318 of 318 strings)
2018-02-22 22:48:45 +01:00
Weblate
eb6dac2e9f Merge remote-tracking branch 'origin/dev' into dev 2018-02-22 22:41:15 +01:00
Oleh Ilnytskyi
255760de16 Translated using Weblate (Ukrainian)
Currently translated at 69.8% (220 of 315 strings)
2018-02-22 22:41:09 +01:00
wb9688
a2373b817a Use OkHttp with UIL 2018-02-22 13:25:56 +01:00
Christian Schabesberger
7d4c45c4c0 Merge tag 'v0.11.6-beta'
v0.11.6-beta
2018-02-21 11:38:48 +01:00
245 changed files with 11276 additions and 3190 deletions

View File

@@ -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.

View File

@@ -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.

View File

@@ -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);
}
}
}

View File

@@ -59,18 +59,17 @@ NewPipe does not use any Google framework libraries, or the YouTube API. It only
* Search/Watch Playlists
* Watch as queues Playlists
* Queuing videos
* Local playlists
* Subtitles
* Multi-service support (eg. SoundCloud in NewPipe Beta)
### Coming Features
* Multiservice support (eg. SoundCloud)
* Bookmarks
* Subtitles support
* livestream support
* Livestream support
* Cast to UPnP and Cast
* Show comments
* ... and many more
### Multiservice support
Although NewPipe only supports YouTube at the moment, it's designed to support many more streaming services. The plan is, that NewPipe will get such support by the version 2.0.
## Contribution
Whether you have ideas, translations, design changes, code cleaning, or real heavy code changes, help is always welcome.
The more is done the better it gets!
@@ -78,19 +77,24 @@ The more is done the better it gets!
If you'd like to get involved, check our [contribution notes](.github/CONTRIBUTING.md).
## Donate
If you like NewPipe we'd be happy about a donation. You can either donate via Bitcoin or BountySource. For further information about donating to NewPipe, please visit our [website](https://newpipe.schabi.org/donate/).
If you like NewPipe we'd be happy about a donation. You can either donate via Bitcoin, Bountysource or Liberapay. For further information about donating to NewPipe, please visit our [website](https://newpipe.schabi.org/donate).
<table>
<tr>
<td><img src="https://bitcoin.org/img/icons/logotop.svg" alt="Bitcoin" /></td>
<td><img src="assets/bitcoin_qr_code.png" alt="Bitcoin QR Code" width="100px"/></td>
<td><samp>16A9J59ahMRqkLSZjhYj33n9j3fMztFxnh</samp></td>
</tr>
<tr>
<td><a href="https://www.bountysource.com/teams/newpipe"><img src="https://upload.wikimedia.org/wikipedia/commons/thumb/2/22/Bountysource.png/320px-Bountysource.png" alz="Bountysource" width="190px" /></a></td>
<td><a href="https://www.bountysource.com/teams/newpipe"><img src="assets/bountysource_qr_code.png" alt="Visit NewPipe at bountysource.com" width="100px"/></a></td>
<td><a href="https://www.bountysource.com/teams/newpipe/issues"><img src="https://img.shields.io/bountysource/team/newpipe/activity.svg?colorB=cd201f" height="30px" alt="Check out how many bounties you can earn." /></a></td>
</tr>
<tr>
<td><img src="https://bitcoin.org/img/icons/logotop.svg" alt="Bitcoin" /></td>
<td><img src="assets/bitcoin_qr_code.png" alt="Bitcoin QR Code" width="100px"/></td>
<td><samp>16A9J59ahMRqkLSZjhYj33n9j3fMztFxnh</samp></td>
</tr>
<tr>
<td><a href="https://liberapay.com/TeamNewPipe/"><img src="https://upload.wikimedia.org/wikipedia/commons/2/27/Liberapay_logo_v2_white-on-yellow.svg" alt="Liberapay" width="80px" /></a></td>
<td><a href="https://liberapay.com/TeamNewPipe/"><img src="assets/liberapay_qr_code.png" alt="Visit NewPipe at liberapay.com" width="100px"/></a></td>
<td><a href="https://liberapay.com/TeamNewPipe/donate"><img src="assets/liberapay_donate_button.svg" alt="Donate via Liberapay" height="35px" /></a></td>
</tr>
<tr>
<td><a href="https://www.bountysource.com/teams/newpipe"><img src="https://upload.wikimedia.org/wikipedia/commons/thumb/2/22/Bountysource.png/320px-Bountysource.png" alt="Bountysource" width="190px" /></a></td>
<td><a href="https://www.bountysource.com/teams/newpipe"><img src="assets/bountysource_qr_code.png" alt="Visit NewPipe at bountysource.com" width="100px"/></a></td>
<td><a href="https://www.bountysource.com/teams/newpipe/issues"><img src="https://img.shields.io/bountysource/team/newpipe/activity.svg?colorB=cd201f" height="30px" alt="Check out how many bounties you can earn." /></a></td>
</tr>
</table>
## License

View File

@@ -2,14 +2,14 @@ apply plugin: 'com.android.application'
android {
compileSdkVersion 27
buildToolsVersion '27.0.1'
buildToolsVersion '27.0.3'
defaultConfig {
applicationId "org.schabi.newpipe"
minSdkVersion 15
targetSdkVersion 27
versionCode 48
versionName "0.12.0"
versionCode 61
versionName "0.13.2"
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
vectorDrawables.useSupportLibrary = true
@@ -26,13 +26,6 @@ android {
debuggable true
applicationIdSuffix ".debug"
}
beta {
minifyEnabled true
shrinkResources true
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
applicationIdSuffix ".beta"
}
}
lintOptions {
@@ -48,14 +41,21 @@ android {
}
ext {
supportLibVersion = '27.0.2'
supportLibVersion = '27.1.0'
exoPlayerLibVersion = '2.7.3'
roomDbLibVersion = '1.0.0'
leakCanaryLibVersion = '1.5.4'
okHttpLibVersion = '1.5.0'
icepickLibVersion = '3.2.0'
stethoLibVersion = '1.5.0'
}
dependencies {
androidTestImplementation('com.android.support.test.espresso:espresso-core:2.2.2') {
exclude module: 'support-annotations'
}
implementation 'com.github.TeamNewPipe:NewPipeExtractor:7716b1437815'
implementation 'com.github.TeamNewPipe:NewPipeExtractor:77a74b8'
testImplementation 'junit:junit:4.12'
testImplementation 'org.mockito:mockito-core:1.10.19'
@@ -72,28 +72,29 @@ dependencies {
implementation 'com.nostra13.universalimageloader:universal-image-loader:1.9.5'
implementation 'de.hdodenhof:circleimageview:2.2.0'
implementation 'com.github.nirhart:ParallaxScroll:dd53d1f9d1'
implementation 'com.nononsenseapps:filepicker:3.0.1'
implementation 'com.google.android.exoplayer:exoplayer:2.6.0'
implementation 'com.nononsenseapps:filepicker:4.2.1'
debugImplementation 'com.facebook.stetho:stetho:1.5.0'
debugImplementation 'com.facebook.stetho:stetho-urlconnection:1.5.0'
debugImplementation 'com.android.support:multidex:1.0.2'
implementation "com.google.android.exoplayer:exoplayer:$exoPlayerLibVersion"
implementation "com.google.android.exoplayer:extension-mediasession:$exoPlayerLibVersion"
implementation 'io.reactivex.rxjava2:rxjava:2.1.7'
implementation 'io.reactivex.rxjava2:rxandroid:2.0.1'
implementation 'com.jakewharton.rxbinding2:rxbinding:2.0.0'
debugImplementation "com.facebook.stetho:stetho:$stethoLibVersion"
debugImplementation "com.facebook.stetho:stetho-urlconnection:$stethoLibVersion"
debugImplementation 'com.android.support:multidex:1.0.3'
implementation 'android.arch.persistence.room:runtime:1.0.0'
implementation 'android.arch.persistence.room:rxjava2:1.0.0'
annotationProcessor 'android.arch.persistence.room:compiler:1.0.0'
implementation 'io.reactivex.rxjava2:rxjava:2.1.10'
implementation 'io.reactivex.rxjava2:rxandroid:2.0.2'
implementation 'com.jakewharton.rxbinding2:rxbinding:2.1.1'
implementation 'frankiesardo:icepick:3.2.0'
annotationProcessor 'frankiesardo:icepick-processor:3.2.0'
implementation "android.arch.persistence.room:runtime:$roomDbLibVersion"
implementation "android.arch.persistence.room:rxjava2:$roomDbLibVersion"
annotationProcessor "android.arch.persistence.room:compiler:$roomDbLibVersion"
debugImplementation 'com.squareup.leakcanary:leakcanary-android:1.5.4'
betaImplementation 'com.squareup.leakcanary:leakcanary-android-no-op:1.5.4'
releaseImplementation 'com.squareup.leakcanary:leakcanary-android-no-op:1.5.4'
implementation "frankiesardo:icepick:$icepickLibVersion"
annotationProcessor "frankiesardo:icepick-processor:$icepickLibVersion"
debugImplementation "com.squareup.leakcanary:leakcanary-android:$leakCanaryLibVersion"
releaseImplementation "com.squareup.leakcanary:leakcanary-android-no-op:$leakCanaryLibVersion"
implementation 'com.squareup.okhttp3:okhttp:3.9.1'
debugImplementation 'com.facebook.stetho:stetho-okhttp3:1.5.0'
debugImplementation "com.facebook.stetho:stetho-okhttp3:$okHttpLibVersion"
}

View File

@@ -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

View File

@@ -28,6 +28,12 @@
</intent-filter>
</activity>
<receiver android:name="android.support.v4.media.session.MediaButtonReceiver" >
<intent-filter>
<action android:name="android.intent.action.MEDIA_BUTTON" />
</intent-filter>
</receiver>
<activity
android:name=".player.old.PlayVideoActivity"
android:configChanges="orientation|keyboardHidden|screenSize"
@@ -36,7 +42,11 @@
<service
android:name=".player.BackgroundPlayer"
android:exported="false"/>
android:exported="false">
<intent-filter>
<action android:name="android.intent.action.MEDIA_BUTTON" />
</intent-filter>
</service>
<activity
android:name=".player.BackgroundPlayerActivity"
@@ -70,6 +80,9 @@
android:name=".history.HistoryActivity"
android:label="@string/title_activity_history"/>
<service android:name=".subscription.services.SubscriptionsImportService"/>
<service android:name=".subscription.services.SubscriptionsExportService"/>
<activity
android:name=".PanicResponderActivity"
android:launchMode="singleInstance"
@@ -117,13 +130,13 @@
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/provider_paths"/>
android:resource="@xml/nnf_provider_paths"/>
</provider>
<activity
android:name=".RouterActivity"
android:excludeFromRecents="true"
android:label="@string/preferred_player_share_menu_title"
android:label="@string/preferred_open_action_share_menu_title"
android:taskAffinity=""
android:theme="@style/RouterActivityThemeDark">

View File

@@ -89,7 +89,6 @@ public class App extends Application {
SettingsActivity.initSettings(this);
NewPipe.init(getDownloader());
NewPipeDatabase.init(this);
StateSaver.init(this);
initNotificationChannel();
@@ -168,6 +167,7 @@ public class App extends Application {
return new ImageLoaderConfiguration.Builder(this)
.memoryCache(new LRULimitedMemoryCache(memoryCacheSizeMb * 1024 * 1024))
.diskCacheSize(diskCacheSizeMb * 1024 * 1024)
.imageDownloader(new ImageDownloader(getApplicationContext()))
.build();
}

View File

@@ -1,18 +1,14 @@
package org.schabi.newpipe;
import android.content.Context;
import android.content.res.TypedArray;
import android.os.Bundle;
import android.support.annotation.AttrRes;
import android.support.annotation.NonNull;
import android.support.v4.app.Fragment;
import android.support.v7.app.AppCompatActivity;
import android.util.Log;
import android.view.View;
import com.nostra13.universalimageloader.core.DisplayImageOptions;
import com.nostra13.universalimageloader.core.ImageLoader;
import com.nostra13.universalimageloader.core.display.FadeInBitmapDisplayer;
import com.squareup.leakcanary.RefWatcher;
import icepick.Icepick;
@@ -87,33 +83,13 @@ public abstract class BaseFragment extends Fragment {
}
/*//////////////////////////////////////////////////////////////////////////
// DisplayImageOptions default configurations
// Utils
//////////////////////////////////////////////////////////////////////////*/
public static final DisplayImageOptions BASE_OPTIONS =
new DisplayImageOptions.Builder().cacheInMemory(true).build();
public static final DisplayImageOptions DISPLAY_AVATAR_OPTIONS =
new DisplayImageOptions.Builder()
.cloneFrom(BASE_OPTIONS)
.showImageOnLoading(R.drawable.buddy)
.showImageForEmptyUri(R.drawable.buddy)
.showImageOnFail(R.drawable.buddy)
.build();
public static final DisplayImageOptions DISPLAY_THUMBNAIL_OPTIONS =
new DisplayImageOptions.Builder()
.cloneFrom(BASE_OPTIONS)
.displayer(new FadeInBitmapDisplayer(250))
.showImageForEmptyUri(R.drawable.dummy_thumbnail)
.showImageOnFail(R.drawable.dummy_thumbnail)
.build();
public static final DisplayImageOptions DISPLAY_BANNER_OPTIONS =
new DisplayImageOptions.Builder()
.cloneFrom(BASE_OPTIONS)
.showImageOnLoading(R.drawable.channel_banner)
.showImageForEmptyUri(R.drawable.channel_banner)
.showImageOnFail(R.drawable.channel_banner)
.build();
public void setTitle(String title) {
if (DEBUG) Log.d(TAG, "setTitle() called with: title = [" + title + "]");
if (activity != null && activity.getSupportActionBar() != null) {
activity.getSupportActionBar().setTitle(title);
}
}
}

View File

@@ -6,6 +6,7 @@ import android.text.TextUtils;
import org.schabi.newpipe.extractor.exceptions.ReCaptchaException;
import java.io.IOException;
import java.io.InputStream;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
@@ -72,6 +73,31 @@ public class Downloader implements org.schabi.newpipe.extractor.Downloader {
mCookies = cookies;
}
/**
* Get the size of the content that the url is pointing by firing a HEAD request.
*
* @param url an url pointing to the content
* @return the size of the content, in bytes
*/
public long getContentLength(String url) throws IOException {
Response response = null;
try {
final Request request = new Request.Builder()
.head().url(url)
.addHeader("User-Agent", USER_AGENT)
.build();
response = client.newCall(request).execute();
return Long.parseLong(response.header("Content-Length"));
} catch (NumberFormatException e) {
throw new IOException("Invalid content length", e);
} finally {
if (response != null) {
response.close();
}
}
}
/**
* Download the text file at the supplied URL as in download(String),
* but set the HTTP header field "Accept-Language" to the supplied string.
@@ -98,6 +124,18 @@ public class Downloader implements org.schabi.newpipe.extractor.Downloader {
*/
@Override
public String download(String siteUrl, Map<String, String> customProperties) throws IOException, ReCaptchaException {
return getBody(siteUrl, customProperties).string();
}
public InputStream stream(String siteUrl) throws IOException {
try {
return getBody(siteUrl, Collections.emptyMap()).byteStream();
} catch (ReCaptchaException e) {
throw new IOException(e.getMessage(), e.getCause());
}
}
private ResponseBody getBody(String siteUrl, Map<String, String> customProperties) throws IOException, ReCaptchaException {
final Request.Builder requestBuilder = new Request.Builder()
.method("GET", null).url(siteUrl)
.addHeader("User-Agent", USER_AGENT);
@@ -123,7 +161,7 @@ public class Downloader implements org.schabi.newpipe.extractor.Downloader {
return null;
}
return body.string();
return body;
}
/**

View File

@@ -0,0 +1,46 @@
package org.schabi.newpipe;
import android.annotation.SuppressLint;
import android.content.Context;
import android.content.SharedPreferences;
import android.content.res.Resources;
import android.preference.PreferenceManager;
import com.nostra13.universalimageloader.core.download.BaseImageDownloader;
import org.schabi.newpipe.extractor.NewPipe;
import java.io.IOException;
import java.io.InputStream;
public class ImageDownloader extends BaseImageDownloader {
private final Resources resources;
private final SharedPreferences preferences;
private final String downloadThumbnailKey;
public ImageDownloader(Context context) {
super(context);
this.resources = context.getResources();
this.preferences = PreferenceManager.getDefaultSharedPreferences(context);
this.downloadThumbnailKey = context.getString(R.string.download_thumbnail_key);
}
private boolean isDownloadingThumbnail() {
return preferences.getBoolean(downloadThumbnailKey, true);
}
@SuppressLint("ResourceType")
@Override
public InputStream getStream(String imageUri, Object extra) throws IOException {
if (isDownloadingThumbnail()) {
return super.getStream(imageUri, extra);
} else {
return resources.openRawResource(R.drawable.dummy_thumbnail_dark);
}
}
protected InputStream getStreamFromNetwork(String imageUri, Object extra) throws IOException {
final Downloader downloader = (Downloader) NewPipe.getDownloader();
return downloader.stream(imageUri);
}
}

View File

@@ -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;
}
@@ -230,6 +235,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>

View File

@@ -11,31 +11,32 @@ import static org.schabi.newpipe.database.Migrations.MIGRATION_11_12;
public final class NewPipeDatabase {
private static AppDatabase databaseInstance;
private static volatile AppDatabase databaseInstance;
private NewPipeDatabase() {
//no instance
}
public static void init(Context context) {
databaseInstance = Room
.databaseBuilder(context, AppDatabase.class, DATABASE_NAME)
private static AppDatabase getDatabase(Context context) {
return Room
.databaseBuilder(context.getApplicationContext(), AppDatabase.class, DATABASE_NAME)
.addMigrations(MIGRATION_11_12)
.fallbackToDestructiveMigration()
.build();
}
@NonNull
@Deprecated
public static AppDatabase getInstance() {
if (databaseInstance == null) throw new RuntimeException("Database not initialized");
public static AppDatabase getInstance(@NonNull Context context) {
AppDatabase result = databaseInstance;
if (result == null) {
synchronized (NewPipeDatabase.class) {
result = databaseInstance;
if (result == null) {
databaseInstance = (result = getDatabase(context));
}
}
}
return databaseInstance;
}
@NonNull
public static AppDatabase getInstance(Context context) {
if (databaseInstance == null) init(context);
return databaseInstance;
return result;
}
}

View File

@@ -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;
@@ -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,7 @@ import io.reactivex.disposables.Disposable;
import io.reactivex.functions.Consumer;
import io.reactivex.schedulers.Schedulers;
import static org.schabi.newpipe.extractor.StreamingService.ServiceInfo.MediaCapability.*;
import static org.schabi.newpipe.util.ThemeHelper.resolveResourceIdFromAttr;
/**
@@ -122,7 +125,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);
}
@@ -168,18 +171,23 @@ public class RouterActivity extends AppCompatActivity {
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 playerChoiceKey = preferences.getString(
getString(R.string.preferred_open_action_key),
getString(R.string.preferred_open_action_default));
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)) {
final List<StreamingService.ServiceInfo.MediaCapability> capabilities = currentService.getServiceInfo().getMediaCapabilities();
boolean serviceSupportsPlayer = false;
if (playerChoiceKey.equals(videoPlayerKey) || playerChoiceKey.equals(popupPlayerKey)) {
serviceSupportsPlayer = capabilities.contains(VIDEO);
} else if (playerChoiceKey.equals(backgroundPlayerKey)) {
serviceSupportsPlayer = capabilities.contains(AUDIO);
}
if (playerChoiceKey.equals(alwaysAskKey) || !serviceSupportsPlayer) {
showDialog();
} else {
handleChoice(playerChoiceKey);
@@ -187,29 +195,20 @@ public class RouterActivity extends AppCompatActivity {
}
private void showDialog() {
SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(this);
final ContextThemeWrapper themeWrapper = new ContextThemeWrapper(this,
final SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(this);
final ContextThemeWrapper themeWrapperContext = new ContextThemeWrapper(this,
ThemeHelper.isLightThemeSelected(this) ? R.style.LightTheme : R.style.DarkTheme);
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 List<AdapterChoiceItem> choices = getChoicesForService(themeWrapperContext, currentService);
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 +217,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 +226,7 @@ public class RouterActivity extends AppCompatActivity {
.setOnDismissListener((dialog) -> finish())
.create();
//noinspection CodeBlock2Expr
alertDialog.setOnShowListener(dialog -> {
setDialogButtonsState(alertDialog, radioGroup.getCheckedRadioButtonId() != -1);
});
@@ -240,7 +240,7 @@ public class RouterActivity extends AppCompatActivity {
selectedRadioPosition = indexOfChild;
if (selectedPreviously == selectedRadioPosition) {
handleChoice(choices[selectedRadioPosition].key);
handleChoice(choices.get(selectedRadioPosition).key);
}
};
@@ -259,8 +259,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 +269,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 +278,28 @@ public class RouterActivity extends AppCompatActivity {
alertDialog.show();
}
private List<AdapterChoiceItem> getChoicesForService(Context context, StreamingService service) {
final List<AdapterChoiceItem> returnList = new ArrayList<>();
final List<StreamingService.ServiceInfo.MediaCapability> capabilities = service.getServiceInfo().getMediaCapabilities();
returnList.add(new AdapterChoiceItem(getString(R.string.show_info_key), getString(R.string.show_info),
resolveResourceIdFromAttr(context, R.attr.info)));
if (capabilities.contains(VIDEO)) {
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)) {
returnList.add(new AdapterChoiceItem(getString(R.string.background_player_key), getString(R.string.background_player),
resolveResourceIdFromAttr(context, R.attr.audio)));
}
return returnList;
}
private void setDialogButtonsState(AlertDialog dialog, boolean state) {
final Button negativeButton = dialog.getButton(DialogInterface.BUTTON_NEGATIVE);
final Button positiveButton = dialog.getButton(DialogInterface.BUTTON_POSITIVE);
@@ -288,16 +310,14 @@ public class RouterActivity extends AppCompatActivity {
}
private void handleChoice(final String playerChoiceKey) {
if (Arrays.asList(getResources()
.getStringArray(R.array.preferred_open_action_values_list))
.contains(playerChoiceKey)) {
final List<String> validChoicesList = Arrays.asList(getResources().getStringArray(R.array.preferred_open_action_values_list));
if (validChoicesList.contains(playerChoiceKey)) {
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), playerChoiceKey)
.apply();
}
if (playerChoiceKey.equals(getString(R.string.popup_player_key))
&& !PermissionHelper.isPopupEnabled(this)) {
if (playerChoiceKey.equals(getString(R.string.popup_player_key)) && !PermissionHelper.isPopupEnabled(this)) {
PermissionHelper.showPopupEnablementToast(this);
finish();
return;
@@ -305,7 +325,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 (playerChoiceKey.equals(getString(R.string.show_info_key))) {
disposables.add(Observable
.fromCallable(() -> NavigationHelper.getIntentByLink(this, currentUrl))
.subscribeOn(Schedulers.io())
@@ -322,11 +342,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, playerChoiceKey);
intent.putExtra(FetcherService.KEY_CHOICE, choice);
startService(intent);
finish();
@@ -334,8 +351,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 +570,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}");

View File

@@ -71,14 +71,14 @@ public class StreamEntity implements Serializable {
@Ignore
public StreamEntity(final StreamInfoItem item) {
this(item.service_id, item.name, item.url, item.stream_type, item.thumbnail_url,
item.uploader_name, item.duration);
this(item.getServiceId(), item.getName(), item.getUrl(), item.getStreamType(), item.getThumbnailUrl(),
item.getUploaderName(), item.getDuration());
}
@Ignore
public StreamEntity(final StreamInfo info) {
this(info.service_id, info.name, info.url, info.stream_type, info.thumbnail_url,
info.uploader_name, info.duration);
this(info.getServiceId(), info.getName(), info.getUrl(), info.getStreamType(), info.getThumbnailUrl(),
info.getUploaderName(), info.getDuration());
}
@Ignore

View File

@@ -1,7 +1,10 @@
package org.schabi.newpipe.database.subscription;
import android.arch.persistence.room.Dao;
import android.arch.persistence.room.Insert;
import android.arch.persistence.room.OnConflictStrategy;
import android.arch.persistence.room.Query;
import android.arch.persistence.room.Transaction;
import org.schabi.newpipe.database.BasicDAO;
@@ -11,24 +14,56 @@ import io.reactivex.Flowable;
import static org.schabi.newpipe.database.subscription.SubscriptionEntity.SUBSCRIPTION_SERVICE_ID;
import static org.schabi.newpipe.database.subscription.SubscriptionEntity.SUBSCRIPTION_TABLE;
import static org.schabi.newpipe.database.subscription.SubscriptionEntity.SUBSCRIPTION_UID;
import static org.schabi.newpipe.database.subscription.SubscriptionEntity.SUBSCRIPTION_URL;
@Dao
public interface SubscriptionDAO extends BasicDAO<SubscriptionEntity> {
public abstract class SubscriptionDAO implements BasicDAO<SubscriptionEntity> {
@Override
@Query("SELECT * FROM " + SUBSCRIPTION_TABLE)
Flowable<List<SubscriptionEntity>> getAll();
public abstract Flowable<List<SubscriptionEntity>> getAll();
@Override
@Query("DELETE FROM " + SUBSCRIPTION_TABLE)
int deleteAll();
public abstract int deleteAll();
@Override
@Query("SELECT * FROM " + SUBSCRIPTION_TABLE + " WHERE " + SUBSCRIPTION_SERVICE_ID + " = :serviceId")
Flowable<List<SubscriptionEntity>> listByService(int serviceId);
public abstract Flowable<List<SubscriptionEntity>> listByService(int serviceId);
@Query("SELECT * FROM " + SUBSCRIPTION_TABLE + " WHERE " +
SUBSCRIPTION_URL + " LIKE :url AND " +
SUBSCRIPTION_SERVICE_ID + " = :serviceId")
Flowable<List<SubscriptionEntity>> getSubscription(int serviceId, String url);
public abstract Flowable<List<SubscriptionEntity>> getSubscription(int serviceId, String url);
@Query("SELECT " + SUBSCRIPTION_UID + " FROM " + SUBSCRIPTION_TABLE + " WHERE " +
SUBSCRIPTION_URL + " LIKE :url AND " +
SUBSCRIPTION_SERVICE_ID + " = :serviceId")
abstract Long getSubscriptionIdInternal(int serviceId, String url);
@Insert(onConflict = OnConflictStrategy.IGNORE)
abstract Long insertInternal(final SubscriptionEntity entities);
@Transaction
public List<SubscriptionEntity> upsertAll(List<SubscriptionEntity> entities) {
for (SubscriptionEntity entity : entities) {
Long uid = insertInternal(entity);
if (uid != -1) {
entity.setUid(uid);
continue;
}
uid = getSubscriptionIdInternal(entity.getServiceId(), entity.getUrl());
entity.setUid(uid);
if (uid == -1) {
throw new IllegalStateException("Invalid subscription id (-1)");
}
update(entity);
}
return entities;
}
}

View File

@@ -5,7 +5,9 @@ import android.arch.persistence.room.Entity;
import android.arch.persistence.room.Ignore;
import android.arch.persistence.room.Index;
import android.arch.persistence.room.PrimaryKey;
import android.support.annotation.NonNull;
import org.schabi.newpipe.extractor.channel.ChannelInfo;
import org.schabi.newpipe.extractor.channel.ChannelInfoItem;
import org.schabi.newpipe.util.Constants;
@@ -17,6 +19,7 @@ import static org.schabi.newpipe.database.subscription.SubscriptionEntity.SUBSCR
indices = {@Index(value = {SUBSCRIPTION_SERVICE_ID, SUBSCRIPTION_URL}, unique = true)})
public class SubscriptionEntity {
final static String SUBSCRIPTION_UID = "uid";
final static String SUBSCRIPTION_TABLE = "subscriptions";
final static String SUBSCRIPTION_SERVICE_ID = "service_id";
final static String SUBSCRIPTION_URL = "url";
@@ -116,9 +119,18 @@ public class SubscriptionEntity {
@Ignore
public ChannelInfoItem toChannelInfoItem() {
ChannelInfoItem item = new ChannelInfoItem(getServiceId(), getUrl(), getName());
item.thumbnail_url = getAvatarUrl();
item.subscriber_count = getSubscriberCount();
item.description = getDescription();
item.setThumbnailUrl(getAvatarUrl());
item.setSubscriberCount(getSubscriberCount());
item.setDescription(getDescription());
return item;
}
@Ignore
public static SubscriptionEntity from(@NonNull ChannelInfo info) {
SubscriptionEntity result = new SubscriptionEntity();
result.setServiceId(info.getServiceId());
result.setUrl(info.getUrl());
result.setData(info.getName(), info.getAvatarUrl(), info.getDescription(), info.getSubscriberCount());
return result;
}
}

View File

@@ -1,56 +1,61 @@
package org.schabi.newpipe.download;
import android.content.Context;
import android.os.Bundle;
import android.support.annotation.IdRes;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.v4.app.DialogFragment;
import android.support.v7.widget.Toolbar;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.widget.AdapterView;
import android.widget.ArrayAdapter;
import android.widget.EditText;
import android.widget.RadioButton;
import android.widget.RadioGroup;
import android.widget.SeekBar;
import android.widget.Spinner;
import android.widget.TextView;
import android.widget.Toast;
import org.schabi.newpipe.MainActivity;
import org.schabi.newpipe.R;
import org.schabi.newpipe.extractor.MediaFormat;
import org.schabi.newpipe.extractor.stream.AudioStream;
import org.schabi.newpipe.extractor.stream.Stream;
import org.schabi.newpipe.extractor.stream.StreamInfo;
import org.schabi.newpipe.extractor.stream.VideoStream;
import org.schabi.newpipe.fragments.detail.SpinnerToolbarAdapter;
import org.schabi.newpipe.settings.NewPipeSettings;
import org.schabi.newpipe.util.FilenameUtils;
import org.schabi.newpipe.util.ListHelper;
import org.schabi.newpipe.util.PermissionHelper;
import org.schabi.newpipe.util.StreamItemAdapter;
import org.schabi.newpipe.util.StreamItemAdapter.StreamSizeWrapper;
import org.schabi.newpipe.util.ThemeHelper;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;
import icepick.Icepick;
import icepick.State;
import io.reactivex.disposables.CompositeDisposable;
import us.shandian.giga.service.DownloadManagerService;
public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheckedChangeListener, AdapterView.OnItemSelectedListener {
private static final String TAG = "DialogFragment";
private static final boolean DEBUG = MainActivity.DEBUG;
private static final String INFO_KEY = "info_key";
private static final String SORTED_VIDEOS_LIST_KEY = "sorted_videos_list_key";
private static final String SELECTED_VIDEO_KEY = "selected_video_key";
private static final String SELECTED_AUDIO_KEY = "selected_audio_key";
@State protected StreamInfo currentInfo;
@State protected StreamSizeWrapper<AudioStream> wrappedAudioStreams = StreamSizeWrapper.empty();
@State protected StreamSizeWrapper<VideoStream> wrappedVideoStreams = StreamSizeWrapper.empty();
@State protected int selectedVideoIndex = 0;
@State protected int selectedAudioIndex = 0;
private StreamInfo currentInfo;
private ArrayList<VideoStream> sortedStreamVideosList;
private int selectedVideoIndex;
private int selectedAudioIndex;
private StreamItemAdapter<AudioStream> audioStreamsAdapter;
private StreamItemAdapter<VideoStream> videoStreamsAdapter;
private CompositeDisposable disposables = new CompositeDisposable();
private EditText nameEditText;
private Spinner streamsSpinner;
@@ -58,17 +63,50 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck
private TextView threadsCountTextView;
private SeekBar threadsSeekBar;
public static DownloadDialog newInstance(StreamInfo info, ArrayList<VideoStream> sortedStreamVideosList, int selectedVideoIndex) {
public static DownloadDialog newInstance(StreamInfo info) {
DownloadDialog dialog = new DownloadDialog();
dialog.setInfo(info, sortedStreamVideosList, selectedVideoIndex);
dialog.setStyle(DialogFragment.STYLE_NO_TITLE, 0);
dialog.setInfo(info);
return dialog;
}
private void setInfo(StreamInfo info, ArrayList<VideoStream> sortedStreamVideosList, int selectedVideoIndex) {
public static DownloadDialog newInstance(Context context, StreamInfo info) {
final ArrayList<VideoStream> streamsList = new ArrayList<>(ListHelper.getSortedStreamVideosList(context,
info.getVideoStreams(), info.getVideoOnlyStreams(), false));
final int selectedStreamIndex = ListHelper.getDefaultResolutionIndex(context, streamsList);
final DownloadDialog instance = newInstance(info);
instance.setVideoStreams(streamsList);
instance.setSelectedVideoStream(selectedStreamIndex);
instance.setAudioStreams(info.getAudioStreams());
return instance;
}
private void setInfo(StreamInfo info) {
this.currentInfo = info;
}
public void setAudioStreams(List<AudioStream> audioStreams) {
setAudioStreams(new StreamSizeWrapper<>(audioStreams));
}
public void setAudioStreams(StreamSizeWrapper<AudioStream> wrappedAudioStreams) {
this.wrappedAudioStreams = wrappedAudioStreams;
}
public void setVideoStreams(List<VideoStream> videoStreams) {
setVideoStreams(new StreamSizeWrapper<>(videoStreams));
}
public void setVideoStreams(StreamSizeWrapper<VideoStream> wrappedVideoStreams) {
this.wrappedVideoStreams = wrappedVideoStreams;
}
public void setSelectedVideoStream(int selectedVideoIndex) {
this.selectedVideoIndex = selectedVideoIndex;
this.sortedStreamVideosList = sortedStreamVideosList;
}
public void setSelectedAudioStream(int selectedAudioIndex) {
this.selectedAudioIndex = selectedAudioIndex;
}
/*//////////////////////////////////////////////////////////////////////////
@@ -79,33 +117,26 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
if (DEBUG) Log.d(TAG, "onCreate() called with: savedInstanceState = [" + savedInstanceState + "]");
if (!PermissionHelper.checkStoragePermissions(getActivity())) {
if (!PermissionHelper.checkStoragePermissions(getActivity(), PermissionHelper.DOWNLOAD_DIALOG_REQUEST_CODE)) {
getDialog().dismiss();
return;
}
if (savedInstanceState != null) {
Serializable serial = savedInstanceState.getSerializable(INFO_KEY);
if (serial instanceof StreamInfo) currentInfo = (StreamInfo) serial;
setStyle(STYLE_NO_TITLE, ThemeHelper.getDialogTheme(getContext()));
Icepick.restoreInstanceState(this, savedInstanceState);
serial = savedInstanceState.getSerializable(SORTED_VIDEOS_LIST_KEY);
if (serial instanceof ArrayList) { //noinspection unchecked
sortedStreamVideosList = (ArrayList<VideoStream>) serial;
}
selectedVideoIndex = savedInstanceState.getInt(SELECTED_VIDEO_KEY, 0);
selectedAudioIndex = savedInstanceState.getInt(SELECTED_AUDIO_KEY, 0);
}
this.videoStreamsAdapter = new StreamItemAdapter<>(getContext(), wrappedVideoStreams, true);
this.audioStreamsAdapter = new StreamItemAdapter<>(getContext(), wrappedAudioStreams);
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
if (DEBUG) Log.d(TAG, "onCreateView() called with: inflater = [" + inflater + "], container = [" + container + "], savedInstanceState = [" + savedInstanceState + "]");
return inflater.inflate(R.layout.dialog_url, container);
return inflater.inflate(R.layout.download_dialog, container);
}
@Override
public void onViewCreated(View view, @Nullable Bundle savedInstanceState) {
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
nameEditText = view.findViewById(R.id.file_name);
nameEditText.setText(FilenameUtils.createFilename(getContext(), currentInfo.getName()));
@@ -116,12 +147,12 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck
threadsCountTextView = view.findViewById(R.id.threads_count);
threadsSeekBar = view.findViewById(R.id.threads);
radioVideoAudioGroup = view.findViewById(R.id.video_audio_group);
radioVideoAudioGroup.setOnCheckedChangeListener(this);
initToolbar(view.<Toolbar>findViewById(R.id.toolbar));
checkDownloadOptions(view);
setupVideoSpinner(sortedStreamVideosList, streamsSpinner);
initToolbar(view.findViewById(R.id.toolbar));
setupDownloadOptions();
int def = 3;
threadsCountTextView.setText(String.valueOf(def));
@@ -141,15 +172,35 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck
public void onStopTrackingTouch(SeekBar p1) {
}
});
fetchStreamsSize();
}
private void fetchStreamsSize() {
disposables.clear();
disposables.add(StreamSizeWrapper.fetchSizeForWrapper(wrappedVideoStreams).subscribe(result -> {
if (radioVideoAudioGroup.getCheckedRadioButtonId() == R.id.video_button) {
setupVideoSpinner();
}
}));
disposables.add(StreamSizeWrapper.fetchSizeForWrapper(wrappedAudioStreams).subscribe(result -> {
if (radioVideoAudioGroup.getCheckedRadioButtonId() == R.id.audio_button) {
setupAudioSpinner();
}
}));
}
@Override
public void onDestroy() {
super.onDestroy();
disposables.clear();
}
@Override
public void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
outState.putSerializable(INFO_KEY, currentInfo);
outState.putSerializable(SORTED_VIDEOS_LIST_KEY, sortedStreamVideosList);
outState.putInt(SELECTED_VIDEO_KEY, selectedVideoIndex);
outState.putInt(SELECTED_AUDIO_KEY, selectedAudioIndex);
Icepick.saveInstanceState(this, outState);
}
/*//////////////////////////////////////////////////////////////////////////
@@ -161,39 +212,31 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck
toolbar.setTitle(R.string.download_dialog_title);
toolbar.setNavigationIcon(ThemeHelper.isLightThemeSelected(getActivity()) ? R.drawable.ic_arrow_back_black_24dp : R.drawable.ic_arrow_back_white_24dp);
toolbar.inflateMenu(R.menu.dialog_url);
toolbar.setNavigationOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
getDialog().dismiss();
}
});
toolbar.setNavigationOnClickListener(v -> getDialog().dismiss());
toolbar.setOnMenuItemClickListener(new Toolbar.OnMenuItemClickListener() {
@Override
public boolean onMenuItemClick(MenuItem item) {
if (item.getItemId() == R.id.okay) {
downloadSelected();
return true;
} else return false;
toolbar.setOnMenuItemClickListener(item -> {
if (item.getItemId() == R.id.okay) {
downloadSelected();
return true;
}
return false;
});
}
public void setupAudioSpinner(final List<AudioStream> audioStreams, Spinner spinner) {
String[] items = new String[audioStreams.size()];
for (int i = 0; i < audioStreams.size(); i++) {
AudioStream audioStream = audioStreams.get(i);
items[i] = audioStream.getFormat().getName() + " " + audioStream.getAverageBitrate() + "kbps";
}
private void setupAudioSpinner() {
if (getContext() == null) return;
ArrayAdapter<String> itemAdapter = new ArrayAdapter<>(getContext(), android.R.layout.simple_spinner_dropdown_item, items);
spinner.setAdapter(itemAdapter);
spinner.setSelection(selectedAudioIndex);
streamsSpinner.setAdapter(audioStreamsAdapter);
streamsSpinner.setSelection(selectedAudioIndex);
setRadioButtonsState(true);
}
public void setupVideoSpinner(final List<VideoStream> videoStreams, Spinner spinner) {
spinner.setAdapter(new SpinnerToolbarAdapter(getContext(), videoStreams, true));
spinner.setSelection(selectedVideoIndex);
private void setupVideoSpinner() {
if (getContext() == null) return;
streamsSpinner.setAdapter(videoStreamsAdapter);
streamsSpinner.setSelection(selectedVideoIndex);
setRadioButtonsState(true);
}
/*//////////////////////////////////////////////////////////////////////////
@@ -205,10 +248,10 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck
if (DEBUG) Log.d(TAG, "onCheckedChanged() called with: group = [" + group + "], checkedId = [" + checkedId + "]");
switch (checkedId) {
case R.id.audio_button:
setupAudioSpinner(currentInfo.audio_streams, streamsSpinner);
setupAudioSpinner();
break;
case R.id.video_button:
setupVideoSpinner(sortedStreamVideosList, streamsSpinner);
setupVideoSpinner();
break;
}
}
@@ -238,37 +281,53 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck
// Utils
//////////////////////////////////////////////////////////////////////////*/
protected void checkDownloadOptions(View view) {
RadioButton audioButton = view.findViewById(R.id.audio_button);
RadioButton videoButton = view.findViewById(R.id.video_button);
protected void setupDownloadOptions() {
setRadioButtonsState(false);
if (currentInfo.getAudioStreams() == null || currentInfo.getAudioStreams().size() == 0) {
audioButton.setVisibility(View.GONE);
final RadioButton audioButton = radioVideoAudioGroup.findViewById(R.id.audio_button);
final RadioButton videoButton = radioVideoAudioGroup.findViewById(R.id.video_button);
final boolean isVideoStreamsAvailable = videoStreamsAdapter.getCount() > 0;
final boolean isAudioStreamsAvailable = audioStreamsAdapter.getCount() > 0;
audioButton.setVisibility(isAudioStreamsAvailable ? View.VISIBLE : View.GONE);
videoButton.setVisibility(isVideoStreamsAvailable ? View.VISIBLE : View.GONE);
if (isVideoStreamsAvailable) {
videoButton.setChecked(true);
} else if (sortedStreamVideosList == null || sortedStreamVideosList.size() == 0) {
videoButton.setVisibility(View.GONE);
setupVideoSpinner();
} else if (isAudioStreamsAvailable) {
audioButton.setChecked(true);
setupAudioSpinner();
} else {
Toast.makeText(getContext(), R.string.no_streams_available_download, Toast.LENGTH_SHORT).show();
getDialog().dismiss();
}
}
private void setRadioButtonsState(boolean enabled) {
radioVideoAudioGroup.findViewById(R.id.audio_button).setEnabled(enabled);
radioVideoAudioGroup.findViewById(R.id.video_button).setEnabled(enabled);
}
private void downloadSelected() {
String url, location;
Stream stream;
String location;
String fileName = nameEditText.getText().toString().trim();
if (fileName.isEmpty()) fileName = FilenameUtils.createFilename(getContext(), currentInfo.getName());
boolean isAudio = radioVideoAudioGroup.getCheckedRadioButtonId() == R.id.audio_button;
if (isAudio) {
url = currentInfo.getAudioStreams().get(selectedAudioIndex).getUrl();
stream = audioStreamsAdapter.getItem(selectedAudioIndex);
location = NewPipeSettings.getAudioDownloadPath(getContext());
fileName += "." + currentInfo.getAudioStreams().get(selectedAudioIndex).getFormat().getSuffix();
} else {
url = sortedStreamVideosList.get(selectedVideoIndex).getUrl();
stream = videoStreamsAdapter.getItem(selectedVideoIndex);
location = NewPipeSettings.getVideoDownloadPath(getContext());
fileName += "." + sortedStreamVideosList.get(selectedVideoIndex).getFormat().getSuffix();
}
String url = stream.getUrl();
fileName += "." + stream.getFormat().getSuffix();
DownloadManagerService.startMission(getContext(), url, location, fileName, isAudio, threadsSeekBar.getProgress() + 1);
getDialog().dismiss();
}

View File

@@ -246,13 +246,6 @@ public abstract class BaseStateFragment<I> extends BaseFragment implements ViewC
// Utils
//////////////////////////////////////////////////////////////////////////*/
public void setTitle(String title) {
if (DEBUG) Log.d(TAG, "setTitle() called with: title = [" + title + "]");
if (activity != null && activity.getSupportActionBar() != null) {
activity.getSupportActionBar().setTitle(title);
}
}
protected void openUrlInBrowser(String url) {
Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(url));
startActivity(Intent.createChooser(intent, activity.getString(R.string.share_dialog_title)));

View File

@@ -106,7 +106,7 @@ public class MainFragment extends BaseFragment implements TabLayout.OnTabSelecte
super.onCreateOptionsMenu(menu, inflater);
if (DEBUG) Log.d(TAG, "onCreateOptionsMenu() called with: menu = [" + menu + "], inflater = [" + inflater + "]");
inflater.inflate(R.menu.main_fragment_menu, menu);
SubMenu kioskMenu = menu.addSubMenu(getString(R.string.kiosk));
SubMenu kioskMenu = menu.addSubMenu(Menu.NONE, Menu.NONE, 200, getString(R.string.kiosk));
try {
createKioskMenu(kioskMenu, inflater);
} catch (Exception e) {

View File

@@ -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;
}
}

View File

@@ -12,6 +12,7 @@ import android.preference.PreferenceManager;
import android.support.annotation.DrawableRes;
import android.support.annotation.FloatRange;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.v4.content.ContextCompat;
import android.support.v4.view.animation.FastOutSlowInInterpolator;
import android.support.v7.app.ActionBar;
@@ -43,6 +44,7 @@ import android.widget.Toast;
import com.nirhart.parallaxscroll.views.ParallaxScrollView;
import com.nostra13.universalimageloader.core.assist.FailReason;
import com.nostra13.universalimageloader.core.listener.ImageLoadingListener;
import com.nostra13.universalimageloader.core.listener.SimpleImageLoadingListener;
import org.schabi.newpipe.R;
@@ -56,9 +58,12 @@ import org.schabi.newpipe.extractor.services.youtube.YoutubeStreamExtractor;
import org.schabi.newpipe.extractor.stream.AudioStream;
import org.schabi.newpipe.extractor.stream.StreamInfo;
import org.schabi.newpipe.extractor.stream.StreamInfoItem;
import org.schabi.newpipe.extractor.stream.StreamType;
import org.schabi.newpipe.extractor.stream.VideoStream;
import org.schabi.newpipe.fragments.BackPressable;
import org.schabi.newpipe.fragments.BaseStateFragment;
import org.schabi.newpipe.util.StreamItemAdapter;
import org.schabi.newpipe.util.StreamItemAdapter.StreamSizeWrapper;
import org.schabi.newpipe.fragments.local.dialog.PlaylistAppendDialog;
import org.schabi.newpipe.info_list.InfoItemBuilder;
import org.schabi.newpipe.info_list.InfoItemDialog;
@@ -68,10 +73,10 @@ 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.report.UserAction;
import org.schabi.newpipe.util.Constants;
import org.schabi.newpipe.util.ExtractorHelper;
import org.schabi.newpipe.util.ImageDisplayConstants;
import org.schabi.newpipe.util.InfoCache;
import org.schabi.newpipe.util.ListHelper;
import org.schabi.newpipe.util.Localization;
@@ -81,17 +86,16 @@ import org.schabi.newpipe.util.PermissionHelper;
import org.schabi.newpipe.util.ThemeHelper;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
import icepick.State;
import io.reactivex.Single;
import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.disposables.CompositeDisposable;
import io.reactivex.disposables.Disposable;
import io.reactivex.functions.Consumer;
import io.reactivex.functions.Function;
import io.reactivex.schedulers.Schedulers;
import static org.schabi.newpipe.util.AnimationUtils.animateView;
@@ -107,8 +111,6 @@ public class VideoDetailFragment
// Amount of videos to show on start
private static final int INITIAL_RELATED_VIDEOS = 8;
private ArrayList<VideoStream> sortedStreamVideosList;
private InfoItemBuilder infoItemBuilder = null;
private int updateFlags = 0;
@@ -120,22 +122,21 @@ public class VideoDetailFragment
private boolean showRelatedStreams;
private boolean wasRelatedStreamsExpanded = false;
@State
protected int serviceId = Constants.NO_SERVICE_ID;
@State
protected String name;
@State
protected String url;
@State protected int serviceId = Constants.NO_SERVICE_ID;
@State protected String name;
@State protected String url;
private StreamInfo currentInfo;
private Disposable currentWorker;
private CompositeDisposable disposables = new CompositeDisposable();
private int selectedVideoStream = -1;
private List<VideoStream> sortedVideoStreams;
private int selectedVideoStreamIndex = -1;
/*//////////////////////////////////////////////////////////////////////////
// Views
//////////////////////////////////////////////////////////////////////////*/
private Menu menu;
private Spinner spinnerToolbar;
@@ -157,6 +158,7 @@ public class VideoDetailFragment
private TextView detailControlsAddToPlaylist;
private TextView detailControlsDownload;
private TextView appendControlsDetail;
private TextView detailDurationView;
private LinearLayout videoDescriptionRootLayout;
private TextView videoUploadDateView;
@@ -202,7 +204,7 @@ public class VideoDetailFragment
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
return inflater.inflate(R.layout.fragment_video_detail, container, false);
}
@@ -321,7 +323,7 @@ public class VideoDetailFragment
if (serializable instanceof StreamInfo) {
//noinspection unchecked
currentInfo = (StreamInfo) serializable;
InfoCache.getInstance().putInfo(currentInfo);
InfoCache.getInstance().putInfo(serviceId, url, currentInfo);
}
serializable = savedState.getSerializable(STACK_KEY);
@@ -353,21 +355,8 @@ public class VideoDetailFragment
}
break;
case R.id.detail_controls_download:
if (!PermissionHelper.checkStoragePermissions(activity)) {
return;
}
try {
DownloadDialog downloadDialog =
DownloadDialog.newInstance(currentInfo,
sortedStreamVideosList,
selectedVideoStream);
downloadDialog.show(activity.getSupportFragmentManager(), "downloadDialog");
} catch (Exception e) {
Toast.makeText(activity,
R.string.could_not_setup_download_menu,
Toast.LENGTH_LONG).show();
e.printStackTrace();
if (PermissionHelper.checkStoragePermissions(activity, PermissionHelper.DOWNLOAD_DIALOG_REQUEST_CODE)) {
this.openDownloadDialog();
}
break;
case R.id.detail_uploader_root_layout:
@@ -382,7 +371,8 @@ public class VideoDetailFragment
}
break;
case R.id.detail_thumbnail_root_layout:
if (currentInfo.video_streams.isEmpty() && currentInfo.video_only_streams.isEmpty()) {
if (currentInfo.getVideoStreams().isEmpty()
&& currentInfo.getVideoOnlyStreams().isEmpty()) {
openBackgroundPlayer(false);
} else {
openVideoPlayer();
@@ -408,6 +398,9 @@ public class VideoDetailFragment
case R.id.detail_controls_popup:
openPopupPlayer(true);
break;
case R.id.detail_controls_download:
NavigationHelper.openDownloads(getActivity());
break;
}
return true;
@@ -458,7 +451,6 @@ public class VideoDetailFragment
@Override
protected void initViews(View rootView, Bundle savedInstanceState) {
super.initViews(rootView, savedInstanceState);
spinnerToolbar = activity.findViewById(R.id.toolbar).findViewById(R.id.toolbar_spinner);
parallaxScrollRootView = rootView.findViewById(R.id.detail_main_content);
@@ -479,6 +471,7 @@ public class VideoDetailFragment
detailControlsAddToPlaylist = rootView.findViewById(R.id.detail_controls_playlist_append);
detailControlsDownload = rootView.findViewById(R.id.detail_controls_download);
appendControlsDetail = rootView.findViewById(R.id.touch_append_detail);
detailDurationView = rootView.findViewById(R.id.detail_duration_view);
videoDescriptionRootLayout = rootView.findViewById(R.id.detail_description_root_layout);
videoUploadDateView = rootView.findViewById(R.id.detail_upload_date_view);
@@ -529,6 +522,7 @@ public class VideoDetailFragment
detailControlsPopup.setOnClickListener(this);
detailControlsAddToPlaylist.setOnClickListener(this);
detailControlsDownload.setOnClickListener(this);
detailControlsDownload.setOnLongClickListener(this);
relatedStreamExpandButton.setOnClickListener(this);
detailControlsBackground.setLongClickable(true);
@@ -545,7 +539,8 @@ public class VideoDetailFragment
final String[] commands = new String[]{
context.getResources().getString(R.string.enqueue_on_background),
context.getResources().getString(R.string.enqueue_on_popup)
context.getResources().getString(R.string.enqueue_on_popup),
context.getResources().getString(R.string.append_playlist)
};
final DialogInterface.OnClickListener actions = (DialogInterface dialogInterface, int i) -> {
@@ -556,6 +551,12 @@ public class VideoDetailFragment
case 1:
NavigationHelper.enqueueOnPopupPlayer(getActivity(), new SinglePlayQueue(item));
break;
case 2:
if (getFragmentManager() != null) {
PlaylistAppendDialog.fromStreamInfoItems(Collections.singletonList(item))
.show(getFragmentManager(), TAG);
}
break;
default:
break;
}
@@ -579,30 +580,25 @@ public class VideoDetailFragment
};
}
private void initThumbnailViews(StreamInfo info) {
private void initThumbnailViews(@NonNull StreamInfo info) {
thumbnailImageView.setImageResource(R.drawable.dummy_thumbnail_dark);
if (!TextUtils.isEmpty(info.getThumbnailUrl())) {
imageLoader.displayImage(
info.getThumbnailUrl(),
thumbnailImageView,
DISPLAY_THUMBNAIL_OPTIONS, new SimpleImageLoadingListener() {
final String infoServiceName = NewPipe.getNameOfService(info.getServiceId());
final ImageLoadingListener onFailListener = new SimpleImageLoadingListener() {
@Override
public void onLoadingFailed(String imageUri, View view, FailReason failReason) {
ErrorActivity.reportError(
activity,
failReason.getCause(),
null,
activity.findViewById(android.R.id.content),
ErrorActivity.ErrorInfo.make(UserAction.LOAD_IMAGE,
NewPipe.getNameOfService(currentInfo.getServiceId()),
imageUri,
R.string.could_not_load_thumbnails));
showSnackBarError(failReason.getCause(), UserAction.LOAD_IMAGE,
infoServiceName, imageUri, R.string.could_not_load_thumbnails);
}
});
};
imageLoader.displayImage(info.getThumbnailUrl(), thumbnailImageView,
ImageDisplayConstants.DISPLAY_THUMBNAIL_OPTIONS, onFailListener);
}
if (!TextUtils.isEmpty(info.getUploaderAvatarUrl())) {
imageLoader.displayImage(info.getUploaderAvatarUrl(), uploaderThumb, DISPLAY_AVATAR_OPTIONS);
imageLoader.displayImage(info.getUploaderAvatarUrl(), uploaderThumb,
ImageDisplayConstants.DISPLAY_AVATAR_OPTIONS);
}
}
@@ -617,7 +613,8 @@ public class VideoDetailFragment
relatedStreamRootLayout.setVisibility(View.VISIBLE);
} else nextStreamTitle.setVisibility(View.GONE);
if (info.related_streams != null && !info.related_streams.isEmpty() && showRelatedStreams) {
if (info.getRelatedStreams() != null
&& !info.getRelatedStreams().isEmpty() && showRelatedStreams) {
//long first = System.nanoTime(), each;
int to = info.getRelatedStreams().size() >= INITIAL_RELATED_VIDEOS
? INITIAL_RELATED_VIDEOS
@@ -681,15 +678,15 @@ public class VideoDetailFragment
int id = item.getItemId();
switch (id) {
case R.id.menu_item_share: {
if(currentInfo != null) {
shareUrl(currentInfo.name, url);
} else {
shareUrl(url, url);
if (currentInfo != null) {
shareUrl(currentInfo.getName(), currentInfo.getUrl());
}
return true;
}
case R.id.menu_item_openInBrowser: {
openUrlInBrowser(url);
if (currentInfo != null) {
openUrlInBrowser(currentInfo.getUrl());
}
return true;
}
case R.id.action_play_with_kodi:
@@ -722,27 +719,25 @@ public class VideoDetailFragment
private void setupActionBar(final StreamInfo info) {
if (DEBUG) Log.d(TAG, "setupActionBarHandler() called with: info = [" + info + "]");
sortedStreamVideosList = new ArrayList<>(ListHelper.getSortedStreamVideosList(
activity, info.getVideoStreams(), info.getVideoOnlyStreams(), false));
selectedVideoStream = ListHelper.getDefaultResolutionIndex(activity, sortedStreamVideosList);
boolean isExternalPlayerEnabled = PreferenceManager.getDefaultSharedPreferences(activity)
.getBoolean(activity.getString(R.string.use_external_video_player_key), false);
spinnerToolbar.setAdapter(new SpinnerToolbarAdapter(activity, sortedStreamVideosList,
isExternalPlayerEnabled));
spinnerToolbar.setSelection(selectedVideoStream);
sortedVideoStreams = ListHelper.getSortedStreamVideosList(activity, info.getVideoStreams(), info.getVideoOnlyStreams(), false);
selectedVideoStreamIndex = ListHelper.getDefaultResolutionIndex(activity, sortedVideoStreams);
final StreamItemAdapter<VideoStream> streamsAdapter = new StreamItemAdapter<>(activity, new StreamSizeWrapper<>(sortedVideoStreams), isExternalPlayerEnabled);
spinnerToolbar.setAdapter(streamsAdapter);
spinnerToolbar.setSelection(selectedVideoStreamIndex);
spinnerToolbar.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
@Override
public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
selectedVideoStream = position;
selectedVideoStreamIndex = position;
}
@Override
public void onNothingSelected(AdapterView<?> parent) {
}
});
}
/*//////////////////////////////////////////////////////////////////////////
@@ -818,7 +813,7 @@ public class VideoDetailFragment
public void prepareAndHandleInfo(final StreamInfo info, boolean scrollToTop) {
if (DEBUG) Log.d(TAG, "prepareAndHandleInfo() called with: info = [" + info + "], scrollToTop = [" + scrollToTop + "]");
setInitialData(info.getServiceId(), info.getUrl(), info.getName());
setInitialData(info.getServiceId(), info.getOriginalUrl(), info.getName());
pushToStack(serviceId, url, name);
showLoading();
@@ -954,8 +949,9 @@ public class VideoDetailFragment
this.autoPlayEnabled = autoplay;
}
@Nullable
private VideoStream getSelectedVideoStream() {
return sortedStreamVideosList.get(selectedVideoStream);
return sortedVideoStreams != null ? sortedVideoStreams.get(selectedVideoStreamIndex) : null;
}
private void prepareDescription(final String descriptionHtml) {
@@ -1006,9 +1002,6 @@ public class VideoDetailFragment
int height = isPortrait
? (int) (metrics.widthPixels / (16.0f / 9.0f))
: (int) (metrics.heightPixels / 2f);
thumbnailImageView.setScaleType(isPortrait
? ImageView.ScaleType.CENTER_CROP
: ImageView.ScaleType.FIT_CENTER);
thumbnailImageView.setLayoutParams(
new FrameLayout.LayoutParams(RelativeLayout.LayoutParams.MATCH_PARENT, height));
thumbnailImageView.setMinimumHeight(height);
@@ -1094,6 +1087,7 @@ public class VideoDetailFragment
animateView(contentRootLayoutHiding, false, 200);
animateView(spinnerToolbar, false, 200);
animateView(thumbnailPlayButton, false, 50);
animateView(detailDurationView, false, 100);
videoTitleTextView.setText(name != null ? name : "");
videoTitleTextView.setMaxLines(1);
@@ -1114,7 +1108,7 @@ public class VideoDetailFragment
public void handleResult(@NonNull StreamInfo info) {
super.handleResult(info);
setInitialData(info.getServiceId(), info.getUrl(), info.getName());
setInitialData(info.getServiceId(), info.getOriginalUrl(), info.getName());
pushToStack(serviceId, url, name);
animateView(thumbnailPlayButton, true, 200);
@@ -1164,6 +1158,18 @@ public class VideoDetailFragment
thumbsDisabledTextView.setVisibility(View.GONE);
}
if (info.getDuration() > 0) {
detailDurationView.setText(Localization.getDurationString(info.getDuration()));
detailDurationView.setBackgroundColor(ContextCompat.getColor(activity, R.color.duration_background_color));
animateView(detailDurationView, true, 100);
} else if (info.getStreamType() == StreamType.LIVE_STREAM) {
detailDurationView.setText(R.string.duration_live);
detailDurationView.setBackgroundColor(ContextCompat.getColor(activity, R.color.live_duration_background_color));
animateView(detailDurationView, true, 100);
} else {
detailDurationView.setVisibility(View.GONE);
}
videoTitleRoot.setClickable(true);
videoTitleToggleArrow.setVisibility(View.VISIBLE);
videoTitleToggleArrow.setImageResource(R.drawable.arrow_down);
@@ -1182,7 +1188,9 @@ public class VideoDetailFragment
toggleExpandRelatedVideos(currentInfo);
wasRelatedStreamsExpanded = false;
}
setTitleToUrl(info.getServiceId(), info.getUrl(), info.getName());
setTitleToUrl(info.getServiceId(), info.getOriginalUrl(), info.getName());
if (!info.getErrors().isEmpty()) {
showSnackBarError(info.getErrors(),
@@ -1192,11 +1200,21 @@ public class VideoDetailFragment
0);
}
if (info.video_streams.isEmpty() && info.video_only_streams.isEmpty()) {
detailControlsBackground.setVisibility(View.GONE);
detailControlsPopup.setVisibility(View.GONE);
spinnerToolbar.setVisibility(View.GONE);
thumbnailPlayButton.setImageResource(R.drawable.ic_headset_white_24dp);
switch (info.getStreamType()) {
case LIVE_STREAM:
case AUDIO_LIVE_STREAM:
detailControlsDownload.setVisibility(View.GONE);
spinnerToolbar.setVisibility(View.GONE);
break;
default:
if (!info.getVideoStreams().isEmpty()
|| !info.getVideoOnlyStreams().isEmpty()) break;
detailControlsBackground.setVisibility(View.GONE);
detailControlsPopup.setVisibility(View.GONE);
spinnerToolbar.setVisibility(View.GONE);
thumbnailPlayButton.setImageResource(R.drawable.ic_headset_white_24dp);
break;
}
if (autoPlayEnabled) {
@@ -1206,6 +1224,23 @@ public class VideoDetailFragment
}
}
public void openDownloadDialog() {
try {
DownloadDialog downloadDialog = DownloadDialog.newInstance(currentInfo);
downloadDialog.setVideoStreams(sortedVideoStreams);
downloadDialog.setAudioStreams(currentInfo.getAudioStreams());
downloadDialog.setSelectedVideoStream(selectedVideoStreamIndex);
downloadDialog.show(activity.getSupportFragmentManager(), "downloadDialog");
} catch (Exception e) {
Toast.makeText(activity,
R.string.could_not_setup_download_menu,
Toast.LENGTH_LONG).show();
e.printStackTrace();
}
}
/*//////////////////////////////////////////////////////////////////////////
// Stream Results
//////////////////////////////////////////////////////////////////////////*/
@@ -1216,8 +1251,6 @@ public class VideoDetailFragment
if (exception instanceof YoutubeStreamExtractor.GemaException) {
onBlockedByGemaError();
} else if (exception instanceof YoutubeStreamExtractor.LiveStreamException) {
showError(getString(R.string.live_streams_not_supported), false);
} else if (exception instanceof ContentNotAvailableException) {
showError(getString(R.string.content_not_available), false);
} else {

View File

@@ -141,8 +141,7 @@ public abstract class BaseListFragment<I, N> extends BaseStateFragment<I> implem
@Override
public void selected(StreamInfoItem selectedItem) {
onItemSelected(selectedItem);
NavigationHelper.openVideoDetailFragment(
useAsFrontPage?getParentFragment().getFragmentManager():getFragmentManager(),
NavigationHelper.openVideoDetailFragment(useAsFrontPage ? getParentFragment().getFragmentManager() : getFragmentManager(),
selectedItem.getServiceId(), selectedItem.getUrl(), selectedItem.getName());
}
@@ -156,8 +155,7 @@ public abstract class BaseListFragment<I, N> extends BaseStateFragment<I> implem
@Override
public void selected(ChannelInfoItem selectedItem) {
onItemSelected(selectedItem);
NavigationHelper.openChannelFragment(
useAsFrontPage?getParentFragment().getFragmentManager():getFragmentManager(),
NavigationHelper.openChannelFragment(useAsFrontPage ? getParentFragment().getFragmentManager() : getFragmentManager(),
selectedItem.getServiceId(), selectedItem.getUrl(), selectedItem.getName());
}
});
@@ -166,8 +164,7 @@ public abstract class BaseListFragment<I, N> extends BaseStateFragment<I> implem
@Override
public void selected(PlaylistInfoItem selectedItem) {
onItemSelected(selectedItem);
NavigationHelper.openPlaylistFragment(
useAsFrontPage?getParentFragment().getFragmentManager():getFragmentManager(),
NavigationHelper.openPlaylistFragment(useAsFrontPage ? getParentFragment().getFragmentManager() : getFragmentManager(),
selectedItem.getServiceId(), selectedItem.getUrl(), selectedItem.getName());
}
});
@@ -230,7 +227,7 @@ public abstract class BaseListFragment<I, N> extends BaseStateFragment<I> implem
ActionBar supportActionBar = activity.getSupportActionBar();
if (supportActionBar != null) {
supportActionBar.setDisplayShowTitleEnabled(true);
if(useAsFrontPage) {
if (useAsFrontPage) {
supportActionBar.setDisplayHomeAsUpEnabled(false);
} else {
supportActionBar.setDisplayHomeAsUpEnabled(true);
@@ -277,9 +274,8 @@ public abstract class BaseListFragment<I, N> extends BaseStateFragment<I> implem
@Override
public void showListFooter(final boolean show) {
itemsList.post(new Runnable() {
@Override
public void run() {
itemsList.post(() -> {
if (infoListAdapter != null && itemsList != null) {
infoListAdapter.showFooter(show);
}
});

View File

@@ -19,7 +19,8 @@ import io.reactivex.disposables.Disposable;
import io.reactivex.functions.Consumer;
import io.reactivex.schedulers.Schedulers;
public abstract class BaseListInfoFragment<I extends ListInfo> extends BaseListFragment<I, ListExtractor.NextItemsResult> {
public abstract class BaseListInfoFragment<I extends ListInfo>
extends BaseListFragment<I, ListExtractor.InfoItemsPage> {
@State
protected int serviceId = Constants.NO_SERVICE_ID;
@@ -29,7 +30,7 @@ public abstract class BaseListInfoFragment<I extends ListInfo> extends BaseListF
protected String url;
protected I currentInfo;
protected String currentNextItemsUrl;
protected String currentNextPageUrl;
protected Disposable currentWorker;
@Override
@@ -73,7 +74,7 @@ public abstract class BaseListInfoFragment<I extends ListInfo> extends BaseListF
public void writeTo(Queue<Object> objectsToSave) {
super.writeTo(objectsToSave);
objectsToSave.add(currentInfo);
objectsToSave.add(currentNextItemsUrl);
objectsToSave.add(currentNextPageUrl);
}
@Override
@@ -81,7 +82,7 @@ public abstract class BaseListInfoFragment<I extends ListInfo> extends BaseListF
public void readFrom(@NonNull Queue<Object> savedObjects) throws Exception {
super.readFrom(savedObjects);
currentInfo = (I) savedObjects.poll();
currentNextItemsUrl = (String) savedObjects.poll();
currentNextPageUrl = (String) savedObjects.poll();
}
/*//////////////////////////////////////////////////////////////////////////
@@ -116,7 +117,7 @@ public abstract class BaseListInfoFragment<I extends ListInfo> extends BaseListF
.subscribe((@NonNull I result) -> {
isLoading.set(false);
currentInfo = result;
currentNextItemsUrl = result.next_streams_url;
currentNextPageUrl = result.getNextPageUrl();
handleResult(result);
}, (@NonNull Throwable throwable) -> onError(throwable));
}
@@ -125,7 +126,7 @@ public abstract class BaseListInfoFragment<I extends ListInfo> extends BaseListF
* Implement the logic to load more items<br/>
* You can use the default implementations from {@link org.schabi.newpipe.util.ExtractorHelper}
*/
protected abstract Single<ListExtractor.NextItemsResult> loadMoreItemsLogic();
protected abstract Single<ListExtractor.InfoItemsPage> loadMoreItemsLogic();
protected void loadMoreItems() {
isLoading.set(true);
@@ -134,9 +135,9 @@ public abstract class BaseListInfoFragment<I extends ListInfo> extends BaseListF
currentWorker = loadMoreItemsLogic()
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe((@io.reactivex.annotations.NonNull ListExtractor.NextItemsResult nextItemsResult) -> {
.subscribe((@io.reactivex.annotations.NonNull ListExtractor.InfoItemsPage InfoItemsPage) -> {
isLoading.set(false);
handleNextItems(nextItemsResult);
handleNextItems(InfoItemsPage);
}, (@io.reactivex.annotations.NonNull Throwable throwable) -> {
isLoading.set(false);
onError(throwable);
@@ -144,17 +145,17 @@ public abstract class BaseListInfoFragment<I extends ListInfo> extends BaseListF
}
@Override
public void handleNextItems(ListExtractor.NextItemsResult result) {
public void handleNextItems(ListExtractor.InfoItemsPage result) {
super.handleNextItems(result);
currentNextItemsUrl = result.nextItemsUrl;
infoListAdapter.addInfoItemList(result.nextItemsList);
currentNextPageUrl = result.getNextPageUrl();
infoListAdapter.addInfoItemList(result.getItems());
showListFooter(hasMoreItems());
}
@Override
protected boolean hasMoreItems() {
return !TextUtils.isEmpty(currentNextItemsUrl);
return !TextUtils.isEmpty(currentNextPageUrl);
}
/*//////////////////////////////////////////////////////////////////////////
@@ -170,8 +171,8 @@ public abstract class BaseListInfoFragment<I extends ListInfo> extends BaseListF
setTitle(name);
if (infoListAdapter.getItemsList().size() == 0) {
if (result.related_streams.size() > 0) {
infoListAdapter.addInfoItemList(result.related_streams);
if (result.getRelatedItems().size() > 0) {
infoListAdapter.addInfoItemList(result.getRelatedItems());
showListFooter(hasMoreItems());
} else {
infoListAdapter.clearStreamItemList();

View File

@@ -27,23 +27,30 @@ import com.jakewharton.rxbinding2.view.RxView;
import org.schabi.newpipe.R;
import org.schabi.newpipe.database.subscription.SubscriptionEntity;
import org.schabi.newpipe.extractor.InfoItem;
import org.schabi.newpipe.extractor.ListExtractor;
import org.schabi.newpipe.extractor.NewPipe;
import org.schabi.newpipe.extractor.channel.ChannelInfo;
import org.schabi.newpipe.extractor.exceptions.ExtractionException;
import org.schabi.newpipe.extractor.stream.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.fragments.subscription.SubscriptionService;
import org.schabi.newpipe.fragments.local.dialog.PlaylistAppendDialog;
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.report.UserAction;
import org.schabi.newpipe.subscription.SubscriptionService;
import org.schabi.newpipe.util.AnimationUtils;
import org.schabi.newpipe.util.ExtractorHelper;
import org.schabi.newpipe.util.ImageDisplayConstants;
import org.schabi.newpipe.util.Localization;
import org.schabi.newpipe.util.NavigationHelper;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.TimeUnit;
@@ -108,11 +115,11 @@ public class ChannelFragment extends BaseListInfoFragment<ChannelInfo> {
@Override
public void onAttach(Context context) {
super.onAttach(context);
subscriptionService = SubscriptionService.getInstance();
subscriptionService = SubscriptionService.getInstance(activity);
}
@Override
public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
return inflater.inflate(R.layout.fragment_channel, container, false);
}
@@ -156,6 +163,7 @@ public class ChannelFragment extends BaseListInfoFragment<ChannelInfo> {
context.getResources().getString(R.string.start_here_on_main),
context.getResources().getString(R.string.start_here_on_background),
context.getResources().getString(R.string.start_here_on_popup),
context.getResources().getString(R.string.append_playlist)
};
final DialogInterface.OnClickListener actions = new DialogInterface.OnClickListener() {
@@ -178,6 +186,12 @@ public class ChannelFragment extends BaseListInfoFragment<ChannelInfo> {
case 4:
NavigationHelper.playOnPopupPlayer(activity, getPlayQueue(index));
break;
case 5:
if (getFragmentManager() != null) {
PlaylistAppendDialog.fromStreamInfoItems(Collections.singletonList(item))
.show(getFragmentManager(), TAG);
}
break;
default:
break;
}
@@ -388,8 +402,8 @@ public class ChannelFragment extends BaseListInfoFragment<ChannelInfo> {
//////////////////////////////////////////////////////////////////////////*/
@Override
protected Single<ListExtractor.NextItemsResult> loadMoreItemsLogic() {
return ExtractorHelper.getMoreChannelItems(serviceId, url, currentNextItemsUrl);
protected Single<ListExtractor.InfoItemsPage> loadMoreItemsLogic() {
return ExtractorHelper.getMoreChannelItems(serviceId, url, currentNextPageUrl);
}
@Override
@@ -415,8 +429,10 @@ public class ChannelFragment extends BaseListInfoFragment<ChannelInfo> {
super.handleResult(result);
headerRootLayout.setVisibility(View.VISIBLE);
imageLoader.displayImage(result.banner_url, headerChannelBanner, DISPLAY_BANNER_OPTIONS);
imageLoader.displayImage(result.avatar_url, headerAvatarView, DISPLAY_AVATAR_OPTIONS);
imageLoader.displayImage(result.getBannerUrl(), headerChannelBanner,
ImageDisplayConstants.DISPLAY_BANNER_OPTIONS);
imageLoader.displayImage(result.getAvatarUrl(), headerAvatarView,
ImageDisplayConstants.DISPLAY_AVATAR_OPTIONS);
if (result.getSubscriberCount() != -1) {
headerSubscribersTextView.setText(Localization.localizeSubscribersCount(activity, result.getSubscriberCount()));
@@ -427,8 +443,8 @@ public class ChannelFragment extends BaseListInfoFragment<ChannelInfo> {
playlistCtrl.setVisibility(View.VISIBLE);
if (!result.errors.isEmpty()) {
showSnackBarError(result.errors, UserAction.REQUESTED_CHANNEL, NewPipe.getNameOfService(result.getServiceId()), result.getUrl(), 0);
if (!result.getErrors().isEmpty()) {
showSnackBarError(result.getErrors(), UserAction.REQUESTED_CHANNEL, NewPipe.getNameOfService(result.getServiceId()), result.getUrl(), 0);
}
if (disposables != null) disposables.clear();
@@ -436,24 +452,12 @@ public class ChannelFragment extends BaseListInfoFragment<ChannelInfo> {
updateSubscription(result);
monitorSubscription(result);
headerPlayAllButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
NavigationHelper.playOnMainPlayer(activity, getPlayQueue());
}
});
headerPopupButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
NavigationHelper.playOnPopupPlayer(activity, getPlayQueue());
}
});
headerBackgroundButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
NavigationHelper.playOnBackgroundPlayer(activity, getPlayQueue());
}
});
headerPlayAllButton.setOnClickListener(
view -> NavigationHelper.playOnMainPlayer(activity, getPlayQueue()));
headerPopupButton.setOnClickListener(
view -> NavigationHelper.playOnPopupPlayer(activity, getPlayQueue()));
headerBackgroundButton.setOnClickListener(
view -> NavigationHelper.playOnBackgroundPlayer(activity, getPlayQueue()));
}
private PlayQueue getPlayQueue() {
@@ -461,17 +465,23 @@ public class ChannelFragment extends BaseListInfoFragment<ChannelInfo> {
}
private PlayQueue getPlayQueue(final int index) {
final List<StreamInfoItem> streamItems = new ArrayList<>();
for(InfoItem i : infoListAdapter.getItemsList()) {
if(i instanceof StreamInfoItem) {
streamItems.add((StreamInfoItem) i);
}
}
return new ChannelPlayQueue(
currentInfo.getServiceId(),
currentInfo.getUrl(),
currentInfo.getNextStreamsUrl(),
infoListAdapter.getItemsList(),
currentInfo.getNextPageUrl(),
streamItems,
index
);
}
@Override
public void handleNextItems(ListExtractor.NextItemsResult result) {
public void handleNextItems(ListExtractor.InfoItemsPage result) {
super.handleNextItems(result);
if (!result.getErrors().isEmpty()) {

View File

@@ -21,8 +21,8 @@ import org.schabi.newpipe.extractor.NewPipe;
import org.schabi.newpipe.extractor.channel.ChannelInfo;
import org.schabi.newpipe.extractor.exceptions.ExtractionException;
import org.schabi.newpipe.fragments.list.BaseListFragment;
import org.schabi.newpipe.fragments.subscription.SubscriptionService;
import org.schabi.newpipe.report.UserAction;
import org.schabi.newpipe.subscription.SubscriptionService;
import java.util.Collections;
import java.util.HashSet;
@@ -64,7 +64,7 @@ public class FeedFragment extends BaseListFragment<List<SubscriptionEntity>, Voi
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
subscriptionService = SubscriptionService.getInstance();
subscriptionService = SubscriptionService.getInstance(activity);
FEED_LOAD_COUNT = howManyItemsToLoad();
}
@@ -297,12 +297,12 @@ public class FeedFragment extends BaseListFragment<List<SubscriptionEntity>, Voi
// Called only when response is non-empty
@Override
public void onSuccess(final ChannelInfo channelInfo) {
if (infoListAdapter == null || channelInfo.getRelatedStreams().isEmpty()) {
if (infoListAdapter == null || channelInfo.getRelatedItems().isEmpty()) {
onDone();
return;
}
final InfoItem item = channelInfo.getRelatedStreams().get(0);
final InfoItem item = channelInfo.getRelatedItems().get(0);
// Keep requesting new items if the current one already exists
boolean itemExists = doesItemExist(infoListAdapter.getItemsList(), item);
if (!itemExists) {
@@ -411,7 +411,7 @@ public class FeedFragment extends BaseListFragment<List<SubscriptionEntity>, Voi
private boolean doesItemExist(final List<InfoItem> items, final InfoItem item) {
for (final InfoItem existingItem : items) {
if (existingItem.info_type == item.info_type &&
if (existingItem.getInfoType() == item.getInfoType() &&
existingItem.getServiceId() == item.getServiceId() &&
existingItem.getName().equals(item.getName()) &&
existingItem.getUrl().equals(item.getUrl())) return true;

View File

@@ -141,12 +141,12 @@ public class KioskFragment extends BaseListInfoFragment<KioskInfo> {
}
@Override
public Single<ListExtractor.NextItemsResult> loadMoreItemsLogic() {
public Single<ListExtractor.InfoItemsPage> loadMoreItemsLogic() {
String contentCountry = PreferenceManager
.getDefaultSharedPreferences(activity)
.getString(getString(R.string.content_country_key),
getString(R.string.default_country_value));
return ExtractorHelper.getMoreKioskItems(serviceId, url, currentNextItemsUrl, contentCountry);
return ExtractorHelper.getMoreKioskItems(serviceId, url, currentNextPageUrl, contentCountry);
}
/*//////////////////////////////////////////////////////////////////////////
@@ -168,13 +168,13 @@ public class KioskFragment extends BaseListInfoFragment<KioskInfo> {
if (!result.getErrors().isEmpty()) {
showSnackBarError(result.getErrors(),
UserAction.REQUESTED_PLAYLIST,
UserAction.REQUESTED_KIOSK,
NewPipe.getNameOfService(result.getServiceId()), result.getUrl(), 0);
}
}
@Override
public void handleNextItems(ListExtractor.NextItemsResult result) {
public void handleNextItems(ListExtractor.InfoItemsPage result) {
super.handleNextItems(result);
if (!result.getErrors().isEmpty()) {

View File

@@ -22,10 +22,12 @@ import org.reactivestreams.Subscription;
import org.schabi.newpipe.NewPipeDatabase;
import org.schabi.newpipe.R;
import org.schabi.newpipe.database.playlist.model.PlaylistRemoteEntity;
import org.schabi.newpipe.extractor.InfoItem;
import org.schabi.newpipe.extractor.ListExtractor;
import org.schabi.newpipe.extractor.NewPipe;
import org.schabi.newpipe.extractor.exceptions.ExtractionException;
import org.schabi.newpipe.extractor.playlist.PlaylistInfo;
import org.schabi.newpipe.extractor.stream.StreamInfo;
import org.schabi.newpipe.extractor.stream.StreamInfoItem;
import org.schabi.newpipe.fragments.list.BaseListInfoFragment;
import org.schabi.newpipe.fragments.local.RemotePlaylistManager;
@@ -35,9 +37,11 @@ import org.schabi.newpipe.playlist.PlaylistPlayQueue;
import org.schabi.newpipe.playlist.SinglePlayQueue;
import org.schabi.newpipe.report.UserAction;
import org.schabi.newpipe.util.ExtractorHelper;
import org.schabi.newpipe.util.ImageDisplayConstants;
import org.schabi.newpipe.util.NavigationHelper;
import org.schabi.newpipe.util.ThemeHelper;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.atomic.AtomicBoolean;
@@ -206,8 +210,8 @@ public class PlaylistFragment extends BaseListInfoFragment<PlaylistInfo> {
//////////////////////////////////////////////////////////////////////////*/
@Override
protected Single<ListExtractor.NextItemsResult> loadMoreItemsLogic() {
return ExtractorHelper.getMorePlaylistItems(serviceId, url, currentNextItemsUrl);
protected Single<ListExtractor.InfoItemsPage> loadMoreItemsLogic() {
return ExtractorHelper.getMorePlaylistItems(serviceId, url, currentNextPageUrl);
}
@Override
@@ -268,8 +272,10 @@ public class PlaylistFragment extends BaseListInfoFragment<PlaylistInfo> {
playlistCtrl.setVisibility(View.VISIBLE);
imageLoader.displayImage(result.getUploaderAvatarUrl(), headerUploaderAvatar, DISPLAY_AVATAR_OPTIONS);
headerStreamCount.setText(getResources().getQuantityString(R.plurals.videos, (int) result.stream_count, (int) result.stream_count));
imageLoader.displayImage(result.getUploaderAvatarUrl(), headerUploaderAvatar,
ImageDisplayConstants.DISPLAY_AVATAR_OPTIONS);
headerStreamCount.setText(getResources().getQuantityString(R.plurals.videos,
(int) result.getStreamCount(), (int) result.getStreamCount()));
if (!result.getErrors().isEmpty()) {
showSnackBarError(result.getErrors(), UserAction.REQUESTED_PLAYLIST, NewPipe.getNameOfService(result.getServiceId()), result.getUrl(), 0);
@@ -297,17 +303,23 @@ public class PlaylistFragment extends BaseListInfoFragment<PlaylistInfo> {
}
private PlayQueue getPlayQueue(final int index) {
final List<StreamInfoItem> infoItems = new ArrayList<>();
for(InfoItem i : infoListAdapter.getItemsList()) {
if(i instanceof StreamInfoItem) {
infoItems.add((StreamInfoItem) i);
}
}
return new PlaylistPlayQueue(
currentInfo.getServiceId(),
currentInfo.getUrl(),
currentInfo.getNextStreamsUrl(),
infoListAdapter.getItemsList(),
currentInfo.getNextPageUrl(),
infoItems,
index
);
}
@Override
public void handleNextItems(ListExtractor.NextItemsResult result) {
public void handleNextItems(ListExtractor.InfoItemsPage result) {
super.handleNextItems(result);
if (!result.getErrors().isEmpty()) {

View File

@@ -71,7 +71,9 @@ import io.reactivex.subjects.PublishSubject;
import static org.schabi.newpipe.util.AnimationUtils.animateView;
public class SearchFragment extends BaseListFragment<SearchResult, ListExtractor.NextItemsResult> implements BackPressable {
public class SearchFragment
extends BaseListFragment<SearchResult, ListExtractor.InfoItemsPage>
implements BackPressable {
/*//////////////////////////////////////////////////////////////////////////
// Search
@@ -527,23 +529,26 @@ public class SearchFragment extends BaseListFragment<SearchResult, ListExtractor
}
private void showDeleteSuggestionDialog(final SuggestionItem item) {
final Disposable onDelete = historyRecordManager.deleteSearchHistory(item.query)
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
howManyDeleted -> suggestionPublisher
.onNext(searchEditText.getText().toString()),
throwable -> showSnackBarError(throwable,
UserAction.SOMETHING_ELSE, "none",
"Deleting item failed", R.string.general_error)
);
if (activity == null || historyRecordManager == null || suggestionPublisher == null ||
searchEditText == null || disposables == null) return;
final String query = item.query;
new AlertDialog.Builder(activity)
.setTitle(item.query)
.setTitle(query)
.setMessage(R.string.delete_item_search_history)
.setCancelable(true)
.setNegativeButton(R.string.cancel, null)
.setPositiveButton(R.string.delete, (dialog, which) -> disposables.add(onDelete))
.setPositiveButton(R.string.delete, (dialog, which) -> {
final Disposable onDelete = historyRecordManager.deleteSearchHistory(query)
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
howManyDeleted -> suggestionPublisher
.onNext(searchEditText.getText().toString()),
throwable -> showSnackBarError(throwable,
UserAction.SOMETHING_ELSE, "none",
"Deleting item failed", R.string.general_error)
);
disposables.add(onDelete);
})
.show();
}
@@ -701,19 +706,8 @@ public class SearchFragment extends BaseListFragment<SearchResult, ListExtractor
searchDisposable = ExtractorHelper.searchFor(serviceId, searchQuery, currentPage, contentCountry, filter)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(new Consumer<SearchResult>() {
@Override
public void accept(@NonNull SearchResult result) throws Exception {
isLoading.set(false);
handleResult(result);
}
}, new Consumer<Throwable>() {
@Override
public void accept(@NonNull Throwable throwable) throws Exception {
isLoading.set(false);
onError(throwable);
}
});
.doOnEvent((searchResult, throwable) -> isLoading.set(false))
.subscribe(this::handleResult, this::onError);
}
@Override
@@ -725,19 +719,8 @@ public class SearchFragment extends BaseListFragment<SearchResult, ListExtractor
searchDisposable = ExtractorHelper.getMoreSearchItems(serviceId, searchQuery, currentNextPage, contentCountry, filter)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(new Consumer<ListExtractor.NextItemsResult>() {
@Override
public void accept(@NonNull ListExtractor.NextItemsResult result) throws Exception {
isLoading.set(false);
handleNextItems(result);
}
}, new Consumer<Throwable>() {
@Override
public void accept(@NonNull Throwable throwable) throws Exception {
isLoading.set(false);
onError(throwable);
}
});
.doOnEvent((nextItemsResult, throwable) -> isLoading.set(false))
.subscribe(this::handleNextItems, this::onError);
}
@Override
@@ -778,12 +761,7 @@ public class SearchFragment extends BaseListFragment<SearchResult, ListExtractor
public void handleSuggestions(@NonNull final List<SuggestionItem> suggestions) {
if (DEBUG) Log.d(TAG, "handleSuggestions() called with: suggestions = [" + suggestions + "]");
suggestionsRecyclerView.smoothScrollToPosition(0);
suggestionsRecyclerView.post(new Runnable() {
@Override
public void run() {
suggestionListAdapter.setItems(suggestions);
}
});
suggestionsRecyclerView.post(() -> suggestionListAdapter.setItems(suggestions));
if (errorPanelRoot.getVisibility() == View.VISIBLE) {
hideLoading();
@@ -841,10 +819,10 @@ public class SearchFragment extends BaseListFragment<SearchResult, ListExtractor
}
@Override
public void handleNextItems(ListExtractor.NextItemsResult result) {
public void handleNextItems(ListExtractor.InfoItemsPage result) {
showListFooter(false);
currentPage = Integer.parseInt(result.getNextItemsUrl());
infoListAdapter.addInfoItemList(result.getNextItemsList());
currentPage = Integer.parseInt(result.getNextPageUrl());
infoListAdapter.addInfoItemList(result.getItems());
if (!result.getErrors().isEmpty()) {
showSnackBarError(result.getErrors(), UserAction.SEARCHED, NewPipe.getNameOfService(serviceId)

View File

@@ -1,12 +1,10 @@
package org.schabi.newpipe.fragments.local;
import android.content.Context;
import android.graphics.Bitmap;
import android.widget.ImageView;
import com.nostra13.universalimageloader.core.DisplayImageOptions;
import com.nostra13.universalimageloader.core.ImageLoader;
import com.nostra13.universalimageloader.core.process.BitmapProcessor;
import org.schabi.newpipe.database.LocalItem;
import org.schabi.newpipe.util.OnClickGesture;

View File

@@ -151,7 +151,10 @@ public abstract class BaseLocalListFragment<I, N> extends BaseStateFragment<I>
@Override
public void showListFooter(final boolean show) {
itemsList.post(() -> itemListAdapter.showFooter(show));
if (itemsList == null) return;
itemsList.post(() -> {
if (itemListAdapter != null) itemListAdapter.showFooter(show);
});
}
@Override

View File

@@ -56,7 +56,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 +80,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));
}
}
///////////////////////////////////////////////////////////////////////////

View File

@@ -27,8 +27,6 @@ import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import javax.annotation.Nonnull;
import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.disposables.Disposable;
@@ -147,7 +145,7 @@ public final class PlaylistAppendDialog extends PlaylistDialog {
private void onPlaylistSelected(@NonNull LocalPlaylistManager manager,
@NonNull PlaylistMetadataEntry playlist,
@Nonnull List<StreamEntity> streams) {
@NonNull List<StreamEntity> streams) {
if (getStreams() == null) return;
@SuppressLint("ShowToast")

View File

@@ -1,14 +1,8 @@
package org.schabi.newpipe.fragments.local.holder;
import android.graphics.Bitmap;
import android.support.annotation.DimenRes;
import android.support.v7.widget.RecyclerView;
import android.view.LayoutInflater;
import android.view.ViewGroup;
import android.widget.ImageView;
import com.nostra13.universalimageloader.core.DisplayImageOptions;
import com.nostra13.universalimageloader.core.process.BitmapProcessor;
import org.schabi.newpipe.database.LocalItem;
import org.schabi.newpipe.fragments.local.LocalItemBuilder;
@@ -45,19 +39,4 @@ public abstract class LocalItemHolder extends RecyclerView.ViewHolder {
}
public abstract void updateFromItem(final LocalItem item, final DateFormat dateFormat);
/*//////////////////////////////////////////////////////////////////////////
// ImageLoaderOptions
//////////////////////////////////////////////////////////////////////////*/
/**
* Base display options
*/
public static final DisplayImageOptions BASE_DISPLAY_IMAGE_OPTIONS =
new DisplayImageOptions.Builder()
.cacheInMemory(true)
.cacheOnDisk(true)
.bitmapConfig(Bitmap.Config.RGB_565)
.resetViewBeforeLoading(false)
.build();
}

View File

@@ -2,15 +2,11 @@ package org.schabi.newpipe.fragments.local.holder;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageView;
import android.widget.TextView;
import com.nostra13.universalimageloader.core.DisplayImageOptions;
import org.schabi.newpipe.R;
import org.schabi.newpipe.database.LocalItem;
import org.schabi.newpipe.database.playlist.PlaylistMetadataEntry;
import org.schabi.newpipe.fragments.local.LocalItemBuilder;
import org.schabi.newpipe.util.ImageDisplayConstants;
import java.text.DateFormat;
@@ -29,7 +25,8 @@ public class LocalPlaylistItemHolder extends PlaylistItemHolder {
itemStreamCountView.setText(String.valueOf(item.streamCount));
itemUploaderView.setVisibility(View.INVISIBLE);
itemBuilder.displayImage(item.thumbnailUrl, itemThumbnailView, DISPLAY_THUMBNAIL_OPTIONS);
itemBuilder.displayImage(item.thumbnailUrl, itemThumbnailView,
ImageDisplayConstants.DISPLAY_PLAYLIST_OPTIONS);
super.updateFromItem(localItem, dateFormat);
}

View File

@@ -1,6 +1,5 @@
package org.schabi.newpipe.fragments.local.holder;
import android.graphics.Bitmap;
import android.support.v4.content.ContextCompat;
import android.view.MotionEvent;
import android.view.View;
@@ -8,14 +7,12 @@ import android.view.ViewGroup;
import android.widget.ImageView;
import android.widget.TextView;
import com.nostra13.universalimageloader.core.DisplayImageOptions;
import com.nostra13.universalimageloader.core.assist.ImageScaleType;
import org.schabi.newpipe.R;
import org.schabi.newpipe.database.LocalItem;
import org.schabi.newpipe.database.playlist.PlaylistStreamEntry;
import org.schabi.newpipe.extractor.NewPipe;
import org.schabi.newpipe.fragments.local.LocalItemBuilder;
import org.schabi.newpipe.util.ImageDisplayConstants;
import org.schabi.newpipe.util.Localization;
import java.text.DateFormat;
@@ -61,7 +58,8 @@ public class LocalPlaylistStreamItemHolder extends LocalItemHolder {
}
// Default thumbnail is shown on error, while loading and if the url is empty
itemBuilder.displayImage(item.thumbnailUrl, itemThumbnailView, DISPLAY_THUMBNAIL_OPTIONS);
itemBuilder.displayImage(item.thumbnailUrl, itemThumbnailView,
ImageDisplayConstants.DISPLAY_THUMBNAIL_OPTIONS);
itemView.setOnClickListener(view -> {
if (itemBuilder.getOnItemSelectedListener() != null) {
@@ -92,15 +90,4 @@ public class LocalPlaylistStreamItemHolder extends LocalItemHolder {
return false;
};
}
/**
* Display options for stream thumbnails
*/
private static final DisplayImageOptions DISPLAY_THUMBNAIL_OPTIONS =
new DisplayImageOptions.Builder()
.cloneFrom(BASE_DISPLAY_IMAGE_OPTIONS)
.showImageOnFail(R.drawable.dummy_thumbnail)
.showImageForEmptyUri(R.drawable.dummy_thumbnail)
.showImageOnLoading(R.drawable.dummy_thumbnail)
.build();
}

View File

@@ -6,13 +6,12 @@ import android.view.ViewGroup;
import android.widget.ImageView;
import android.widget.TextView;
import com.nostra13.universalimageloader.core.DisplayImageOptions;
import org.schabi.newpipe.R;
import org.schabi.newpipe.database.LocalItem;
import org.schabi.newpipe.database.stream.StreamStatisticsEntry;
import org.schabi.newpipe.extractor.NewPipe;
import org.schabi.newpipe.fragments.local.LocalItemBuilder;
import org.schabi.newpipe.util.ImageDisplayConstants;
import org.schabi.newpipe.util.Localization;
import java.text.DateFormat;
@@ -84,7 +83,8 @@ public class LocalStatisticStreamItemHolder extends LocalItemHolder {
itemAdditionalDetails.setText(getStreamInfoDetailLine(item, dateFormat));
// Default thumbnail is shown on error, while loading and if the url is empty
itemBuilder.displayImage(item.thumbnailUrl, itemThumbnailView, DISPLAY_THUMBNAIL_OPTIONS);
itemBuilder.displayImage(item.thumbnailUrl, itemThumbnailView,
ImageDisplayConstants.DISPLAY_THUMBNAIL_OPTIONS);
itemView.setOnClickListener(view -> {
if (itemBuilder.getOnItemSelectedListener() != null) {
@@ -100,15 +100,4 @@ public class LocalStatisticStreamItemHolder extends LocalItemHolder {
return true;
});
}
/**
* Display options for stream thumbnails
*/
public static final DisplayImageOptions DISPLAY_THUMBNAIL_OPTIONS =
new DisplayImageOptions.Builder()
.cloneFrom(BASE_DISPLAY_IMAGE_OPTIONS)
.showImageOnFail(R.drawable.dummy_thumbnail)
.showImageForEmptyUri(R.drawable.dummy_thumbnail)
.showImageOnLoading(R.drawable.dummy_thumbnail)
.build();
}

View File

@@ -4,8 +4,6 @@ import android.view.ViewGroup;
import android.widget.ImageView;
import android.widget.TextView;
import com.nostra13.universalimageloader.core.DisplayImageOptions;
import org.schabi.newpipe.R;
import org.schabi.newpipe.database.LocalItem;
import org.schabi.newpipe.fragments.local.LocalItemBuilder;
@@ -48,15 +46,4 @@ public abstract class PlaylistItemHolder extends LocalItemHolder {
return true;
});
}
/**
* Display options for playlist thumbnails
*/
public static final DisplayImageOptions DISPLAY_THUMBNAIL_OPTIONS =
new DisplayImageOptions.Builder()
.cloneFrom(BASE_DISPLAY_IMAGE_OPTIONS)
.showImageOnLoading(R.drawable.dummy_thumbnail_playlist)
.showImageForEmptyUri(R.drawable.dummy_thumbnail_playlist)
.showImageOnFail(R.drawable.dummy_thumbnail_playlist)
.build();
}

View File

@@ -6,6 +6,7 @@ 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.util.ImageDisplayConstants;
import org.schabi.newpipe.util.Localization;
import java.text.DateFormat;
@@ -26,7 +27,7 @@ public class RemotePlaylistItemHolder extends PlaylistItemHolder {
NewPipe.getNameOfService(item.getServiceId())));
itemBuilder.displayImage(item.getThumbnailUrl(), itemThumbnailView,
DISPLAY_THUMBNAIL_OPTIONS);
ImageDisplayConstants.DISPLAY_PLAYLIST_OPTIONS);
super.updateFromItem(localItem, dateFormat);
}

View File

@@ -0,0 +1,64 @@
package org.schabi.newpipe.fragments.subscription;
import android.app.AlertDialog;
import android.app.Dialog;
import android.content.Intent;
import android.os.Bundle;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.v4.app.DialogFragment;
import android.support.v4.app.Fragment;
import org.schabi.newpipe.R;
import org.schabi.newpipe.util.ThemeHelper;
import icepick.Icepick;
import icepick.State;
public class ImportConfirmationDialog extends DialogFragment {
@State
protected Intent resultServiceIntent;
public void setResultServiceIntent(Intent resultServiceIntent) {
this.resultServiceIntent = resultServiceIntent;
}
public static void show(@NonNull Fragment fragment, @NonNull Intent resultServiceIntent) {
if (fragment.getFragmentManager() == null) return;
final ImportConfirmationDialog confirmationDialog = new ImportConfirmationDialog();
confirmationDialog.setResultServiceIntent(resultServiceIntent);
confirmationDialog.show(fragment.getFragmentManager(), null);
}
@NonNull
@Override
public Dialog onCreateDialog(@Nullable Bundle savedInstanceState) {
return new AlertDialog.Builder(getContext(), ThemeHelper.getDialogTheme(getContext()))
.setMessage(R.string.import_network_expensive_warning)
.setCancelable(true)
.setNegativeButton(R.string.cancel, null)
.setPositiveButton(android.R.string.ok, (dialogInterface, i) -> {
if (resultServiceIntent != null && getContext() != null) {
getContext().startService(resultServiceIntent);
}
dismiss();
})
.create();
}
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
if (resultServiceIntent == null) throw new IllegalStateException("Result intent is null");
Icepick.restoreInstanceState(this, savedInstanceState);
}
@Override
public void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
Icepick.saveInstanceState(this, outState);
}
}

View File

@@ -1,30 +1,62 @@
package org.schabi.newpipe.fragments.subscription;
import android.app.Activity;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.graphics.Color;
import android.graphics.PorterDuff;
import android.os.Bundle;
import android.os.Environment;
import android.os.Parcelable;
import android.support.annotation.DrawableRes;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.v4.content.LocalBroadcastManager;
import android.support.v7.app.ActionBar;
import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageView;
import android.widget.TextView;
import android.widget.Toast;
import com.nononsenseapps.filepicker.Utils;
import org.schabi.newpipe.R;
import org.schabi.newpipe.database.subscription.SubscriptionEntity;
import org.schabi.newpipe.extractor.InfoItem;
import org.schabi.newpipe.extractor.NewPipe;
import org.schabi.newpipe.extractor.StreamingService;
import org.schabi.newpipe.extractor.channel.ChannelInfoItem;
import org.schabi.newpipe.extractor.exceptions.ExtractionException;
import org.schabi.newpipe.extractor.subscription.SubscriptionExtractor;
import org.schabi.newpipe.fragments.BaseStateFragment;
import org.schabi.newpipe.info_list.InfoListAdapter;
import org.schabi.newpipe.report.UserAction;
import org.schabi.newpipe.subscription.SubscriptionService;
import org.schabi.newpipe.subscription.services.SubscriptionsExportService;
import org.schabi.newpipe.subscription.services.SubscriptionsImportService;
import org.schabi.newpipe.util.FilePickerActivityHelper;
import org.schabi.newpipe.util.NavigationHelper;
import org.schabi.newpipe.util.OnClickGesture;
import org.schabi.newpipe.util.ServiceHelper;
import org.schabi.newpipe.util.ThemeHelper;
import org.schabi.newpipe.views.CollapsibleView;
import java.io.File;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.Date;
import java.util.List;
import java.util.Locale;
import icepick.State;
import io.reactivex.Observer;
@@ -33,18 +65,29 @@ 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.util.AnimationUtils.animateRotation;
import static org.schabi.newpipe.util.AnimationUtils.animateView;
public class SubscriptionFragment extends BaseStateFragment<List<SubscriptionEntity>> {
private View headerRootLayout;
private static final int REQUEST_EXPORT_CODE = 666;
private static final int REQUEST_IMPORT_CODE = 667;
private InfoListAdapter infoListAdapter;
private RecyclerView itemsList;
@State
protected Parcelable itemsListState;
private InfoListAdapter infoListAdapter;
private View headerRootLayout;
private View whatsNewItemListHeader;
private View importExportListHeader;
@State
protected Parcelable importExportOptionsState;
private CollapsibleView importExportOptions;
/* Used for independent events */
private CompositeDisposable disposables = new CompositeDisposable();
private SubscriptionService subscriptionService;
@@ -52,39 +95,48 @@ public class SubscriptionFragment extends BaseStateFragment<List<SubscriptionEnt
// Fragment LifeCycle
///////////////////////////////////////////////////////////////////////////
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setHasOptionsMenu(true);
}
@Override
public void setUserVisibleHint(boolean isVisibleToUser) {
super.setUserVisibleHint(isVisibleToUser);
if(isVisibleToUser && activity != null) {
activity.getSupportActionBar()
.setTitle(R.string.tab_subscriptions);
if (activity != null && isVisibleToUser) {
setTitle(activity.getString(R.string.tab_subscriptions));
}
}
@Override
public void onAttach(Context context) {
super.onAttach(context);
infoListAdapter = new InfoListAdapter(activity);
subscriptionService = SubscriptionService.getInstance();
subscriptionService = SubscriptionService.getInstance(activity);
}
@Nullable
@Override
public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, Bundle savedInstanceState) {
activity.getSupportActionBar().setDisplayShowTitleEnabled(true);
activity.setTitle(R.string.tab_subscriptions);
if(useAsFrontPage) {
activity.getSupportActionBar().setDisplayHomeAsUpEnabled(false);
}
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, Bundle savedInstanceState) {
return inflater.inflate(R.layout.fragment_subscription, container, false);
}
@Override
public void onResume() {
super.onResume();
setupBroadcastReceiver();
}
@Override
public void onPause() {
super.onPause();
itemsListState = itemsList.getLayoutManager().onSaveInstanceState();
importExportOptionsState = importExportOptions.onSaveInstanceState();
if (subscriptionBroadcastReceiver != null && activity != null) {
LocalBroadcastManager.getInstance(activity).unregisterReceiver(subscriptionBroadcastReceiver);
}
}
@Override
@@ -103,9 +155,131 @@ public class SubscriptionFragment extends BaseStateFragment<List<SubscriptionEnt
super.onDestroy();
}
///////////////////////////////////////////////////////////////////////////
/*/////////////////////////////////////////////////////////////////////////
// Menu
/////////////////////////////////////////////////////////////////////////*/
@Override
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
super.onCreateOptionsMenu(menu, inflater);
ActionBar supportActionBar = activity.getSupportActionBar();
if (supportActionBar != null) {
supportActionBar.setDisplayShowTitleEnabled(true);
setTitle(getString(R.string.tab_subscriptions));
}
}
/*//////////////////////////////////////////////////////////////////////////
// Subscriptions import/export
//////////////////////////////////////////////////////////////////////////*/
private BroadcastReceiver subscriptionBroadcastReceiver;
private void setupBroadcastReceiver() {
if (activity == null) return;
if (subscriptionBroadcastReceiver != null) {
LocalBroadcastManager.getInstance(activity).unregisterReceiver(subscriptionBroadcastReceiver);
}
final IntentFilter filters = new IntentFilter();
filters.addAction(SubscriptionsExportService.EXPORT_COMPLETE_ACTION);
filters.addAction(SubscriptionsImportService.IMPORT_COMPLETE_ACTION);
subscriptionBroadcastReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
if (importExportOptions != null) importExportOptions.collapse();
}
};
LocalBroadcastManager.getInstance(activity).registerReceiver(subscriptionBroadcastReceiver, filters);
}
private View addItemView(final String title, @DrawableRes final int icon, ViewGroup container) {
final View itemRoot = View.inflate(getContext(), R.layout.subscription_import_export_item, null);
final TextView titleView = itemRoot.findViewById(android.R.id.text1);
final ImageView iconView = itemRoot.findViewById(android.R.id.icon1);
titleView.setText(title);
iconView.setImageResource(icon);
container.addView(itemRoot);
return itemRoot;
}
private void setupImportFromItems(final ViewGroup listHolder) {
final View previousBackupItem = addItemView(getString(R.string.previous_export), ThemeHelper.resolveResourceIdFromAttr(getContext(), R.attr.ic_backup), listHolder);
previousBackupItem.setOnClickListener(item -> onImportPreviousSelected());
final int iconColor = ThemeHelper.isLightThemeSelected(getContext()) ? Color.BLACK : Color.WHITE;
final String[] services = getResources().getStringArray(R.array.service_list);
for (String serviceName : services) {
try {
final StreamingService service = NewPipe.getService(serviceName);
final SubscriptionExtractor subscriptionExtractor = service.getSubscriptionExtractor();
if (subscriptionExtractor == null) continue;
final List<SubscriptionExtractor.ContentSource> supportedSources = subscriptionExtractor.getSupportedSources();
if (supportedSources.isEmpty()) continue;
final View itemView = addItemView(serviceName, ServiceHelper.getIcon(service.getServiceId()), listHolder);
final ImageView iconView = itemView.findViewById(android.R.id.icon1);
iconView.setColorFilter(iconColor, PorterDuff.Mode.SRC_IN);
itemView.setOnClickListener(selectedItem -> onImportFromServiceSelected(service.getServiceId()));
} catch (ExtractionException e) {
throw new RuntimeException("Services array contains an entry that it's not a valid service name (" + serviceName + ")", e);
}
}
}
private void setupExportToItems(final ViewGroup listHolder) {
final View previousBackupItem = addItemView(getString(R.string.file), ThemeHelper.resolveResourceIdFromAttr(getContext(), R.attr.ic_save), listHolder);
previousBackupItem.setOnClickListener(item -> onExportSelected());
}
private void onImportFromServiceSelected(int serviceId) {
if (getParentFragment() == null) return;
NavigationHelper.openSubscriptionsImportFragment(getParentFragment().getFragmentManager(), serviceId);
}
private void onImportPreviousSelected() {
startActivityForResult(FilePickerActivityHelper.chooseSingleFile(activity), REQUEST_IMPORT_CODE);
}
private void onExportSelected() {
final String date = new SimpleDateFormat("yyyyMMddHHmm", Locale.ENGLISH).format(new Date());
final String exportName = "newpipe_subscriptions_" + date + ".json";
final File exportFile = new File(Environment.getExternalStorageDirectory(), exportName);
startActivityForResult(FilePickerActivityHelper.chooseFileToSave(activity, exportFile.getAbsolutePath()), REQUEST_EXPORT_CODE);
}
@Override
public void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if (data != null && data.getData() != null && resultCode == Activity.RESULT_OK) {
if (requestCode == REQUEST_EXPORT_CODE) {
final File exportFile = Utils.getFileForUri(data.getData());
if (!exportFile.getParentFile().canWrite() || !exportFile.getParentFile().canRead()) {
Toast.makeText(activity, R.string.invalid_directory, Toast.LENGTH_SHORT).show();
} else {
activity.startService(new Intent(activity, SubscriptionsExportService.class)
.putExtra(SubscriptionsExportService.KEY_FILE_PATH, exportFile.getAbsolutePath()));
}
} else if (requestCode == REQUEST_IMPORT_CODE) {
final String path = Utils.getFileForUri(data.getData()).getAbsolutePath();
ImportConfirmationDialog.show(this, new Intent(activity, SubscriptionsImportService.class)
.putExtra(KEY_MODE, PREVIOUS_EXPORT_MODE)
.putExtra(KEY_VALUE, path));
}
}
}
/*/////////////////////////////////////////////////////////////////////////
// Fragment Views
///////////////////////////////////////////////////////////////////////////
/////////////////////////////////////////////////////////////////////////*/
@Override
protected void initViews(View rootView, Bundle savedInstanceState) {
@@ -116,9 +290,27 @@ public class SubscriptionFragment extends BaseStateFragment<List<SubscriptionEnt
itemsList.setLayoutManager(new LinearLayoutManager(activity));
infoListAdapter.setHeader(headerRootLayout = activity.getLayoutInflater().inflate(R.layout.subscription_header, itemsList, false));
infoListAdapter.useMiniItemVariants(true);
whatsNewItemListHeader = headerRootLayout.findViewById(R.id.whats_new);
importExportListHeader = headerRootLayout.findViewById(R.id.import_export);
importExportOptions = headerRootLayout.findViewById(R.id.import_export_options);
infoListAdapter.useMiniItemVariants(true);
itemsList.setAdapter(infoListAdapter);
setupImportFromItems(headerRootLayout.findViewById(R.id.import_from_options));
setupExportToItems(headerRootLayout.findViewById(R.id.export_to_options));
if (importExportOptionsState != null) {
importExportOptions.onRestoreInstanceState(importExportOptionsState);
importExportOptionsState = null;
}
importExportOptions.addListener(getExpandIconSyncListener(headerRootLayout.findViewById(R.id.import_export_expand_icon)));
importExportOptions.ready();
}
private CollapsibleView.StateListener getExpandIconSyncListener(final ImageView iconView) {
return newState -> animateRotation(iconView, 250, newState == CollapsibleView.COLLAPSED ? 0 : 180);
}
@Override
@@ -130,12 +322,14 @@ public class SubscriptionFragment extends BaseStateFragment<List<SubscriptionEnt
public void selected(ChannelInfoItem selectedItem) {
// Requires the parent fragment to find holder for fragment replacement
NavigationHelper.openChannelFragment(getParentFragment().getFragmentManager(),
selectedItem.getServiceId(), selectedItem.url, selectedItem.getName());
selectedItem.getServiceId(), selectedItem.getUrl(), selectedItem.getName());
}
});
headerRootLayout.setOnClickListener(view ->
//noinspection ConstantConditions
whatsNewItemListHeader.setOnClickListener(v ->
NavigationHelper.openWhatsNewFragment(getParentFragment().getFragmentManager()));
importExportListHeader.setOnClickListener(v -> importExportOptions.switchState());
}
private void resetFragment() {
@@ -189,6 +383,7 @@ public class SubscriptionFragment extends BaseStateFragment<List<SubscriptionEnt
infoListAdapter.clearStreamItemList();
if (result.isEmpty()) {
whatsNewItemListHeader.setVisibility(View.GONE);
showEmptyState();
} else {
infoListAdapter.addInfoItemList(getSubscriptionItems(result));
@@ -196,7 +391,7 @@ public class SubscriptionFragment extends BaseStateFragment<List<SubscriptionEnt
itemsList.getLayoutManager().onRestoreInstanceState(itemsListState);
itemsListState = null;
}
whatsNewItemListHeader.setVisibility(View.VISIBLE);
hideLoading();
}
}
@@ -206,12 +401,8 @@ public class SubscriptionFragment extends BaseStateFragment<List<SubscriptionEnt
List<InfoItem> items = new ArrayList<>();
for (final SubscriptionEntity subscription : subscriptions) items.add(subscription.toChannelInfoItem());
Collections.sort(items, new Comparator<InfoItem>() {
@Override
public int compare(InfoItem o1, InfoItem o2) {
return o1.name.compareToIgnoreCase(o2.name);
}
});
Collections.sort(items,
(InfoItem o1, InfoItem o2) -> o1.getName().compareToIgnoreCase(o2.getName()));
return items;
}
@@ -231,12 +422,6 @@ public class SubscriptionFragment extends BaseStateFragment<List<SubscriptionEnt
animateView(itemsList, true, 200);
}
@Override
public void showEmptyState() {
super.showEmptyState();
animateView(itemsList, false, 200);
}
///////////////////////////////////////////////////////////////////////////
// Fragment Error Handling
///////////////////////////////////////////////////////////////////////////

View File

@@ -0,0 +1,210 @@
package org.schabi.newpipe.fragments.subscription;
import android.app.Activity;
import android.content.Intent;
import android.os.Bundle;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.annotation.StringRes;
import android.support.v4.text.util.LinkifyCompat;
import android.support.v7.app.ActionBar;
import android.text.TextUtils;
import android.text.util.Linkify;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Button;
import android.widget.EditText;
import android.widget.TextView;
import com.nononsenseapps.filepicker.Utils;
import org.schabi.newpipe.BaseFragment;
import org.schabi.newpipe.R;
import org.schabi.newpipe.extractor.NewPipe;
import org.schabi.newpipe.extractor.exceptions.ExtractionException;
import org.schabi.newpipe.extractor.subscription.SubscriptionExtractor;
import org.schabi.newpipe.report.ErrorActivity;
import org.schabi.newpipe.report.UserAction;
import org.schabi.newpipe.subscription.services.SubscriptionsImportService;
import org.schabi.newpipe.util.Constants;
import org.schabi.newpipe.util.FilePickerActivityHelper;
import org.schabi.newpipe.util.ServiceHelper;
import java.util.Collections;
import java.util.List;
import icepick.State;
import static org.schabi.newpipe.extractor.subscription.SubscriptionExtractor.ContentSource.CHANNEL_URL;
import static org.schabi.newpipe.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;
public class SubscriptionsImportFragment extends BaseFragment {
private static final int REQUEST_IMPORT_FILE_CODE = 666;
@State
protected int currentServiceId = Constants.NO_SERVICE_ID;
private List<SubscriptionExtractor.ContentSource> supportedSources;
private String relatedUrl;
@StringRes
private int instructionsString;
public static SubscriptionsImportFragment getInstance(int serviceId) {
SubscriptionsImportFragment instance = new SubscriptionsImportFragment();
instance.setInitialData(serviceId);
return instance;
}
public void setInitialData(int serviceId) {
this.currentServiceId = serviceId;
}
/*//////////////////////////////////////////////////////////////////////////
// Views
//////////////////////////////////////////////////////////////////////////*/
private TextView infoTextView;
private EditText inputText;
private Button inputButton;
///////////////////////////////////////////////////////////////////////////
// Fragment LifeCycle
///////////////////////////////////////////////////////////////////////////
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setupServiceVariables();
if (supportedSources.isEmpty() && currentServiceId != Constants.NO_SERVICE_ID) {
ErrorActivity.reportError(activity, Collections.emptyList(), null, null, ErrorActivity.ErrorInfo.make(UserAction.SOMETHING_ELSE,
NewPipe.getNameOfService(currentServiceId), "Service don't support importing", R.string.general_error));
activity.finish();
}
}
@Override
public void setUserVisibleHint(boolean isVisibleToUser) {
super.setUserVisibleHint(isVisibleToUser);
if (isVisibleToUser) {
setTitle(getString(R.string.import_title));
}
}
@Nullable
@Override
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, Bundle savedInstanceState) {
return inflater.inflate(R.layout.fragment_import, container, false);
}
/*/////////////////////////////////////////////////////////////////////////
// Fragment Views
/////////////////////////////////////////////////////////////////////////*/
@Override
protected void initViews(View rootView, Bundle savedInstanceState) {
super.initViews(rootView, savedInstanceState);
inputButton = rootView.findViewById(R.id.input_button);
inputText = rootView.findViewById(R.id.input_text);
infoTextView = rootView.findViewById(R.id.info_text_view);
// TODO: Support services that can import from more than one source (show the option to the user)
if (supportedSources.contains(CHANNEL_URL)) {
inputButton.setText(R.string.import_title);
inputText.setVisibility(View.VISIBLE);
inputText.setHint(ServiceHelper.getImportInstructionsHint(currentServiceId));
} else {
inputButton.setText(R.string.import_file_title);
}
if (instructionsString != 0) {
if (TextUtils.isEmpty(relatedUrl)) {
setInfoText(getString(instructionsString));
} else {
setInfoText(getString(instructionsString, relatedUrl));
}
} else {
setInfoText("");
}
ActionBar supportActionBar = activity.getSupportActionBar();
if (supportActionBar != null) {
supportActionBar.setDisplayShowTitleEnabled(true);
setTitle(getString(R.string.import_title));
}
}
@Override
protected void initListeners() {
super.initListeners();
inputButton.setOnClickListener(v -> onImportClicked());
}
private void onImportClicked() {
if (inputText.getVisibility() == View.VISIBLE) {
final String value = inputText.getText().toString();
if (!value.isEmpty()) onImportUrl(value);
} else {
onImportFile();
}
}
public void onImportUrl(String value) {
ImportConfirmationDialog.show(this, new Intent(activity, SubscriptionsImportService.class)
.putExtra(KEY_MODE, CHANNEL_URL_MODE)
.putExtra(KEY_VALUE, value)
.putExtra(Constants.KEY_SERVICE_ID, currentServiceId));
}
public void onImportFile() {
startActivityForResult(FilePickerActivityHelper.chooseSingleFile(activity), REQUEST_IMPORT_FILE_CODE);
}
@Override
public void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if (data == null) return;
if (resultCode == Activity.RESULT_OK && requestCode == REQUEST_IMPORT_FILE_CODE && data.getData() != null) {
final String path = Utils.getFileForUri(data.getData()).getAbsolutePath();
ImportConfirmationDialog.show(this, new Intent(activity, SubscriptionsImportService.class)
.putExtra(KEY_MODE, INPUT_STREAM_MODE)
.putExtra(KEY_VALUE, path)
.putExtra(Constants.KEY_SERVICE_ID, currentServiceId));
}
}
///////////////////////////////////////////////////////////////////////////
// Subscriptions
///////////////////////////////////////////////////////////////////////////
private void setupServiceVariables() {
if (currentServiceId != Constants.NO_SERVICE_ID) {
try {
final SubscriptionExtractor extractor = NewPipe.getService(currentServiceId).getSubscriptionExtractor();
supportedSources = extractor.getSupportedSources();
relatedUrl = extractor.getRelatedUrl();
instructionsString = ServiceHelper.getImportInstructions(currentServiceId);
return;
} catch (ExtractionException ignored) {
}
}
supportedSources = Collections.emptyList();
relatedUrl = null;
instructionsString = 0;
}
private void setInfoText(String infoString) {
infoTextView.setText(infoString);
LinkifyCompat.addLinks(infoTextView, Linkify.WEB_URLS);
}
}

View File

@@ -20,6 +20,7 @@ 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;
@@ -147,7 +148,7 @@ public class WatchHistoryFragment extends HistoryFragment<StreamHistoryEntry> {
holder.uploader.setText(entry.uploader);
holder.duration.setText(Localization.getDurationString(entry.duration));
ImageLoader.getInstance().displayImage(entry.thumbnailUrl, holder.thumbnailView,
StreamInfoItemHolder.DISPLAY_THUMBNAIL_OPTIONS);
ImageDisplayConstants.DISPLAY_THUMBNAIL_OPTIONS);
}
}

View File

@@ -60,7 +60,7 @@ public class InfoItemBuilder {
}
public View buildView(@NonNull ViewGroup parent, @NonNull final InfoItem infoItem, boolean useMiniVariant) {
InfoItemHolder holder = holderFromInfoType(parent, infoItem.info_type, useMiniVariant);
InfoItemHolder holder = holderFromInfoType(parent, infoItem.getInfoType(), useMiniVariant);
holder.updateFromItem(infoItem);
return holder.itemView;
}

View File

@@ -203,7 +203,7 @@ public class InfoListAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolde
return FOOTER_TYPE;
}
final InfoItem item = infoItemList.get(position);
switch (item.info_type) {
switch (item.getInfoType()) {
case STREAM:
return useMiniVariant ? MINI_STREAM_HOLDER_TYPE : STREAM_HOLDER_TYPE;
case CHANNEL:

View File

@@ -44,15 +44,16 @@ public class ChannelInfoItemHolder extends ChannelMiniInfoItemHolder {
if (!(infoItem instanceof ChannelInfoItem)) return;
final ChannelInfoItem item = (ChannelInfoItem) infoItem;
itemChannelDescriptionView.setText(item.description);
itemChannelDescriptionView.setText(item.getDescription());
}
@Override
protected String getDetailLine(final ChannelInfoItem item) {
String details = super.getDetailLine(item);
if (item.stream_count >= 0) {
String formattedVideoAmount = Localization.localizeStreamCount(itemBuilder.getContext(), item.stream_count);
if (item.getStreamCount() >= 0) {
String formattedVideoAmount = Localization.localizeStreamCount(itemBuilder.getContext(),
item.getStreamCount());
if (!details.isEmpty()) {
details += "" + formattedVideoAmount;

View File

@@ -1,15 +1,13 @@
package org.schabi.newpipe.info_list.holder;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
import com.nostra13.universalimageloader.core.DisplayImageOptions;
import org.schabi.newpipe.R;
import org.schabi.newpipe.extractor.InfoItem;
import org.schabi.newpipe.extractor.channel.ChannelInfoItem;
import org.schabi.newpipe.info_list.InfoItemBuilder;
import org.schabi.newpipe.util.ImageDisplayConstants;
import org.schabi.newpipe.util.Localization;
import de.hdodenhof.circleimageview.CircleImageView;
@@ -40,34 +38,23 @@ public class ChannelMiniInfoItemHolder extends InfoItemHolder {
itemAdditionalDetailView.setText(getDetailLine(item));
itemBuilder.getImageLoader()
.displayImage(item.thumbnail_url, itemThumbnailView, ChannelInfoItemHolder.DISPLAY_THUMBNAIL_OPTIONS);
.displayImage(item.getThumbnailUrl(),
itemThumbnailView,
ImageDisplayConstants.DISPLAY_THUMBNAIL_OPTIONS);
itemView.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
if (itemBuilder.getOnChannelSelectedListener() != null) {
itemBuilder.getOnChannelSelectedListener().selected(item);
}
itemView.setOnClickListener(view -> {
if (itemBuilder.getOnChannelSelectedListener() != null) {
itemBuilder.getOnChannelSelectedListener().selected(item);
}
});
}
protected String getDetailLine(final ChannelInfoItem item) {
String details = "";
if (item.subscriber_count >= 0) {
details += Localization.shortSubscriberCount(itemBuilder.getContext(), item.subscriber_count);
if (item.getSubscriberCount() >= 0) {
details += Localization.shortSubscriberCount(itemBuilder.getContext(),
item.getSubscriberCount());
}
return details;
}
/**
* Display options for channel thumbnails
*/
public static final DisplayImageOptions DISPLAY_THUMBNAIL_OPTIONS =
new DisplayImageOptions.Builder()
.cloneFrom(BASE_DISPLAY_IMAGE_OPTIONS)
.showImageOnLoading(R.drawable.buddy_channel_item)
.showImageForEmptyUri(R.drawable.buddy_channel_item)
.showImageOnFail(R.drawable.buddy_channel_item)
.build();
}

View File

@@ -4,8 +4,6 @@ import android.support.v7.widget.RecyclerView;
import android.view.LayoutInflater;
import android.view.ViewGroup;
import com.nostra13.universalimageloader.core.DisplayImageOptions;
import org.schabi.newpipe.extractor.InfoItem;
import org.schabi.newpipe.info_list.InfoItemBuilder;
@@ -38,16 +36,4 @@ public abstract class InfoItemHolder extends RecyclerView.ViewHolder {
}
public abstract void updateFromItem(final InfoItem infoItem);
/*//////////////////////////////////////////////////////////////////////////
// ImageLoaderOptions
//////////////////////////////////////////////////////////////////////////*/
/**
* Base display options
*/
public static final DisplayImageOptions BASE_DISPLAY_IMAGE_OPTIONS =
new DisplayImageOptions.Builder()
.cacheInMemory(true)
.build();
}

View File

@@ -4,12 +4,11 @@ import android.view.ViewGroup;
import android.widget.ImageView;
import android.widget.TextView;
import com.nostra13.universalimageloader.core.DisplayImageOptions;
import org.schabi.newpipe.R;
import org.schabi.newpipe.extractor.InfoItem;
import org.schabi.newpipe.extractor.playlist.PlaylistInfoItem;
import org.schabi.newpipe.info_list.InfoItemBuilder;
import org.schabi.newpipe.util.ImageDisplayConstants;
public class PlaylistMiniInfoItemHolder extends InfoItemHolder {
public final ImageView itemThumbnailView;
@@ -40,7 +39,8 @@ public class PlaylistMiniInfoItemHolder extends InfoItemHolder {
itemUploaderView.setText(item.getUploaderName());
itemBuilder.getImageLoader()
.displayImage(item.thumbnail_url, itemThumbnailView, DISPLAY_THUMBNAIL_OPTIONS);
.displayImage(item.getThumbnailUrl(), itemThumbnailView,
ImageDisplayConstants.DISPLAY_THUMBNAIL_OPTIONS);
itemView.setOnClickListener(view -> {
if (itemBuilder.getOnPlaylistSelectedListener() != null) {
@@ -56,15 +56,4 @@ public class PlaylistMiniInfoItemHolder extends InfoItemHolder {
return true;
});
}
/**
* Display options for playlist thumbnails
*/
public static final DisplayImageOptions DISPLAY_THUMBNAIL_OPTIONS =
new DisplayImageOptions.Builder()
.cloneFrom(BASE_DISPLAY_IMAGE_OPTIONS)
.showImageOnLoading(R.drawable.dummy_thumbnail_playlist)
.showImageForEmptyUri(R.drawable.dummy_thumbnail_playlist)
.showImageOnFail(R.drawable.dummy_thumbnail_playlist)
.build();
}

View File

@@ -51,14 +51,14 @@ public class StreamInfoItemHolder extends StreamMiniInfoItemHolder {
private String getStreamInfoDetailLine(final StreamInfoItem infoItem) {
String viewsAndDate = "";
if (infoItem.view_count >= 0) {
viewsAndDate = Localization.shortViewCount(itemBuilder.getContext(), infoItem.view_count);
if (infoItem.getViewCount() >= 0) {
viewsAndDate = Localization.shortViewCount(itemBuilder.getContext(), infoItem.getViewCount());
}
if (!TextUtils.isEmpty(infoItem.upload_date)) {
if (!TextUtils.isEmpty(infoItem.getUploadDate())) {
if (viewsAndDate.isEmpty()) {
viewsAndDate = infoItem.upload_date;
viewsAndDate = infoItem.getUploadDate();
} else {
viewsAndDate += "" + infoItem.upload_date;
viewsAndDate += "" + infoItem.getUploadDate();
}
}
return viewsAndDate;

View File

@@ -6,13 +6,12 @@ import android.view.ViewGroup;
import android.widget.ImageView;
import android.widget.TextView;
import com.nostra13.universalimageloader.core.DisplayImageOptions;
import org.schabi.newpipe.R;
import org.schabi.newpipe.extractor.InfoItem;
import org.schabi.newpipe.extractor.stream.StreamInfoItem;
import org.schabi.newpipe.extractor.stream.StreamType;
import org.schabi.newpipe.info_list.InfoItemBuilder;
import org.schabi.newpipe.util.ImageDisplayConstants;
import org.schabi.newpipe.util.Localization;
public class StreamMiniInfoItemHolder extends InfoItemHolder {
@@ -41,15 +40,17 @@ public class StreamMiniInfoItemHolder extends InfoItemHolder {
final StreamInfoItem item = (StreamInfoItem) infoItem;
itemVideoTitleView.setText(item.getName());
itemUploaderView.setText(item.uploader_name);
itemUploaderView.setText(item.getUploaderName());
if (item.duration > 0) {
itemDurationView.setText(Localization.getDurationString(item.duration));
itemDurationView.setBackgroundColor(ContextCompat.getColor(itemBuilder.getContext(), R.color.duration_background_color));
if (item.getDuration() > 0) {
itemDurationView.setText(Localization.getDurationString(item.getDuration()));
itemDurationView.setBackgroundColor(ContextCompat.getColor(itemBuilder.getContext(),
R.color.duration_background_color));
itemDurationView.setVisibility(View.VISIBLE);
} else if (item.stream_type == StreamType.LIVE_STREAM) {
} else if (item.getStreamType() == StreamType.LIVE_STREAM) {
itemDurationView.setText(R.string.duration_live);
itemDurationView.setBackgroundColor(ContextCompat.getColor(itemBuilder.getContext(), R.color.live_duration_background_color));
itemDurationView.setBackgroundColor(ContextCompat.getColor(itemBuilder.getContext(),
R.color.live_duration_background_color));
itemDurationView.setVisibility(View.VISIBLE);
} else {
itemDurationView.setVisibility(View.GONE);
@@ -57,25 +58,24 @@ public class StreamMiniInfoItemHolder extends InfoItemHolder {
// Default thumbnail is shown on error, while loading and if the url is empty
itemBuilder.getImageLoader()
.displayImage(item.thumbnail_url, itemThumbnailView, StreamInfoItemHolder.DISPLAY_THUMBNAIL_OPTIONS);
.displayImage(item.getThumbnailUrl(),
itemThumbnailView,
ImageDisplayConstants.DISPLAY_THUMBNAIL_OPTIONS);
itemView.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
if (itemBuilder.getOnStreamSelectedListener() != null) {
itemBuilder.getOnStreamSelectedListener().selected(item);
}
itemView.setOnClickListener(view -> {
if (itemBuilder.getOnStreamSelectedListener() != null) {
itemBuilder.getOnStreamSelectedListener().selected(item);
}
});
switch (item.stream_type) {
switch (item.getStreamType()) {
case AUDIO_STREAM:
case VIDEO_STREAM:
case FILE:
enableLongClick(item);
break;
case LIVE_STREAM:
case AUDIO_LIVE_STREAM:
enableLongClick(item);
break;
case FILE:
case NONE:
default:
disableLongClick();
@@ -85,14 +85,11 @@ public class StreamMiniInfoItemHolder extends InfoItemHolder {
private void enableLongClick(final StreamInfoItem item) {
itemView.setLongClickable(true);
itemView.setOnLongClickListener(new View.OnLongClickListener() {
@Override
public boolean onLongClick(View view) {
if (itemBuilder.getOnStreamSelectedListener() != null) {
itemBuilder.getOnStreamSelectedListener().held(item);
}
return true;
itemView.setOnLongClickListener(view -> {
if (itemBuilder.getOnStreamSelectedListener() != null) {
itemBuilder.getOnStreamSelectedListener().held(item);
}
return true;
});
}
@@ -100,15 +97,4 @@ public class StreamMiniInfoItemHolder extends InfoItemHolder {
itemView.setLongClickable(false);
itemView.setOnLongClickListener(null);
}
/**
* Display options for stream thumbnails
*/
public static final DisplayImageOptions DISPLAY_THUMBNAIL_OPTIONS =
new DisplayImageOptions.Builder()
.cloneFrom(BASE_DISPLAY_IMAGE_OPTIONS)
.showImageOnFail(R.drawable.dummy_thumbnail)
.showImageForEmptyUri(R.drawable.dummy_thumbnail)
.showImageOnLoading(R.drawable.dummy_thumbnail)
.build();
}

View File

@@ -33,6 +33,7 @@ import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.v4.app.NotificationCompat;
import android.util.Log;
import android.view.View;
import android.widget.RemoteViews;
import com.google.android.exoplayer2.PlaybackParameters;
@@ -46,6 +47,7 @@ import org.schabi.newpipe.extractor.stream.AudioStream;
import org.schabi.newpipe.extractor.stream.StreamInfo;
import org.schabi.newpipe.player.event.PlayerEventListener;
import org.schabi.newpipe.player.helper.LockManager;
import org.schabi.newpipe.player.helper.PlayerHelper;
import org.schabi.newpipe.playlist.PlayQueueItem;
import org.schabi.newpipe.util.ListHelper;
import org.schabi.newpipe.util.NavigationHelper;
@@ -75,6 +77,7 @@ public final class BackgroundPlayer extends Service {
private BasePlayerImpl basePlayerImpl;
private LockManager lockManager;
/*//////////////////////////////////////////////////////////////////////////
// Service-Activity Binder
//////////////////////////////////////////////////////////////////////////*/
@@ -115,8 +118,12 @@ public final class BackgroundPlayer extends Service {
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
if (DEBUG) Log.d(TAG, "onStartCommand() called with: intent = [" + intent + "], flags = [" + flags + "], startId = [" + startId + "]");
if (DEBUG) Log.d(TAG, "onStartCommand() called with: intent = [" + intent +
"], flags = [" + flags + "], startId = [" + startId + "]");
basePlayerImpl.handleIntent(intent);
if (basePlayerImpl.mediaSessionManager != null) {
basePlayerImpl.mediaSessionManager.handleMediaButtonIntent(intent);
}
return START_NOT_STICKY;
}
@@ -157,6 +164,11 @@ public final class BackgroundPlayer extends Service {
if (DEBUG) Log.d(TAG, "onScreenOnOff() called with: on = [" + on + "]");
shouldUpdateOnProgress = on;
basePlayerImpl.triggerProgressUpdate();
if (on) {
basePlayerImpl.startProgressLoop();
} else {
basePlayerImpl.stopProgressLoop();
}
}
/*//////////////////////////////////////////////////////////////////////////
@@ -291,15 +303,15 @@ public final class BackgroundPlayer extends Service {
}
@Override
public void onThumbnailReceived(Bitmap thumbnail) {
super.onThumbnailReceived(thumbnail);
public void onLoadingComplete(String imageUri, View view, Bitmap loadedImage) {
super.onLoadingComplete(imageUri, view, loadedImage);
if (thumbnail != null) {
if (loadedImage != null) {
// rebuild notification here since remote view does not release bitmaps, causing memory leaks
resetNotification();
if (notRemoteView != null) notRemoteView.setImageViewBitmap(R.id.notificationCover, thumbnail);
if (bigNotRemoteView != null) bigNotRemoteView.setImageViewBitmap(R.id.notificationCover, thumbnail);
if (notRemoteView != null) notRemoteView.setImageViewBitmap(R.id.notificationCover, loadedImage);
if (bigNotRemoteView != null) bigNotRemoteView.setImageViewBitmap(R.id.notificationCover, loadedImage);
updateNotification(-1);
}
@@ -378,29 +390,34 @@ public final class BackgroundPlayer extends Service {
// Playback Listener
//////////////////////////////////////////////////////////////////////////*/
@Override
public void sync(@NonNull final PlayQueueItem item, @Nullable final StreamInfo info) {
if (currentItem == item && currentInfo == info) return;
super.sync(item, info);
resetNotification();
updateNotification(-1);
updateMetadata();
protected void onMetadataChanged(@NonNull final PlayQueueItem item,
@Nullable final StreamInfo info,
final int newPlayQueueIndex,
final boolean hasPlayQueueItemChanged) {
if (shouldUpdateOnProgress || hasPlayQueueItemChanged) {
resetNotification();
updateNotification(-1);
updateMetadata();
}
}
@Override
@Nullable
public MediaSource sourceOf(final PlayQueueItem item, final StreamInfo info) {
final int index = ListHelper.getDefaultAudioFormat(context, info.audio_streams);
if (index < 0 || index >= info.audio_streams.size()) return null;
final MediaSource liveSource = super.sourceOf(item, info);
if (liveSource != null) return liveSource;
final AudioStream audio = info.audio_streams.get(index);
return buildMediaSource(audio.getUrl(), MediaFormat.getSuffixById(audio.getFormatId()));
final int index = ListHelper.getDefaultAudioFormat(context, info.getAudioStreams());
if (index < 0 || index >= info.getAudioStreams().size()) return null;
final AudioStream audio = info.getAudioStreams().get(index);
return buildMediaSource(audio.getUrl(), PlayerHelper.cacheKeyOf(info, audio),
MediaFormat.getSuffixById(audio.getFormatId()));
}
@Override
public void shutdown() {
super.shutdown();
public void onPlaybackShutdown() {
super.onPlaybackShutdown();
onClose();
}
@@ -429,7 +446,8 @@ public final class BackgroundPlayer extends Service {
private void updatePlayback() {
if (activityListener != null && simpleExoPlayer != null && playQueue != null) {
activityListener.onPlaybackUpdate(currentState, getRepeatMode(), playQueue.isShuffled(), getPlaybackParameters());
activityListener.onPlaybackUpdate(currentState, getRepeatMode(),
playQueue.isShuffled(), getPlaybackParameters());
}
}
@@ -477,7 +495,7 @@ public final class BackgroundPlayer extends Service {
onClose();
break;
case ACTION_PLAY_PAUSE:
onVideoPlayPause();
onPlayPause();
break;
case ACTION_REPEAT:
onRepeatClicked();
@@ -536,7 +554,6 @@ public final class BackgroundPlayer extends Service {
super.onPaused();
updateNotification(R.drawable.ic_play_arrow_white);
if (isProgressLoopRunning()) stopProgressLoop();
lockManager.releaseWifiAndCpu();
}

File diff suppressed because it is too large Load Diff

View File

@@ -19,7 +19,6 @@
package org.schabi.newpipe.player;
import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
@@ -31,8 +30,11 @@ import android.os.Build;
import android.os.Bundle;
import android.preference.PreferenceManager;
import android.provider.Settings;
import android.support.annotation.ColorInt;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.v4.app.ActivityCompat;
import android.support.v7.app.AppCompatActivity;
import android.support.v7.widget.RecyclerView;
import android.support.v7.widget.helper.ItemTouchHelper;
import android.util.DisplayMetrics;
@@ -50,6 +52,7 @@ import android.widget.TextView;
import android.widget.Toast;
import com.google.android.exoplayer2.Player;
import com.google.android.exoplayer2.text.CaptionStyleCompat;
import com.google.android.exoplayer2.ui.AspectRatioFrameLayout;
import com.google.android.exoplayer2.ui.SubtitleView;
@@ -57,37 +60,50 @@ import org.schabi.newpipe.R;
import org.schabi.newpipe.extractor.stream.StreamInfo;
import org.schabi.newpipe.extractor.stream.VideoStream;
import org.schabi.newpipe.fragments.OnScrollBelowItemsListener;
import org.schabi.newpipe.player.helper.PlaybackParameterDialog;
import org.schabi.newpipe.player.helper.PlayerHelper;
import org.schabi.newpipe.playlist.PlayQueueItem;
import org.schabi.newpipe.playlist.PlayQueueItemBuilder;
import org.schabi.newpipe.playlist.PlayQueueItemHolder;
import org.schabi.newpipe.playlist.PlayQueueItemTouchCallback;
import org.schabi.newpipe.util.AnimationUtils;
import org.schabi.newpipe.util.ListHelper;
import org.schabi.newpipe.util.NavigationHelper;
import org.schabi.newpipe.util.PermissionHelper;
import org.schabi.newpipe.util.StateSaver;
import org.schabi.newpipe.util.ThemeHelper;
import java.util.List;
import java.util.Queue;
import java.util.UUID;
import static org.schabi.newpipe.player.BasePlayer.STATE_PLAYING;
import static org.schabi.newpipe.player.VideoPlayer.DEFAULT_CONTROLS_DURATION;
import static org.schabi.newpipe.player.VideoPlayer.DEFAULT_CONTROLS_HIDE_TIME;
import static org.schabi.newpipe.util.AnimationUtils.Type.SLIDE_AND_ALPHA;
import static org.schabi.newpipe.util.AnimationUtils.animateRotation;
import static org.schabi.newpipe.util.AnimationUtils.animateView;
import static org.schabi.newpipe.util.StateSaver.KEY_SAVED_STATE;
/**
* Activity Player implementing VideoPlayer
*
* @author mauriciocolli
*/
public final class MainVideoPlayer extends Activity {
public final class MainVideoPlayer extends AppCompatActivity
implements StateSaver.WriteRead, PlaybackParameterDialog.Callback {
private static final String TAG = ".MainVideoPlayer";
private static final boolean DEBUG = BasePlayer.DEBUG;
private static final String PLAYER_STATE_INTENT = "player_state_intent";
private GestureDetector gestureDetector;
private boolean activityPaused;
private VideoPlayerImpl playerImpl;
private SharedPreferences defaultPreferences;
@Nullable private PlayerState playerState;
private boolean isInMultiWindow;
/*//////////////////////////////////////////////////////////////////////////
// Activity LifeCycle
//////////////////////////////////////////////////////////////////////////*/
@@ -101,41 +117,29 @@ public final class MainVideoPlayer extends Activity {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) getWindow().setStatusBarColor(Color.BLACK);
setVolumeControlStream(AudioManager.STREAM_MUSIC);
final Intent intent;
if (savedInstanceState != null && savedInstanceState.getParcelable(PLAYER_STATE_INTENT) != null) {
intent = savedInstanceState.getParcelable(PLAYER_STATE_INTENT);
} else {
intent = getIntent();
}
if (intent == null) {
Toast.makeText(this, R.string.general_error, Toast.LENGTH_SHORT).show();
finish();
return;
}
showSystemUi();
hideSystemUi();
setContentView(R.layout.activity_main_player);
playerImpl = new VideoPlayerImpl(this);
playerImpl.setup(findViewById(android.R.id.content));
playerImpl.handleIntent(intent);
if (savedInstanceState != null && savedInstanceState.get(KEY_SAVED_STATE) != null) {
return; // We have saved states, stop here to restore it
}
final Intent intent = getIntent();
if (intent != null) {
playerImpl.handleIntent(intent);
} else {
Toast.makeText(this, R.string.general_error, Toast.LENGTH_SHORT).show();
finish();
}
}
@Override
protected void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
if (this.playerImpl == null) return;
final Intent intent = NavigationHelper.getPlayerIntent(
getApplicationContext(),
this.getClass(),
this.playerImpl.getPlayQueue(),
this.playerImpl.getRepeatMode(),
this.playerImpl.getPlaybackSpeed(),
this.playerImpl.getPlaybackPitch(),
this.playerImpl.getPlaybackQuality()
);
outState.putParcelable(PLAYER_STATE_INTENT, intent);
protected void onRestoreInstanceState(@NonNull Bundle bundle) {
if (DEBUG) Log.d(TAG, "onRestoreInstanceState() called");
super.onRestoreInstanceState(bundle);
StateSaver.tryToRestore(bundle, this);
}
@Override
@@ -145,51 +149,30 @@ public final class MainVideoPlayer extends Activity {
playerImpl.handleIntent(intent);
}
@Override
public void onBackPressed() {
if (DEBUG) Log.d(TAG, "onBackPressed() called");
super.onBackPressed();
if (playerImpl.isPlaying()) playerImpl.getPlayer().setPlayWhenReady(false);
}
@Override
protected void onStop() {
super.onStop();
if (DEBUG) Log.d(TAG, "onStop() called");
activityPaused = true;
if (playerImpl.getPlayer() != null) {
playerImpl.wasPlaying = playerImpl.getPlayer().getPlayWhenReady();
playerImpl.setRecovery();
playerImpl.destroyPlayer();
}
}
@Override
protected void onResume() {
super.onResume();
if (DEBUG) Log.d(TAG, "onResume() called");
if (activityPaused) {
playerImpl.initPlayer();
playerImpl.getPlayPauseButton().setImageResource(R.drawable.ic_play_arrow_white);
super.onResume();
playerImpl.getPlayer().setPlayWhenReady(playerImpl.wasPlaying);
playerImpl.initPlayback(playerImpl.playQueue);
activityPaused = false;
if (globalScreenOrientationLocked()) {
boolean lastOrientationWasLandscape = defaultPreferences.getBoolean(
getString(R.string.last_orientation_landscape_key), false);
setLandscape(lastOrientationWasLandscape);
}
if(globalScreenOrientationLocked()) {
boolean lastOrientationWasLandscape
= defaultPreferences.getBoolean(getString(R.string.last_orientation_landscape_key), false);
setLandScape(lastOrientationWasLandscape);
}
}
@Override
protected void onDestroy() {
super.onDestroy();
if (DEBUG) Log.d(TAG, "onDestroy() called");
if (playerImpl != null) playerImpl.destroy();
// Upon going in or out of multiwindow mode, isInMultiWindow will always be false,
// since the first onResume needs to restore the player.
// Subsequent onResume calls while multiwindow mode remains the same and the player is
// prepared should be ignored.
if (isInMultiWindow) return;
isInMultiWindow = isInMultiWindow();
if (playerState != null) {
playerImpl.setPlaybackQuality(playerState.getPlaybackQuality());
playerImpl.initPlayback(playerState.getPlayQueue(), playerState.getRepeatMode(),
playerState.getPlaybackSpeed(), playerState.getPlaybackPitch(),
playerState.wasPlaying());
}
}
@Override
@@ -202,49 +185,104 @@ public final class MainVideoPlayer extends Activity {
}
}
@Override
protected void onSaveInstanceState(Bundle outState) {
if (DEBUG) Log.d(TAG, "onSaveInstanceState() called");
super.onSaveInstanceState(outState);
if (playerImpl == null) return;
playerImpl.setRecovery();
playerState = new PlayerState(playerImpl.getPlayQueue(), playerImpl.getRepeatMode(),
playerImpl.getPlaybackSpeed(), playerImpl.getPlaybackPitch(),
playerImpl.getPlaybackQuality(), playerImpl.isPlaying());
StateSaver.tryToSave(isChangingConfigurations(), null, outState, this);
}
@Override
protected void onStop() {
if (DEBUG) Log.d(TAG, "onStop() called");
super.onStop();
playerImpl.destroy();
}
/*//////////////////////////////////////////////////////////////////////////
// Utils
// State Saving
//////////////////////////////////////////////////////////////////////////*/
@Override
public String generateSuffix() {
return "." + UUID.randomUUID().toString() + ".player";
}
@Override
public void writeTo(Queue<Object> objectsToSave) {
if (objectsToSave == null) return;
objectsToSave.add(playerState);
}
@Override
@SuppressWarnings("unchecked")
public void readFrom(@NonNull Queue<Object> savedObjects) {
playerState = (PlayerState) savedObjects.poll();
}
/*//////////////////////////////////////////////////////////////////////////
// View
//////////////////////////////////////////////////////////////////////////*/
private void showSystemUi() {
if (DEBUG) Log.d(TAG, "showSystemUi() called");
if (playerImpl != null && playerImpl.queueVisible) return;
final int visibility;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
getWindow().getDecorView().setSystemUiVisibility(
View.SYSTEM_UI_FLAG_LAYOUT_STABLE
| View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
| View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
);
} else getWindow().getDecorView().setSystemUiVisibility(0);
visibility = View.SYSTEM_UI_FLAG_LAYOUT_STABLE
| View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
| View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION;
} else {
visibility = View.STATUS_BAR_VISIBLE;
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
@ColorInt final int systenUiColor =
ActivityCompat.getColor(getApplicationContext(), R.color.video_overlay_color);
getWindow().setStatusBarColor(systenUiColor);
getWindow().setNavigationBarColor(systenUiColor);
}
getWindow().getDecorView().setSystemUiVisibility(visibility);
getWindow().clearFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN);
}
private void hideSystemUi() {
if (DEBUG) Log.d(TAG, "hideSystemUi() called");
if (android.os.Build.VERSION.SDK_INT >= 16) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
int visibility = View.SYSTEM_UI_FLAG_LAYOUT_STABLE
| View.SYSTEM_UI_FLAG_FULLSCREEN
| View.SYSTEM_UI_FLAG_HIDE_NAVIGATION
| View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
| View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
| View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN;
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.KITKAT) visibility |= View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY;
| View.SYSTEM_UI_FLAG_FULLSCREEN
| View.SYSTEM_UI_FLAG_HIDE_NAVIGATION;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
visibility |= View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY;
}
getWindow().getDecorView().setSystemUiVisibility(visibility);
}
getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN, WindowManager.LayoutParams.FLAG_FULLSCREEN);
getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN,
WindowManager.LayoutParams.FLAG_FULLSCREEN);
}
private void toggleOrientation() {
setLandScape(!isLandScape());
setLandscape(!isLandscape());
defaultPreferences.edit()
.putBoolean(getString(R.string.last_orientation_landscape_key), !isLandScape())
.putBoolean(getString(R.string.last_orientation_landscape_key), !isLandscape())
.apply();
}
private boolean isLandScape() {
private boolean isLandscape() {
return getResources().getDisplayMetrics().heightPixels < getResources().getDisplayMetrics().widthPixels;
}
private void setLandScape(boolean v) {
private void setLandscape(boolean v) {
setRequestedOrientation(v
? ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE
: ActivityInfo.SCREEN_ORIENTATION_SENSOR_PORTRAIT);
@@ -279,6 +317,19 @@ public final class MainVideoPlayer extends Activity {
}
}
private boolean isInMultiWindow() {
return Build.VERSION.SDK_INT >= Build.VERSION_CODES.N && isInMultiWindowMode();
}
////////////////////////////////////////////////////////////////////////////
// Playback Parameters Listener
////////////////////////////////////////////////////////////////////////////
@Override
public void onPlaybackParameterChanged(float playbackTempo, float playbackPitch) {
if (playerImpl != null) playerImpl.setPlaybackParameters(playbackTempo, playbackPitch);
}
///////////////////////////////////////////////////////////////////////////
@SuppressWarnings({"unused", "WeakerAccess"})
@@ -307,6 +358,7 @@ public final class MainVideoPlayer extends Activity {
private ImageButton switchPopupButton;
private ImageButton switchBackgroundButton;
private RelativeLayout windowRootLayout;
private View secondaryControls;
VideoPlayerImpl(final Context context) {
@@ -334,6 +386,10 @@ public final class MainVideoPlayer extends Activity {
this.switchBackgroundButton = rootView.findViewById(R.id.switchBackground);
this.switchPopupButton = rootView.findViewById(R.id.switchPopup);
this.queueLayout = findViewById(R.id.playQueuePanel);
this.itemsListCloseButton = findViewById(R.id.playQueueClose);
this.itemsList = findViewById(R.id.playQueue);
titleTextView.setSelected(true);
channelTextView.setSelected(true);
@@ -342,20 +398,15 @@ public final class MainVideoPlayer extends Activity {
@Override
protected void setupSubtitleView(@NonNull SubtitleView view,
@NonNull String captionSizeKey) {
final float captionRatioInverse;
if (captionSizeKey.equals(getString(R.string.smaller_caption_size_key))) {
captionRatioInverse = 22f;
} else if (captionSizeKey.equals(getString(R.string.larger_caption_size_key))) {
captionRatioInverse = 18f;
} else {
captionRatioInverse = 20f;
}
final float captionScale,
@NonNull final CaptionStyleCompat captionStyle) {
final DisplayMetrics metrics = context.getResources().getDisplayMetrics();
final int minimumLength = Math.min(metrics.heightPixels, metrics.widthPixels);
final float captionRatioInverse = 20f + 4f * (1f - captionScale);
view.setFixedTextSize(TypedValue.COMPLEX_UNIT_PX,
(float) minimumLength / captionRatioInverse);
view.setApplyEmbeddedStyles(captionStyle.equals(CaptionStyleCompat.DEFAULT));
view.setStyle(captionStyle);
}
@Override
@@ -391,31 +442,32 @@ public final class MainVideoPlayer extends Activity {
updatePlaybackButtons();
}
/*//////////////////////////////////////////////////////////////////////////
// Playback Listener
//////////////////////////////////////////////////////////////////////////*/
@Override
public void shutdown() {
super.shutdown();
finish();
}
@Override
public void sync(@NonNull final PlayQueueItem item, @Nullable final StreamInfo info) {
super.sync(item, info);
titleTextView.setText(getVideoTitle());
channelTextView.setText(getUploaderName());
//playPauseButton.setImageResource(R.drawable.ic_pause_white);
}
@Override
public void onShuffleClicked() {
super.onShuffleClicked();
updatePlaybackButtons();
}
/*//////////////////////////////////////////////////////////////////////////
// Playback Listener
//////////////////////////////////////////////////////////////////////////*/
protected void onMetadataChanged(@NonNull final PlayQueueItem item,
@Nullable final StreamInfo info,
final int newPlayQueueIndex,
final boolean hasPlayQueueItemChanged) {
super.onMetadataChanged(item, info, newPlayQueueIndex, false);
titleTextView.setText(getVideoTitle());
channelTextView.setText(getUploaderName());
}
@Override
public void onPlaybackShutdown() {
super.onPlaybackShutdown();
finish();
}
/*//////////////////////////////////////////////////////////////////////////
// Player Overrides
//////////////////////////////////////////////////////////////////////////*/
@@ -475,7 +527,7 @@ public final class MainVideoPlayer extends Activity {
public void onClick(View v) {
super.onClick(v);
if (v.getId() == playPauseButton.getId()) {
onVideoPlayPause();
onPlayPause();
} else if (v.getId() == playPreviousButton.getId()) {
onPlayPrevious();
@@ -508,9 +560,9 @@ public final class MainVideoPlayer extends Activity {
if (getCurrentState() != STATE_COMPLETED) {
getControlsVisibilityHandler().removeCallbacksAndMessages(null);
animateView(getControlsRoot(), true, 300, 0, () -> {
animateView(getControlsRoot(), true, DEFAULT_CONTROLS_DURATION, 0, () -> {
if (getCurrentState() == STATE_PLAYING && !isSomePopupMenuVisible()) {
hideControls(300, DEFAULT_CONTROLS_HIDE_TIME);
hideControls(DEFAULT_CONTROLS_DURATION, DEFAULT_CONTROLS_HIDE_TIME);
}
});
}
@@ -524,29 +576,28 @@ public final class MainVideoPlayer extends Activity {
updatePlaybackButtons();
getControlsRoot().setVisibility(View.INVISIBLE);
queueLayout.setVisibility(View.VISIBLE);
animateView(queueLayout, SLIDE_AND_ALPHA, /*visible=*/true,
DEFAULT_CONTROLS_DURATION);
itemsList.scrollToPosition(playQueue.getIndex());
}
private void onQueueClosed() {
queueLayout.setVisibility(View.GONE);
animateView(queueLayout, SLIDE_AND_ALPHA, /*visible=*/false,
DEFAULT_CONTROLS_DURATION);
queueVisible = false;
}
private void onMoreOptionsClicked() {
if (DEBUG) Log.d(TAG, "onMoreOptionsClicked() called");
if (secondaryControls.getVisibility() == View.VISIBLE) {
moreOptionsButton.setImageDrawable(getResources().getDrawable(
R.drawable.ic_expand_more_white_24dp));
animateView(secondaryControls, false, 200);
} else {
moreOptionsButton.setImageDrawable(getResources().getDrawable(
R.drawable.ic_expand_less_white_24dp));
animateView(secondaryControls, true, 200);
}
showControls(300);
final boolean isMoreControlsVisible = secondaryControls.getVisibility() == View.VISIBLE;
animateRotation(moreOptionsButton, DEFAULT_CONTROLS_DURATION,
isMoreControlsVisible ? 0 : 180);
animateView(secondaryControls, SLIDE_AND_ALPHA, !isMoreControlsVisible,
DEFAULT_CONTROLS_DURATION);
showControls(DEFAULT_CONTROLS_DURATION);
}
private void onScreenRotationClicked() {
@@ -555,18 +606,23 @@ public final class MainVideoPlayer extends Activity {
showControlsThenHide();
}
@Override
public void onPlaybackSpeedClicked() {
PlaybackParameterDialog.newInstance(getPlaybackSpeed(), getPlaybackPitch())
.show(getSupportFragmentManager(), TAG);
}
@Override
public void onStopTrackingTouch(SeekBar seekBar) {
super.onStopTrackingTouch(seekBar);
if (wasPlaying()) {
hideControls(100, 0);
}
if (wasPlaying()) showControlsThenHide();
}
@Override
public void onDismiss(PopupMenu menu) {
super.onDismiss(menu);
if (isPlaying()) hideControls(300, 0);
if (isPlaying()) hideControls(DEFAULT_CONTROLS_DURATION, 0);
hideSystemUi();
}
@Override
@@ -624,7 +680,7 @@ public final class MainVideoPlayer extends Activity {
playPauseButton.setImageResource(R.drawable.ic_pause_white);
animatePlayButtons(true, 200);
});
showSystemUi();
getRootView().setKeepScreenOn(true);
}
@@ -650,10 +706,9 @@ public final class MainVideoPlayer extends Activity {
@Override
public void onCompleted() {
showSystemUi();
animateView(playPauseButton, AnimationUtils.Type.SCALE_AND_ALPHA, false, 0, 0, () -> {
playPauseButton.setImageResource(R.drawable.ic_replay_white);
animatePlayButtons(true, 300);
animatePlayButtons(true, DEFAULT_CONTROLS_DURATION);
});
getRootView().setKeepScreenOn(false);
@@ -683,8 +738,9 @@ public final class MainVideoPlayer extends Activity {
if (DEBUG) Log.d(TAG, "hideControls() called with: delay = [" + delay + "]");
getControlsVisibilityHandler().removeCallbacksAndMessages(null);
getControlsVisibilityHandler().postDelayed(() ->
animateView(getControlsRoot(), false, duration, 0, MainVideoPlayer.this::hideSystemUi),
delay
animateView(getControlsRoot(), false, duration, 0,
MainVideoPlayer.this::hideSystemUi),
/*delayMillis=*/delay
);
}
@@ -697,11 +753,6 @@ public final class MainVideoPlayer extends Activity {
}
private void buildQueue() {
queueLayout = findViewById(R.id.playQueuePanel);
itemsListCloseButton = findViewById(R.id.playQueueClose);
itemsList = findViewById(R.id.playQueue);
itemsList.setAdapter(playQueueAdapter);
itemsList.setClickable(true);
itemsList.setLongClickable(true);
@@ -731,31 +782,11 @@ public final class MainVideoPlayer extends Activity {
}
private ItemTouchHelper.SimpleCallback getItemTouchCallback() {
return new ItemTouchHelper.SimpleCallback(ItemTouchHelper.UP | ItemTouchHelper.DOWN, 0) {
return new PlayQueueItemTouchCallback() {
@Override
public boolean onMove(RecyclerView recyclerView, RecyclerView.ViewHolder source, RecyclerView.ViewHolder target) {
if (source.getItemViewType() != target.getItemViewType()) {
return false;
}
final int sourceIndex = source.getLayoutPosition();
final int targetIndex = target.getLayoutPosition();
playQueue.move(sourceIndex, targetIndex);
return true;
public void onMove(int sourceIndex, int targetIndex) {
if (playQueue != null) playQueue.move(sourceIndex, targetIndex);
}
@Override
public boolean isLongPressDragEnabled() {
return false;
}
@Override
public boolean isItemViewSwipeEnabled() {
return false;
}
@Override
public void onSwiped(RecyclerView.ViewHolder viewHolder, int swipeDir) {}
};
}
@@ -830,14 +861,22 @@ public final class MainVideoPlayer extends Activity {
if (DEBUG) Log.d(TAG, "onSingleTapConfirmed() called with: e = [" + e + "]");
if (playerImpl.getCurrentState() == BasePlayer.STATE_BLOCKED) return true;
if (playerImpl.isControlsVisible()) playerImpl.hideControls(150, 0);
else {
if (playerImpl.isControlsVisible()) {
playerImpl.hideControls(150, 0);
} else {
playerImpl.showControlsThenHide();
showSystemUi();
}
return true;
}
@Override
public boolean onDown(MotionEvent e) {
if (DEBUG) Log.d(TAG, "onDown() called with: e = [" + e + "]");
return super.onDown(e);
}
private final boolean isPlayerGestureEnabled = PlayerHelper.isPlayerGestureEnabled(getApplicationContext());
private final float stepsBrightness = 15, stepBrightness = (1f / stepsBrightness), minBrightness = .01f;
@@ -916,11 +955,15 @@ public final class MainVideoPlayer extends Activity {
eventsNum = 0;
/* if (playerImpl.getVolumeTextView().getVisibility() == View.VISIBLE) playerImpl.getVolumeTextView().setVisibility(View.GONE);
if (playerImpl.getBrightnessTextView().getVisibility() == View.VISIBLE) playerImpl.getBrightnessTextView().setVisibility(View.GONE);*/
if (playerImpl.getVolumeTextView().getVisibility() == View.VISIBLE) animateView(playerImpl.getVolumeTextView(), false, 200, 200);
if (playerImpl.getBrightnessTextView().getVisibility() == View.VISIBLE) animateView(playerImpl.getBrightnessTextView(), false, 200, 200);
if (playerImpl.getVolumeTextView().getVisibility() == View.VISIBLE) {
animateView(playerImpl.getVolumeTextView(), false, 200, 200);
}
if (playerImpl.getBrightnessTextView().getVisibility() == View.VISIBLE) {
animateView(playerImpl.getBrightnessTextView(), false, 200, 200);
}
if (playerImpl.isControlsVisible() && playerImpl.getCurrentState() == BasePlayer.STATE_PLAYING) {
playerImpl.hideControls(300, VideoPlayer.DEFAULT_CONTROLS_HIDE_TIME);
if (playerImpl.isControlsVisible() && playerImpl.getCurrentState() == STATE_PLAYING) {
playerImpl.hideControls(DEFAULT_CONTROLS_DURATION, DEFAULT_CONTROLS_HIDE_TIME);
}
}

View File

@@ -0,0 +1,88 @@
package org.schabi.newpipe.player;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.util.Log;
import com.google.gson.Gson;
import com.google.gson.JsonSyntaxException;
import org.schabi.newpipe.playlist.PlayQueue;
import java.io.Serializable;
public class PlayerState implements Serializable {
private final static String TAG = "PlayerState";
@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
//////////////////////////////////////////////////////////////////////////*/
@Nullable
public static PlayerState fromJson(@NonNull final String json) {
try {
return new Gson().fromJson(json, PlayerState.class);
} catch (JsonSyntaxException error) {
Log.e(TAG, "Failed to deserialize PlayerState from json=[" + json + "]", error);
return null;
}
}
@NonNull
public String toJson() {
return new Gson().toJson(this);
}
/*//////////////////////////////////////////////////////////////////////////
// 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;
}
}

View File

@@ -52,6 +52,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;
@@ -70,6 +71,9 @@ import org.schabi.newpipe.util.ThemeHelper;
import java.util.List;
import static org.schabi.newpipe.player.BasePlayer.STATE_PLAYING;
import static org.schabi.newpipe.player.VideoPlayer.DEFAULT_CONTROLS_DURATION;
import static org.schabi.newpipe.player.VideoPlayer.DEFAULT_CONTROLS_HIDE_TIME;
import static org.schabi.newpipe.player.helper.PlayerHelper.isUsingOldPlayer;
import static org.schabi.newpipe.util.AnimationUtils.animateView;
@@ -394,14 +398,12 @@ 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);
}
@Override
@@ -419,13 +421,15 @@ public final class PopupVideoPlayer extends Service {
}
@Override
public void onThumbnailReceived(Bitmap thumbnail) {
super.onThumbnailReceived(thumbnail);
if (thumbnail != null) {
public void onLoadingComplete(String imageUri, View view, Bitmap loadedImage) {
super.onLoadingComplete(imageUri, view, loadedImage);
if (loadedImage != null) {
// rebuild notification here since remote view does not release bitmaps, causing memory leaks
notBuilder = createNotification();
if (notRemoteView != null) notRemoteView.setImageViewBitmap(R.id.notificationCover, thumbnail);
if (notRemoteView != null) {
notRemoteView.setImageViewBitmap(R.id.notificationCover, loadedImage);
}
updateNotification(-1);
}
@@ -533,7 +537,8 @@ public final class PopupVideoPlayer extends Service {
private void updatePlayback() {
if (activityListener != null && simpleExoPlayer != null && playQueue != null) {
activityListener.onPlaybackUpdate(currentState, getRepeatMode(), playQueue.isShuffled(), simpleExoPlayer.getPlaybackParameters());
activityListener.onPlaybackUpdate(currentState, getRepeatMode(),
playQueue.isShuffled(), simpleExoPlayer.getPlaybackParameters());
}
}
@@ -572,16 +577,17 @@ public final class PopupVideoPlayer extends Service {
// Playback Listener
//////////////////////////////////////////////////////////////////////////*/
@Override
public void sync(@NonNull PlayQueueItem item, @Nullable StreamInfo info) {
if (currentItem == item && currentInfo == info) return;
super.sync(item, info);
protected void onMetadataChanged(@NonNull final PlayQueueItem item,
@Nullable final StreamInfo info,
final int newPlayQueueIndex,
final boolean hasPlayQueueItemChanged) {
super.onMetadataChanged(item, info, newPlayQueueIndex, false);
updateMetadata();
}
@Override
public void shutdown() {
super.shutdown();
public void onPlaybackShutdown() {
super.onPlaybackShutdown();
onClose();
}
@@ -611,7 +617,7 @@ public final class PopupVideoPlayer extends Service {
onClose();
break;
case ACTION_PLAY_PAUSE:
onVideoPlayPause();
onPlayPause();
break;
case ACTION_REPEAT:
onRepeatClicked();
@@ -646,6 +652,8 @@ public final class PopupVideoPlayer extends Service {
super.onPlaying();
updateNotification(R.drawable.ic_pause_white);
lockManager.acquireWifiAndCpu();
hideControls(DEFAULT_CONTROLS_DURATION, DEFAULT_CONTROLS_HIDE_TIME);
}
@Override
@@ -707,7 +715,7 @@ public final class PopupVideoPlayer extends Service {
public boolean onDoubleTap(MotionEvent e) {
if (DEBUG)
Log.d(TAG, "onDoubleTap() called with: e = [" + e + "]" + "rawXy = " + e.getRawX() + ", " + e.getRawY() + ", xy = " + e.getX() + ", " + e.getY());
if (playerImpl == null || !playerImpl.isPlaying() || !playerImpl.isPlayerReady()) return false;
if (playerImpl == null || !playerImpl.isPlaying()) return false;
if (e.getX() > popupWidth / 2) {
playerImpl.onFastForward();
@@ -722,7 +730,7 @@ public final class PopupVideoPlayer extends Service {
public boolean onSingleTapConfirmed(MotionEvent e) {
if (DEBUG) Log.d(TAG, "onSingleTapConfirmed() called with: e = [" + e + "]");
if (playerImpl == null || playerImpl.getPlayer() == null) return false;
playerImpl.onVideoPlayPause();
playerImpl.onPlayPause();
return true;
}
@@ -778,8 +786,8 @@ public final class PopupVideoPlayer extends Service {
private void onScrollEnd() {
if (DEBUG) Log.d(TAG, "onScrollEnd() called");
if (playerImpl == null) return;
if (playerImpl.isControlsVisible() && playerImpl.getCurrentState() == BasePlayer.STATE_PLAYING) {
playerImpl.hideControls(300, VideoPlayer.DEFAULT_CONTROLS_HIDE_TIME);
if (playerImpl.isControlsVisible() && playerImpl.getCurrentState() == STATE_PLAYING) {
playerImpl.hideControls(DEFAULT_CONTROLS_DURATION, DEFAULT_CONTROLS_HIDE_TIME);
}
}

View File

@@ -31,18 +31,25 @@ 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.player.event.PlayerEventListener;
import org.schabi.newpipe.player.helper.PlaybackParameterDialog;
import org.schabi.newpipe.playlist.PlayQueueAdapter;
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.util.Localization;
import org.schabi.newpipe.util.NavigationHelper;
import org.schabi.newpipe.util.ThemeHelper;
import java.util.Collections;
import java.util.List;
import static org.schabi.newpipe.player.helper.PlayerHelper.formatPitch;
import static org.schabi.newpipe.player.helper.PlayerHelper.formatSpeed;
public abstract class ServicePlayerActivity extends AppCompatActivity
implements PlayerEventListener, SeekBar.OnSeekBarChangeListener, View.OnClickListener {
implements PlayerEventListener, SeekBar.OnSeekBarChangeListener,
View.OnClickListener, PlaybackParameterDialog.Callback {
private boolean serviceBound;
private ServiceConnection serviceConnection;
@@ -56,14 +63,9 @@ public abstract class ServicePlayerActivity extends AppCompatActivity
////////////////////////////////////////////////////////////////////////////
private static final int RECYCLER_ITEM_POPUP_MENU_GROUP_ID = 47;
private static final int PLAYBACK_SPEED_POPUP_MENU_GROUP_ID = 61;
private static final int PLAYBACK_PITCH_POPUP_MENU_GROUP_ID = 97;
private static final int SMOOTH_SCROLL_MAXIMUM_DISTANCE = 80;
private static final int MINIMUM_INITIAL_DRAG_VELOCITY = 10;
private static final int MAXIMUM_INITIAL_DRAG_VELOCITY = 25;
private View rootView;
private RecyclerView itemsList;
@@ -76,6 +78,7 @@ public abstract class ServicePlayerActivity extends AppCompatActivity
private SeekBar progressSeekBar;
private TextView progressCurrentTime;
private TextView progressEndTime;
private TextView progressLiveSync;
private TextView seekDisplay;
private ImageButton repeatButton;
@@ -86,9 +89,7 @@ public abstract class ServicePlayerActivity extends AppCompatActivity
private ProgressBar progressBar;
private TextView playbackSpeedButton;
private PopupMenu playbackSpeedPopupMenu;
private TextView playbackPitchButton;
private PopupMenu playbackPitchPopupMenu;
////////////////////////////////////////////////////////////////////////////
// Abstracts
@@ -154,7 +155,7 @@ public abstract class ServicePlayerActivity extends AppCompatActivity
finish();
return true;
case R.id.action_append_playlist:
appendToPlaylist();
appendAllToPlaylist();
return true;
case R.id.action_settings:
NavigationHelper.openSettings(this);
@@ -190,13 +191,6 @@ public abstract class ServicePlayerActivity extends AppCompatActivity
).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
}
private void appendToPlaylist() {
if (this.player != null && this.player.getPlayQueue() != null) {
PlaylistAppendDialog.fromPlayQueueItems(this.player.getPlayQueue().getStreams())
.show(getSupportFragmentManager(), getTag());
}
}
////////////////////////////////////////////////////////////////////////////
// Service Connection
////////////////////////////////////////////////////////////////////////////
@@ -294,9 +288,11 @@ public abstract class ServicePlayerActivity extends AppCompatActivity
progressCurrentTime = rootView.findViewById(R.id.current_time);
progressSeekBar = rootView.findViewById(R.id.seek_bar);
progressEndTime = rootView.findViewById(R.id.end_time);
progressLiveSync = rootView.findViewById(R.id.live_sync);
seekDisplay = rootView.findViewById(R.id.seek_display);
progressSeekBar.setOnSeekBarChangeListener(this);
progressLiveSync.setOnClickListener(this);
}
private void buildControls() {
@@ -316,50 +312,12 @@ public abstract class ServicePlayerActivity extends AppCompatActivity
shuffleButton.setOnClickListener(this);
playbackSpeedButton.setOnClickListener(this);
playbackPitchButton.setOnClickListener(this);
playbackSpeedPopupMenu = new PopupMenu(this, playbackSpeedButton);
playbackPitchPopupMenu = new PopupMenu(this, playbackPitchButton);
buildPlaybackSpeedMenu();
buildPlaybackPitchMenu();
}
private void buildPlaybackSpeedMenu() {
if (playbackSpeedPopupMenu == null) return;
playbackSpeedPopupMenu.getMenu().removeGroup(PLAYBACK_SPEED_POPUP_MENU_GROUP_ID);
for (int i = 0; i < BasePlayer.PLAYBACK_SPEEDS.length; i++) {
final float playbackSpeed = BasePlayer.PLAYBACK_SPEEDS[i];
final String formattedSpeed = formatSpeed(playbackSpeed);
final MenuItem item = playbackSpeedPopupMenu.getMenu().add(PLAYBACK_SPEED_POPUP_MENU_GROUP_ID, i, Menu.NONE, formattedSpeed);
item.setOnMenuItemClickListener(menuItem -> {
if (player == null) return false;
player.setPlaybackSpeed(playbackSpeed);
return true;
});
}
}
private void buildPlaybackPitchMenu() {
if (playbackPitchPopupMenu == null) return;
playbackPitchPopupMenu.getMenu().removeGroup(PLAYBACK_PITCH_POPUP_MENU_GROUP_ID);
for (int i = 0; i < BasePlayer.PLAYBACK_PITCHES.length; i++) {
final float playbackPitch = BasePlayer.PLAYBACK_PITCHES[i];
final String formattedPitch = formatPitch(playbackPitch);
final MenuItem item = playbackPitchPopupMenu.getMenu().add(PLAYBACK_PITCH_POPUP_MENU_GROUP_ID, i, Menu.NONE, formattedPitch);
item.setOnMenuItemClickListener(menuItem -> {
if (player == null) return false;
player.setPlaybackPitch(playbackPitch);
return true;
});
}
}
private void buildItemPopupMenu(final PlayQueueItem item, final View view) {
final PopupMenu menu = new PopupMenu(this, view);
final MenuItem remove = menu.getMenu().add(RECYCLER_ITEM_POPUP_MENU_GROUP_ID, 0, Menu.NONE, R.string.play_queue_remove);
final MenuItem remove = menu.getMenu().add(RECYCLER_ITEM_POPUP_MENU_GROUP_ID, /*pos=*/0,
Menu.NONE, R.string.play_queue_remove);
remove.setOnMenuItemClickListener(menuItem -> {
if (player == null) return false;
@@ -368,12 +326,20 @@ public abstract class ServicePlayerActivity extends AppCompatActivity
return true;
});
final MenuItem detail = menu.getMenu().add(RECYCLER_ITEM_POPUP_MENU_GROUP_ID, 1, Menu.NONE, R.string.play_queue_stream_detail);
final MenuItem detail = menu.getMenu().add(RECYCLER_ITEM_POPUP_MENU_GROUP_ID, /*pos=*/1,
Menu.NONE, R.string.play_queue_stream_detail);
detail.setOnMenuItemClickListener(menuItem -> {
onOpenDetail(item.getServiceId(), item.getUrl(), item.getTitle());
return true;
});
final MenuItem append = menu.getMenu().add(RECYCLER_ITEM_POPUP_MENU_GROUP_ID, /*pos=*/2,
Menu.NONE, R.string.append_playlist);
append.setOnMenuItemClickListener(menuItem -> {
openPlaylistAppendDialog(Collections.singletonList(item));
return true;
});
menu.show();
}
@@ -395,43 +361,11 @@ public abstract class ServicePlayerActivity extends AppCompatActivity
}
private ItemTouchHelper.SimpleCallback getItemTouchCallback() {
return new ItemTouchHelper.SimpleCallback(ItemTouchHelper.UP | ItemTouchHelper.DOWN, 0) {
return new PlayQueueItemTouchCallback() {
@Override
public int interpolateOutOfBoundsScroll(RecyclerView recyclerView, int viewSize,
int viewSizeOutOfBounds, int totalSize,
long msSinceStartScroll) {
final int standardSpeed = super.interpolateOutOfBoundsScroll(recyclerView, viewSize,
viewSizeOutOfBounds, totalSize, msSinceStartScroll);
final int clampedAbsVelocity = Math.max(MINIMUM_INITIAL_DRAG_VELOCITY,
Math.min(Math.abs(standardSpeed), MAXIMUM_INITIAL_DRAG_VELOCITY));
return clampedAbsVelocity * (int) Math.signum(viewSizeOutOfBounds);
}
@Override
public boolean onMove(RecyclerView recyclerView, RecyclerView.ViewHolder source,
RecyclerView.ViewHolder target) {
if (source.getItemViewType() != target.getItemViewType()) {
return false;
}
final int sourceIndex = source.getLayoutPosition();
final int targetIndex = target.getLayoutPosition();
public void onMove(int sourceIndex, int targetIndex) {
if (player != null) player.getPlayQueue().move(sourceIndex, targetIndex);
return true;
}
@Override
public boolean isLongPressDragEnabled() {
return false;
}
@Override
public boolean isItemViewSwipeEnabled() {
return false;
}
@Override
public void onSwiped(RecyclerView.ViewHolder viewHolder, int swipeDir) {}
};
}
@@ -496,7 +430,7 @@ public abstract class ServicePlayerActivity extends AppCompatActivity
player.onPlayPrevious();
} else if (view.getId() == playPauseButton.getId()) {
player.onVideoPlayPause();
player.onPlayPause();
} else if (view.getId() == forwardButton.getId()) {
player.onPlayNext();
@@ -505,17 +439,35 @@ public abstract class ServicePlayerActivity extends AppCompatActivity
player.onShuffleClicked();
} else if (view.getId() == playbackSpeedButton.getId()) {
playbackSpeedPopupMenu.show();
openPlaybackParameterDialog();
} else if (view.getId() == playbackPitchButton.getId()) {
playbackPitchPopupMenu.show();
openPlaybackParameterDialog();
} else if (view.getId() == metadata.getId()) {
scrollToSelected();
} else if (view.getId() == progressLiveSync.getId()) {
player.seekToDefault();
}
}
////////////////////////////////////////////////////////////////////////////
// Playback Parameters
////////////////////////////////////////////////////////////////////////////
private void openPlaybackParameterDialog() {
if (player == null) return;
PlaybackParameterDialog.newInstance(player.getPlaybackSpeed(),
player.getPlaybackPitch()).show(getSupportFragmentManager(), getTag());
}
@Override
public void onPlaybackParameterChanged(float playbackTempo, float playbackPitch) {
if (player != null) player.setPlaybackParameters(playbackTempo, playbackPitch);
}
////////////////////////////////////////////////////////////////////////////
// Seekbar Listener
////////////////////////////////////////////////////////////////////////////
@@ -537,11 +489,26 @@ public abstract class ServicePlayerActivity extends AppCompatActivity
@Override
public void onStopTrackingTouch(SeekBar seekBar) {
if (player != null) player.simpleExoPlayer.seekTo(seekBar.getProgress());
if (player != null) player.seekTo(seekBar.getProgress());
seekDisplay.setVisibility(View.GONE);
seeking = false;
}
////////////////////////////////////////////////////////////////////////////
// Playlist append
////////////////////////////////////////////////////////////////////////////
private void appendAllToPlaylist() {
if (player != null && player.getPlayQueue() != null) {
openPlaylistAppendDialog(player.getPlayQueue().getStreams());
}
}
private void openPlaylistAppendDialog(final List<PlayQueueItem> playlist) {
PlaylistAppendDialog.fromPlayQueueItems(playlist)
.show(getSupportFragmentManager(), getTag());
}
////////////////////////////////////////////////////////////////////////////
// Binding Service Listener
////////////////////////////////////////////////////////////////////////////
@@ -551,6 +518,7 @@ public abstract class ServicePlayerActivity extends AppCompatActivity
onStateChanged(state);
onPlayModeChanged(repeatMode, shuffled);
onPlaybackParameterChanged(parameters);
onMaybePlaybackAdapterChanged();
}
@Override
@@ -567,13 +535,30 @@ public abstract class ServicePlayerActivity extends AppCompatActivity
progressSeekBar.setProgress(currentProgress);
progressCurrentTime.setText(Localization.getDurationString(currentProgress / 1000));
}
if (player != null) {
progressLiveSync.setClickable(!player.isLiveEdge());
}
}
@Override
public void onMetadataUpdate(StreamInfo info) {
if (info != null) {
metadataTitle.setText(info.getName());
metadataArtist.setText(info.uploader_name);
metadataArtist.setText(info.getUploaderName());
progressEndTime.setVisibility(View.GONE);
progressLiveSync.setVisibility(View.GONE);
switch (info.getStreamType()) {
case LIVE_STREAM:
case AUDIO_LIVE_STREAM:
progressLiveSync.setVisibility(View.VISIBLE);
break;
default:
progressEndTime.setVisibility(View.VISIBLE);
break;
}
scrollToSelected();
}
}
@@ -646,4 +631,12 @@ public abstract class ServicePlayerActivity extends AppCompatActivity
playbackPitchButton.setText(formatPitch(parameters.pitch));
}
}
private void onMaybePlaybackAdapterChanged() {
if (itemsList == null || player == null) return;
final PlayQueueAdapter maybeNewAdapter = player.getPlayQueueAdapter();
if (maybeNewAdapter != null && itemsList.getAdapter() != maybeNewAdapter) {
itemsList.setAdapter(maybeNewAdapter);
}
}
}

View File

@@ -32,7 +32,6 @@ import android.graphics.PorterDuff;
import android.net.Uri;
import android.os.Build;
import android.os.Handler;
import android.preference.PreferenceManager;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.v4.content.ContextCompat;
@@ -49,22 +48,24 @@ import android.widget.TextView;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.PlaybackParameters;
import com.google.android.exoplayer2.Player;
import com.google.android.exoplayer2.SimpleExoPlayer;
import com.google.android.exoplayer2.source.MediaSource;
import com.google.android.exoplayer2.source.MergingMediaSource;
import com.google.android.exoplayer2.source.SingleSampleMediaSource;
import com.google.android.exoplayer2.source.TrackGroup;
import com.google.android.exoplayer2.source.TrackGroupArray;
import com.google.android.exoplayer2.text.CaptionStyleCompat;
import com.google.android.exoplayer2.trackselection.TrackSelectionArray;
import com.google.android.exoplayer2.ui.AspectRatioFrameLayout;
import com.google.android.exoplayer2.ui.SubtitleView;
import com.google.android.exoplayer2.video.VideoListener;
import org.schabi.newpipe.R;
import org.schabi.newpipe.extractor.MediaFormat;
import org.schabi.newpipe.extractor.Subtitles;
import org.schabi.newpipe.extractor.stream.AudioStream;
import org.schabi.newpipe.extractor.stream.StreamInfo;
import org.schabi.newpipe.extractor.stream.StreamType;
import org.schabi.newpipe.extractor.stream.VideoStream;
import org.schabi.newpipe.player.helper.PlayerHelper;
import org.schabi.newpipe.playlist.PlayQueueItem;
@@ -87,7 +88,7 @@ import static org.schabi.newpipe.util.AnimationUtils.animateView;
*/
@SuppressWarnings({"WeakerAccess", "unused"})
public abstract class VideoPlayer extends BasePlayer
implements SimpleExoPlayer.VideoListener,
implements VideoListener,
SeekBar.OnSeekBarChangeListener,
View.OnClickListener,
Player.EventListener,
@@ -101,6 +102,7 @@ public abstract class VideoPlayer extends BasePlayer
//////////////////////////////////////////////////////////////////////////*/
protected static final int RENDERER_UNAVAILABLE = -1;
public static final int DEFAULT_CONTROLS_DURATION = 300; // 300 millis
public static final int DEFAULT_CONTROLS_HIDE_TIME = 2000; // 2 Seconds
private ArrayList<VideoStream> availableStreams;
@@ -131,6 +133,7 @@ public abstract class VideoPlayer extends BasePlayer
private SeekBar playbackSeekBar;
private TextView playbackCurrentTime;
private TextView playbackEndTime;
private TextView playbackLiveSync;
private TextView playbackSpeedTextView;
private View topControlsRoot;
@@ -159,7 +162,6 @@ public abstract class VideoPlayer extends BasePlayer
public VideoPlayer(String debugTag, Context context) {
super(context);
this.TAG = debugTag;
this.context = context;
}
public void setup(View rootView) {
@@ -180,16 +182,17 @@ public abstract class VideoPlayer extends BasePlayer
this.playbackSeekBar = rootView.findViewById(R.id.playbackSeekBar);
this.playbackCurrentTime = rootView.findViewById(R.id.playbackCurrentTime);
this.playbackEndTime = rootView.findViewById(R.id.playbackEndTime);
this.playbackLiveSync = rootView.findViewById(R.id.playbackLiveSync);
this.playbackSpeedTextView = rootView.findViewById(R.id.playbackSpeed);
this.bottomControlsRoot = rootView.findViewById(R.id.bottomControls);
this.topControlsRoot = rootView.findViewById(R.id.topControls);
this.qualityTextView = rootView.findViewById(R.id.qualityTextView);
this.subtitleView = rootView.findViewById(R.id.subtitleView);
final String captionSizeKey = PreferenceManager.getDefaultSharedPreferences(context)
.getString(context.getString(R.string.caption_size_key),
context.getString(R.string.caption_size_default));
setupSubtitleView(subtitleView, captionSizeKey);
final float captionScale = PlayerHelper.getCaptionScale(context);
final CaptionStyleCompat captionStyle = PlayerHelper.getCaptionStyle(context);
setupSubtitleView(subtitleView, captionScale, captionStyle);
this.resizeView = rootView.findViewById(R.id.resizeTextView);
resizeView.setText(PlayerHelper.resizeTypeOf(context, aspectRatioFrameLayout.getResizeMode()));
@@ -211,7 +214,8 @@ public abstract class VideoPlayer extends BasePlayer
}
protected abstract void setupSubtitleView(@NonNull SubtitleView view,
@NonNull String captionSizeKey);
final float captionScale,
@NonNull final CaptionStyleCompat captionStyle);
@Override
public void initListeners() {
@@ -221,11 +225,12 @@ public abstract class VideoPlayer extends BasePlayer
qualityTextView.setOnClickListener(this);
captionTextView.setOnClickListener(this);
resizeView.setOnClickListener(this);
playbackLiveSync.setOnClickListener(this);
}
@Override
public void initPlayer() {
super.initPlayer();
public void initPlayer(final boolean playOnReady) {
super.initPlayer(playOnReady);
// Setup video view
simpleExoPlayer.setVideoSurfaceView(surfaceView);
@@ -261,7 +266,8 @@ public abstract class VideoPlayer extends BasePlayer
qualityPopupMenu.getMenu().removeGroup(qualityPopupMenuGroupId);
for (int i = 0; i < availableStreams.size(); i++) {
VideoStream videoStream = availableStreams.get(i);
qualityPopupMenu.getMenu().add(qualityPopupMenuGroupId, i, Menu.NONE, MediaFormat.getNameById(videoStream.format) + " " + videoStream.resolution);
qualityPopupMenu.getMenu().add(qualityPopupMenuGroupId, i, Menu.NONE,
MediaFormat.getNameById(videoStream.getFormatId()) + " " + videoStream.resolution);
}
if (getSelectedVideoStream() != null) {
qualityTextView.setText(getSelectedVideoStream().resolution);
@@ -305,8 +311,7 @@ public abstract class VideoPlayer extends BasePlayer
captionItem.setOnMenuItemClickListener(menuItem -> {
final int textRendererIndex = getRendererIndex(C.TRACK_TYPE_TEXT);
if (trackSelector != null && textRendererIndex != RENDERER_UNAVAILABLE) {
trackSelector.setParameters(trackSelector.getParameters()
.withPreferredTextLanguage(captionLanguage));
trackSelector.setPreferredTextLanguage(captionLanguage);
trackSelector.setRendererDisabled(textRendererIndex, false);
}
return true;
@@ -322,27 +327,53 @@ public abstract class VideoPlayer extends BasePlayer
protected abstract int getOverrideResolutionIndex(final List<VideoStream> sortedVideos, final String playbackQuality);
@Override
public void sync(@NonNull final PlayQueueItem item, @Nullable final StreamInfo info) {
super.sync(item, info);
protected void onMetadataChanged(@NonNull final PlayQueueItem item,
@Nullable final StreamInfo info,
final int newPlayQueueIndex,
final boolean hasPlayQueueItemChanged) {
qualityTextView.setVisibility(View.GONE);
playbackSpeedTextView.setVisibility(View.GONE);
if (info != null && info.video_streams.size() + info.video_only_streams.size() > 0) {
final List<VideoStream> videos = ListHelper.getSortedStreamVideosList(context,
info.video_streams, info.video_only_streams, false);
availableStreams = new ArrayList<>(videos);
if (playbackQuality == null) {
selectedStreamIndex = getDefaultResolutionIndex(videos);
} else {
selectedStreamIndex = getOverrideResolutionIndex(videos, getPlaybackQuality());
}
playbackEndTime.setVisibility(View.GONE);
playbackLiveSync.setVisibility(View.GONE);
buildQualityMenu();
qualityTextView.setVisibility(View.VISIBLE);
surfaceView.setVisibility(View.VISIBLE);
} else {
surfaceView.setVisibility(View.GONE);
final StreamType streamType = info == null ? StreamType.NONE : info.getStreamType();
switch (streamType) {
case AUDIO_STREAM:
surfaceView.setVisibility(View.GONE);
playbackEndTime.setVisibility(View.VISIBLE);
break;
case AUDIO_LIVE_STREAM:
surfaceView.setVisibility(View.GONE);
playbackLiveSync.setVisibility(View.VISIBLE);
break;
case LIVE_STREAM:
surfaceView.setVisibility(View.VISIBLE);
playbackLiveSync.setVisibility(View.VISIBLE);
break;
case VIDEO_STREAM:
if (info.getVideoStreams().size() + info.getVideoOnlyStreams().size() == 0) break;
final List<VideoStream> videos = ListHelper.getSortedStreamVideosList(context,
info.getVideoStreams(), info.getVideoOnlyStreams(), false);
availableStreams = new ArrayList<>(videos);
if (playbackQuality == null) {
selectedStreamIndex = getDefaultResolutionIndex(videos);
} else {
selectedStreamIndex = getOverrideResolutionIndex(videos, getPlaybackQuality());
}
buildQualityMenu();
qualityTextView.setVisibility(View.VISIBLE);
surfaceView.setVisibility(View.VISIBLE);
default:
playbackEndTime.setVisibility(View.VISIBLE);
break;
}
buildPlaybackSpeedMenu();
@@ -352,11 +383,14 @@ public abstract class VideoPlayer extends BasePlayer
@Override
@Nullable
public MediaSource sourceOf(final PlayQueueItem item, final StreamInfo info) {
final MediaSource liveSource = super.sourceOf(item, info);
if (liveSource != null) return liveSource;
List<MediaSource> mediaSources = new ArrayList<>();
// Create video stream source
final List<VideoStream> videos = ListHelper.getSortedStreamVideosList(context,
info.video_streams, info.video_only_streams, false);
info.getVideoStreams(), info.getVideoOnlyStreams(), false);
final int index;
if (videos.isEmpty()) {
index = -1;
@@ -368,6 +402,7 @@ public abstract class VideoPlayer extends BasePlayer
final VideoStream video = index >= 0 && index < videos.size() ? videos.get(index) : null;
if (video != null) {
final MediaSource streamSource = buildMediaSource(video.getUrl(),
PlayerHelper.cacheKeyOf(info, video),
MediaFormat.getSuffixById(video.getFormatId()));
mediaSources.add(streamSource);
}
@@ -380,6 +415,7 @@ public abstract class VideoPlayer extends BasePlayer
// Merge with audio stream in case if video does not contain audio
if (audio != null && ((video != null && video.isVideoOnly) || video == null)) {
final MediaSource audioSource = buildMediaSource(audio.getUrl(),
PlayerHelper.cacheKeyOf(info, audio),
MediaFormat.getSuffixById(audio.getFormatId()));
mediaSources.add(audioSource);
}
@@ -391,12 +427,12 @@ public abstract class VideoPlayer extends BasePlayer
// Create subtitle sources
for (final Subtitles subtitle : info.getSubtitles()) {
final String mimeType = PlayerHelper.mimeTypesOf(subtitle.getFileType());
if (mimeType == null || context == null) continue;
if (mimeType == null) continue;
final Format textFormat = Format.createTextSampleFormat(null, mimeType,
SELECTION_FLAG_AUTOSELECT, PlayerHelper.captionLanguageOf(context, subtitle));
final MediaSource textSource = new SingleSampleMediaSource(
Uri.parse(subtitle.getURL()), cacheDataSourceFactory, textFormat, TIME_UNSET);
final MediaSource textSource = dataSource.getSampleMediaSourceFactory()
.createMediaSource(Uri.parse(subtitle.getURL()), textFormat, TIME_UNSET);
mediaSources.add(textSource);
}
@@ -417,7 +453,7 @@ public abstract class VideoPlayer extends BasePlayer
super.onBlocked();
controlsVisibilityHandler.removeCallbacksAndMessages(null);
animateView(controlsRoot, false, 300);
animateView(controlsRoot, false, DEFAULT_CONTROLS_DURATION);
playbackSeekBar.setEnabled(false);
// Bug on lower api, disabling and enabling the seekBar resets the thumb color -.-, so sets the color again
@@ -442,7 +478,7 @@ public abstract class VideoPlayer extends BasePlayer
playbackSeekBar.getThumb().setColorFilter(Color.RED, PorterDuff.Mode.SRC_IN);
loadingPanel.setVisibility(View.GONE);
showControlsThenHide();
animateView(currentDisplaySeek, AnimationUtils.Type.SCALE_AND_ALPHA, false, 200);
animateView(endScreen, false, 0);
}
@@ -489,6 +525,12 @@ public abstract class VideoPlayer extends BasePlayer
onTextTrackUpdate();
}
@Override
public void onPlaybackParametersChanged(PlaybackParameters playbackParameters) {
super.onPlaybackParametersChanged(playbackParameters);
playbackSpeedTextView.setText(formatSpeed(playbackParameters.speed));
}
@Override
public void onVideoSizeChanged(int width, int height, int unappliedRotationDegrees, float pixelWidthHeightRatio) {
if (DEBUG) {
@@ -529,26 +571,15 @@ public abstract class VideoPlayer extends BasePlayer
}
// Normalize mismatching language strings
final String preferredLanguage = trackSelector.getParameters().preferredTextLanguage;
// Because ExoPlayer normalizes the preferred language string but not the text track
// language strings, some preferred language string will have the language name in lowercase
String formattedPreferredLanguage = null;
if (preferredLanguage != null) {
for (final String language : availableLanguages) {
if (language.compareToIgnoreCase(preferredLanguage) == 0) {
formattedPreferredLanguage = language;
break;
}
}
}
final String preferredLanguage = trackSelector.getPreferredTextLanguage();
// Build UI
buildCaptionMenu(availableLanguages);
if (trackSelector.getRendererDisabled(textRenderer) || formattedPreferredLanguage == null ||
!availableLanguages.contains(formattedPreferredLanguage)) {
if (trackSelector.getRendererDisabled(textRenderer) || preferredLanguage == null ||
!availableLanguages.contains(preferredLanguage)) {
captionTextView.setText(R.string.caption_none);
} else {
captionTextView.setText(formattedPreferredLanguage);
captionTextView.setText(preferredLanguage);
}
captionTextView.setVisibility(availableLanguages.isEmpty() ? View.GONE : View.VISIBLE);
}
@@ -576,7 +607,7 @@ public abstract class VideoPlayer extends BasePlayer
@Override
public void onUpdateProgress(int currentProgress, int duration, int bufferPercent) {
if (!isPrepared) return;
if (!isPrepared()) return;
if (duration != playbackSeekBar.getMax()) {
playbackEndTime.setText(getTimeString(duration));
@@ -592,17 +623,16 @@ public abstract class VideoPlayer extends BasePlayer
if (DEBUG && bufferPercent % 20 == 0) { //Limit log
Log.d(TAG, "updateProgress() called with: isVisible = " + isControlsVisible() + ", currentProgress = [" + currentProgress + "], duration = [" + duration + "], bufferPercent = [" + bufferPercent + "]");
}
playbackLiveSync.setClickable(!isLiveEdge());
}
@Override
public void onThumbnailReceived(Bitmap thumbnail) {
super.onThumbnailReceived(thumbnail);
if (thumbnail != null) endScreen.setImageBitmap(thumbnail);
public void onLoadingComplete(String imageUri, View view, Bitmap loadedImage) {
super.onLoadingComplete(imageUri, view, loadedImage);
if (loadedImage != null) endScreen.setImageBitmap(loadedImage);
}
protected void onFullScreenButtonClicked() {
if (!isPlayerReady()) return;
changeState(STATE_BLOCKED);
}
@@ -633,6 +663,8 @@ public abstract class VideoPlayer extends BasePlayer
onResizeClicked();
} else if (v.getId() == captionTextView.getId()) {
onCaptionClicked();
} else if (v.getId() == playbackLiveSync.getId()) {
seekToDefault();
}
}
@@ -683,7 +715,7 @@ public abstract class VideoPlayer extends BasePlayer
if (DEBUG) Log.d(TAG, "onQualitySelectorClicked() called");
qualityPopupMenu.show();
isSomePopupMenuVisible = true;
showControls(300);
showControls(DEFAULT_CONTROLS_DURATION);
final VideoStream videoStream = getSelectedVideoStream();
if (videoStream != null) {
@@ -695,22 +727,22 @@ public abstract class VideoPlayer extends BasePlayer
wasPlaying = simpleExoPlayer.getPlayWhenReady();
}
private void onPlaybackSpeedClicked() {
public void onPlaybackSpeedClicked() {
if (DEBUG) Log.d(TAG, "onPlaybackSpeedClicked() called");
playbackSpeedPopupMenu.show();
isSomePopupMenuVisible = true;
showControls(300);
showControls(DEFAULT_CONTROLS_DURATION);
}
private void onCaptionClicked() {
if (DEBUG) Log.d(TAG, "onCaptionClicked() called");
captionPopupMenu.show();
isSomePopupMenuVisible = true;
showControls(300);
showControls(DEFAULT_CONTROLS_DURATION);
}
private void onResizeClicked() {
if (getAspectRatioFrameLayout() != null && context != null) {
if (getAspectRatioFrameLayout() != null) {
final int currentResizeMode = getAspectRatioFrameLayout().getResizeMode();
final int newResizeMode = nextResizeMode(currentResizeMode);
getAspectRatioFrameLayout().setResizeMode(newResizeMode);
@@ -739,14 +771,15 @@ public abstract class VideoPlayer extends BasePlayer
if (isPlaying()) simpleExoPlayer.setPlayWhenReady(false);
showControls(0);
animateView(currentDisplaySeek, AnimationUtils.Type.SCALE_AND_ALPHA, true, 300);
animateView(currentDisplaySeek, AnimationUtils.Type.SCALE_AND_ALPHA, true,
DEFAULT_CONTROLS_DURATION);
}
@Override
public void onStopTrackingTouch(SeekBar seekBar) {
if (DEBUG) Log.d(TAG, "onStopTrackingTouch() called with: seekBar = [" + seekBar + "]");
simpleExoPlayer.seekTo(seekBar.getProgress());
seekTo(seekBar.getProgress());
if (wasPlaying || simpleExoPlayer.getDuration() == seekBar.getProgress()) simpleExoPlayer.setPlayWhenReady(true);
playbackCurrentTime.setText(getTimeString(seekBar.getProgress()));
@@ -795,7 +828,7 @@ public abstract class VideoPlayer extends BasePlayer
PropertyValuesHolder.ofFloat(View.ALPHA, 1f, 0f),
PropertyValuesHolder.ofFloat(View.SCALE_X, 1.4f, 1f),
PropertyValuesHolder.ofFloat(View.SCALE_Y, 1.4f, 1f)
).setDuration(300);
).setDuration(DEFAULT_CONTROLS_DURATION);
controlViewAnimator.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
@@ -837,12 +870,8 @@ public abstract class VideoPlayer extends BasePlayer
public void showControlsThenHide() {
if (DEBUG) Log.d(TAG, "showControlsThenHide() called");
animateView(controlsRoot, true, 300, 0, new Runnable() {
@Override
public void run() {
hideControls(300, DEFAULT_CONTROLS_HIDE_TIME);
}
});
animateView(controlsRoot, true, DEFAULT_CONTROLS_DURATION, 0,
() -> hideControls(DEFAULT_CONTROLS_DURATION, DEFAULT_CONTROLS_HIDE_TIME));
}
public void showControls(long duration) {
@@ -854,12 +883,8 @@ public abstract class VideoPlayer extends BasePlayer
public void hideControls(final long duration, long delay) {
if (DEBUG) Log.d(TAG, "hideControls() called with: delay = [" + delay + "]");
controlsVisibilityHandler.removeCallbacksAndMessages(null);
controlsVisibilityHandler.postDelayed(new Runnable() {
@Override
public void run() {
animateView(controlsRoot, false, duration);
}
}, delay);
controlsVisibilityHandler.postDelayed(
() -> animateView(controlsRoot, false, duration), delay);
}
/*//////////////////////////////////////////////////////////////////////////

View File

@@ -17,10 +17,14 @@ import com.google.android.exoplayer2.SimpleExoPlayer;
import com.google.android.exoplayer2.audio.AudioRendererEventListener;
import com.google.android.exoplayer2.decoder.DecoderCounters;
public class AudioReactor implements AudioManager.OnAudioFocusChangeListener, AudioRendererEventListener {
public class AudioReactor implements AudioManager.OnAudioFocusChangeListener,
AudioRendererEventListener {
private static final String TAG = "AudioFocusReactor";
private static final boolean SHOULD_BUILD_FOCUS_REQUEST =
Build.VERSION.SDK_INT >= Build.VERSION_CODES.O;
private static final int DUCK_DURATION = 1500;
private static final float DUCK_AUDIO_TO = .2f;
@@ -33,13 +37,14 @@ public class AudioReactor implements AudioManager.OnAudioFocusChangeListener, Au
private final AudioFocusRequest request;
public AudioReactor(@NonNull final Context context, @NonNull final SimpleExoPlayer player) {
public AudioReactor(@NonNull final Context context,
@NonNull final SimpleExoPlayer player) {
this.player = player;
this.context = context;
this.audioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
player.setAudioDebugListener(this);
player.addAudioDebugListener(this);
if (shouldBuildFocusRequest()) {
if (SHOULD_BUILD_FOCUS_REQUEST) {
request = new AudioFocusRequest.Builder(FOCUS_GAIN_TYPE)
.setAcceptsDelayedFocusGain(true)
.setWillPauseWhenDucked(true)
@@ -50,12 +55,17 @@ public class AudioReactor implements AudioManager.OnAudioFocusChangeListener, Au
}
}
public void dispose() {
abandonAudioFocus();
player.removeAudioDebugListener(this);
}
/*//////////////////////////////////////////////////////////////////////////
// Audio Manager
//////////////////////////////////////////////////////////////////////////*/
public void requestAudioFocus() {
if (shouldBuildFocusRequest()) {
if (SHOULD_BUILD_FOCUS_REQUEST) {
audioManager.requestAudioFocus(request);
} else {
audioManager.requestAudioFocus(this, STREAM_TYPE, FOCUS_GAIN_TYPE);
@@ -63,7 +73,7 @@ public class AudioReactor implements AudioManager.OnAudioFocusChangeListener, Au
}
public void abandonAudioFocus() {
if (shouldBuildFocusRequest()) {
if (SHOULD_BUILD_FOCUS_REQUEST) {
audioManager.abandonAudioFocusRequest(request);
} else {
audioManager.abandonAudioFocus(this);
@@ -82,10 +92,6 @@ public class AudioReactor implements AudioManager.OnAudioFocusChangeListener, Au
audioManager.setStreamVolume(STREAM_TYPE, volume, 0);
}
private boolean shouldBuildFocusRequest() {
return Build.VERSION.SDK_INT >= Build.VERSION_CODES.O;
}
/*//////////////////////////////////////////////////////////////////////////
// AudioFocus
//////////////////////////////////////////////////////////////////////////*/
@@ -148,12 +154,8 @@ public class AudioReactor implements AudioManager.OnAudioFocusChangeListener, Au
player.setVolume(to);
}
});
valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
player.setVolume(((float) animation.getAnimatedValue()));
}
});
valueAnimator.addUpdateListener(animation ->
player.setVolume(((float) animation.getAnimatedValue())));
valueAnimator.start();
}

View File

@@ -4,11 +4,14 @@ import android.content.Context;
import android.support.annotation.NonNull;
import android.util.Log;
import com.google.android.exoplayer2.upstream.BandwidthMeter;
import com.google.android.exoplayer2.upstream.DataSource;
import com.google.android.exoplayer2.upstream.DefaultBandwidthMeter;
import com.google.android.exoplayer2.upstream.DefaultDataSource;
import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory;
import com.google.android.exoplayer2.upstream.DefaultHttpDataSource;
import com.google.android.exoplayer2.upstream.FileDataSource;
import com.google.android.exoplayer2.upstream.TransferListener;
import com.google.android.exoplayer2.upstream.cache.CacheDataSink;
import com.google.android.exoplayer2.upstream.cache.CacheDataSource;
import com.google.android.exoplayer2.upstream.cache.LeastRecentlyUsedCacheEvictor;
@@ -18,7 +21,7 @@ import org.schabi.newpipe.Downloader;
import java.io.File;
public class CacheFactory implements DataSource.Factory {
/* package-private */ class CacheFactory implements DataSource.Factory {
private static final String TAG = "CacheFactory";
private static final String CACHE_FOLDER_NAME = "exoplayer";
private static final int CACHE_FLAGS = CacheDataSource.FLAG_BLOCK_ON_CACHE | CacheDataSource.FLAG_IGNORE_CACHE_ON_ERROR;
@@ -33,18 +36,21 @@ public class CacheFactory implements DataSource.Factory {
// todo: make this a singleton?
private static SimpleCache cache;
public CacheFactory(@NonNull final Context context) {
this(context, PlayerHelper.getPreferredCacheSize(context), PlayerHelper.getPreferredFileSize(context));
public CacheFactory(@NonNull final Context context,
@NonNull final String userAgent,
@NonNull final TransferListener<? super DataSource> transferListener) {
this(context, userAgent, transferListener, PlayerHelper.getPreferredCacheSize(context),
PlayerHelper.getPreferredFileSize(context));
}
CacheFactory(@NonNull final Context context, final long maxCacheSize, final long maxFileSize) {
super();
private CacheFactory(@NonNull final Context context,
@NonNull final String userAgent,
@NonNull final TransferListener<? super DataSource> transferListener,
final long maxCacheSize,
final long maxFileSize) {
this.maxFileSize = maxFileSize;
final String userAgent = Downloader.USER_AGENT;
final DefaultBandwidthMeter bandwidthMeter = new DefaultBandwidthMeter();
dataSourceFactory = new DefaultDataSourceFactory(context, userAgent, bandwidthMeter);
dataSourceFactory = new DefaultDataSourceFactory(context, userAgent, transferListener);
cacheDir = new File(context.getExternalCacheDir(), CACHE_FOLDER_NAME);
if (!cacheDir.exists()) {
//noinspection ResultOfMethodCallIgnored

View File

@@ -11,12 +11,14 @@ import com.google.android.exoplayer2.trackselection.TrackSelectionArray;
import com.google.android.exoplayer2.upstream.Allocator;
import com.google.android.exoplayer2.upstream.DefaultAllocator;
import static com.google.android.exoplayer2.DefaultLoadControl.DEFAULT_BUFFER_FOR_PLAYBACK_AFTER_REBUFFER_MS;
import static com.google.android.exoplayer2.DefaultLoadControl.DEFAULT_PRIORITIZE_TIME_OVER_SIZE_THRESHOLDS;
import static com.google.android.exoplayer2.DefaultLoadControl.DEFAULT_TARGET_BUFFER_BYTES;
public class LoadController implements LoadControl {
public static final String TAG = "LoadController";
private final long initialPlaybackBufferUs;
private final LoadControl internalLoadControl;
/*//////////////////////////////////////////////////////////////////////////
@@ -24,19 +26,25 @@ public class LoadController implements LoadControl {
//////////////////////////////////////////////////////////////////////////*/
public LoadController(final Context context) {
this(PlayerHelper.getMinBufferMs(context),
PlayerHelper.getMaxBufferMs(context),
PlayerHelper.getBufferForPlaybackMs(context));
this(PlayerHelper.getPlaybackStartBufferMs(context),
PlayerHelper.getPlaybackMinimumBufferMs(context),
PlayerHelper.getPlaybackOptimalBufferMs(context));
}
public LoadController(final int minBufferMs,
final int maxBufferMs,
final int bufferForPlaybackMs) {
private LoadController(final int initialPlaybackBufferMs,
final int minimumPlaybackbufferMs,
final int optimalPlaybackBufferMs) {
this.initialPlaybackBufferUs = initialPlaybackBufferMs * 1000;
final DefaultAllocator allocator = new DefaultAllocator(true,
C.DEFAULT_BUFFER_SEGMENT_SIZE);
internalLoadControl = new DefaultLoadControl(allocator, minBufferMs, maxBufferMs,
bufferForPlaybackMs, DEFAULT_BUFFER_FOR_PLAYBACK_AFTER_REBUFFER_MS);
internalLoadControl = new DefaultLoadControl(allocator,
/*minBufferMs=*/minimumPlaybackbufferMs,
/*maxBufferMs=*/optimalPlaybackBufferMs,
/*bufferForPlaybackMs=*/initialPlaybackBufferMs,
/*bufferForPlaybackAfterRebufferMs=*/initialPlaybackBufferMs,
DEFAULT_TARGET_BUFFER_BYTES, DEFAULT_PRIORITIZE_TIME_OVER_SIZE_THRESHOLDS);
}
/*//////////////////////////////////////////////////////////////////////////
@@ -49,7 +57,8 @@ public class LoadController implements LoadControl {
}
@Override
public void onTracksSelected(Renderer[] renderers, TrackGroupArray trackGroupArray, TrackSelectionArray trackSelectionArray) {
public void onTracksSelected(Renderer[] renderers, TrackGroupArray trackGroupArray,
TrackSelectionArray trackSelectionArray) {
internalLoadControl.onTracksSelected(renderers, trackGroupArray, trackSelectionArray);
}
@@ -69,12 +78,27 @@ public class LoadController implements LoadControl {
}
@Override
public boolean shouldStartPlayback(long l, boolean b) {
return internalLoadControl.shouldStartPlayback(l, b);
public long getBackBufferDurationUs() {
return internalLoadControl.getBackBufferDurationUs();
}
@Override
public boolean shouldContinueLoading(long l) {
return internalLoadControl.shouldContinueLoading(l);
public boolean retainBackBufferFromKeyframe() {
return internalLoadControl.retainBackBufferFromKeyframe();
}
@Override
public boolean shouldContinueLoading(long bufferedDurationUs, float playbackSpeed) {
return internalLoadControl.shouldContinueLoading(bufferedDurationUs, playbackSpeed);
}
@Override
public boolean shouldStartPlayback(long bufferedDurationUs, float playbackSpeed,
boolean rebuffering) {
final boolean isInitialPlaybackBufferFilled = bufferedDurationUs >=
this.initialPlaybackBufferUs * playbackSpeed;
final boolean isInternalStartingPlayback = internalLoadControl.shouldStartPlayback(
bufferedDurationUs, playbackSpeed, rebuffering);
return isInitialPlaybackBufferFilled || isInternalStartingPlayback;
}
}

View File

@@ -0,0 +1,40 @@
package org.schabi.newpipe.player.helper;
import android.content.Context;
import android.content.Intent;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.v4.media.session.MediaButtonReceiver;
import android.support.v4.media.session.MediaSessionCompat;
import android.view.KeyEvent;
import com.google.android.exoplayer2.Player;
import com.google.android.exoplayer2.ext.mediasession.MediaSessionConnector;
import org.schabi.newpipe.player.mediasession.DummyPlaybackPreparer;
import org.schabi.newpipe.player.mediasession.MediaSessionCallback;
import org.schabi.newpipe.player.mediasession.PlayQueueNavigator;
import org.schabi.newpipe.player.mediasession.PlayQueuePlaybackController;
public class MediaSessionManager {
private static final String TAG = "MediaSessionManager";
@NonNull private final MediaSessionCompat mediaSession;
@NonNull private final MediaSessionConnector sessionConnector;
public MediaSessionManager(@NonNull final Context context,
@NonNull final Player player,
@NonNull final MediaSessionCallback callback) {
this.mediaSession = new MediaSessionCompat(context, TAG);
this.sessionConnector = new MediaSessionConnector(mediaSession,
new PlayQueuePlaybackController(callback));
this.sessionConnector.setQueueNavigator(new PlayQueueNavigator(mediaSession, callback));
this.sessionConnector.setPlayer(player, new DummyPlaybackPreparer());
}
@Nullable
@SuppressWarnings("UnusedReturnValue")
public KeyEvent handleMediaButtonIntent(final Intent intent) {
return MediaButtonReceiver.handleIntent(mediaSession, intent);
}
}

View File

@@ -0,0 +1,379 @@
package org.schabi.newpipe.player.helper;
import android.app.Dialog;
import android.content.Context;
import android.os.Bundle;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.v4.app.DialogFragment;
import android.support.v7.app.AlertDialog;
import android.util.Log;
import android.view.View;
import android.widget.CheckBox;
import android.widget.SeekBar;
import android.widget.TextView;
import org.schabi.newpipe.R;
import org.schabi.newpipe.util.SliderStrategy;
import static org.schabi.newpipe.player.BasePlayer.DEBUG;
public class PlaybackParameterDialog extends DialogFragment {
@NonNull private static final String TAG = "PlaybackParameterDialog";
public static final double MINIMUM_PLAYBACK_VALUE = 0.25f;
public static final double MAXIMUM_PLAYBACK_VALUE = 3.00f;
public static final char STEP_UP_SIGN = '+';
public static final char STEP_DOWN_SIGN = '-';
public static final double PLAYBACK_STEP_VALUE = 0.05f;
public static final double NIGHTCORE_TEMPO = 1.20f;
public static final double NIGHTCORE_PITCH_LOWER = 1.15f;
public static final double NIGHTCORE_PITCH_UPPER = 1.25f;
public static final double DEFAULT_TEMPO = 1.00f;
public static final double DEFAULT_PITCH = 1.00f;
@NonNull private static final String INITIAL_TEMPO_KEY = "initial_tempo_key";
@NonNull private static final String INITIAL_PITCH_KEY = "initial_pitch_key";
public interface Callback {
void onPlaybackParameterChanged(final float playbackTempo, final float playbackPitch);
}
@Nullable private Callback callback;
@NonNull private final SliderStrategy strategy = new SliderStrategy.Quadratic(
MINIMUM_PLAYBACK_VALUE, MAXIMUM_PLAYBACK_VALUE,
/*centerAt=*/1.00f, /*sliderGranularity=*/10000);
private double initialTempo = DEFAULT_TEMPO;
private double initialPitch = DEFAULT_PITCH;
@Nullable private SeekBar tempoSlider;
@Nullable private TextView tempoMinimumText;
@Nullable private TextView tempoMaximumText;
@Nullable private TextView tempoCurrentText;
@Nullable private TextView tempoStepDownText;
@Nullable private TextView tempoStepUpText;
@Nullable private SeekBar pitchSlider;
@Nullable private TextView pitchMinimumText;
@Nullable private TextView pitchMaximumText;
@Nullable private TextView pitchCurrentText;
@Nullable private TextView pitchStepDownText;
@Nullable private TextView pitchStepUpText;
@Nullable private CheckBox unhookingCheckbox;
@Nullable private TextView nightCorePresetText;
@Nullable private TextView resetPresetText;
public static PlaybackParameterDialog newInstance(final double playbackTempo,
final double playbackPitch) {
PlaybackParameterDialog dialog = new PlaybackParameterDialog();
dialog.initialTempo = playbackTempo;
dialog.initialPitch = playbackPitch;
return dialog;
}
/*//////////////////////////////////////////////////////////////////////////
// Lifecycle
//////////////////////////////////////////////////////////////////////////*/
@Override
public void onAttach(Context context) {
super.onAttach(context);
if (context != null && context instanceof Callback) {
callback = (Callback) context;
} else {
dismiss();
}
}
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
if (savedInstanceState != null) {
initialTempo = savedInstanceState.getDouble(INITIAL_TEMPO_KEY, DEFAULT_TEMPO);
initialPitch = savedInstanceState.getDouble(INITIAL_PITCH_KEY, DEFAULT_PITCH);
}
}
@Override
public void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
outState.putDouble(INITIAL_TEMPO_KEY, initialTempo);
outState.putDouble(INITIAL_PITCH_KEY, initialPitch);
}
/*//////////////////////////////////////////////////////////////////////////
// Dialog
//////////////////////////////////////////////////////////////////////////*/
@NonNull
@Override
public Dialog onCreateDialog(@Nullable Bundle savedInstanceState) {
final View view = View.inflate(getContext(), R.layout.dialog_playback_parameter, null);
setupControlViews(view);
final AlertDialog.Builder dialogBuilder = new AlertDialog.Builder(requireActivity())
.setTitle(R.string.playback_speed_control)
.setView(view)
.setCancelable(true)
.setNegativeButton(R.string.cancel, (dialogInterface, i) ->
setPlaybackParameters(initialTempo, initialPitch))
.setPositiveButton(R.string.finish, (dialogInterface, i) ->
setCurrentPlaybackParameters());
return dialogBuilder.create();
}
/*//////////////////////////////////////////////////////////////////////////
// Control Views
//////////////////////////////////////////////////////////////////////////*/
private void setupControlViews(@NonNull View rootView) {
setupHookingControl(rootView);
setupTempoControl(rootView);
setupPitchControl(rootView);
setupPresetControl(rootView);
}
private void setupTempoControl(@NonNull View rootView) {
tempoSlider = rootView.findViewById(R.id.tempoSeekbar);
tempoMinimumText = rootView.findViewById(R.id.tempoMinimumText);
tempoMaximumText = rootView.findViewById(R.id.tempoMaximumText);
tempoCurrentText = rootView.findViewById(R.id.tempoCurrentText);
tempoStepUpText = rootView.findViewById(R.id.tempoStepUp);
tempoStepDownText = rootView.findViewById(R.id.tempoStepDown);
if (tempoCurrentText != null)
tempoCurrentText.setText(PlayerHelper.formatSpeed(initialTempo));
if (tempoMaximumText != null)
tempoMaximumText.setText(PlayerHelper.formatSpeed(MAXIMUM_PLAYBACK_VALUE));
if (tempoMinimumText != null)
tempoMinimumText.setText(PlayerHelper.formatSpeed(MINIMUM_PLAYBACK_VALUE));
if (tempoStepUpText != null) {
tempoStepUpText.setText(getStepUpPercentString(PLAYBACK_STEP_VALUE));
tempoStepUpText.setOnClickListener(view -> {
onTempoSliderUpdated(getCurrentTempo() + PLAYBACK_STEP_VALUE);
setCurrentPlaybackParameters();
});
}
if (tempoStepDownText != null) {
tempoStepDownText.setText(getStepDownPercentString(PLAYBACK_STEP_VALUE));
tempoStepDownText.setOnClickListener(view -> {
onTempoSliderUpdated(getCurrentTempo() - PLAYBACK_STEP_VALUE);
setCurrentPlaybackParameters();
});
}
if (tempoSlider != null) {
tempoSlider.setMax(strategy.progressOf(MAXIMUM_PLAYBACK_VALUE));
tempoSlider.setProgress(strategy.progressOf(initialTempo));
tempoSlider.setOnSeekBarChangeListener(getOnTempoChangedListener());
}
}
private void setupPitchControl(@NonNull View rootView) {
pitchSlider = rootView.findViewById(R.id.pitchSeekbar);
pitchMinimumText = rootView.findViewById(R.id.pitchMinimumText);
pitchMaximumText = rootView.findViewById(R.id.pitchMaximumText);
pitchCurrentText = rootView.findViewById(R.id.pitchCurrentText);
pitchStepDownText = rootView.findViewById(R.id.pitchStepDown);
pitchStepUpText = rootView.findViewById(R.id.pitchStepUp);
if (pitchCurrentText != null)
pitchCurrentText.setText(PlayerHelper.formatPitch(initialPitch));
if (pitchMaximumText != null)
pitchMaximumText.setText(PlayerHelper.formatPitch(MAXIMUM_PLAYBACK_VALUE));
if (pitchMinimumText != null)
pitchMinimumText.setText(PlayerHelper.formatPitch(MINIMUM_PLAYBACK_VALUE));
if (pitchStepUpText != null) {
pitchStepUpText.setText(getStepUpPercentString(PLAYBACK_STEP_VALUE));
pitchStepUpText.setOnClickListener(view -> {
onPitchSliderUpdated(getCurrentPitch() + PLAYBACK_STEP_VALUE);
setCurrentPlaybackParameters();
});
}
if (pitchStepDownText != null) {
pitchStepDownText.setText(getStepDownPercentString(PLAYBACK_STEP_VALUE));
pitchStepDownText.setOnClickListener(view -> {
onPitchSliderUpdated(getCurrentPitch() - PLAYBACK_STEP_VALUE);
setCurrentPlaybackParameters();
});
}
if (pitchSlider != null) {
pitchSlider.setMax(strategy.progressOf(MAXIMUM_PLAYBACK_VALUE));
pitchSlider.setProgress(strategy.progressOf(initialPitch));
pitchSlider.setOnSeekBarChangeListener(getOnPitchChangedListener());
}
}
private void setupHookingControl(@NonNull View rootView) {
unhookingCheckbox = rootView.findViewById(R.id.unhookCheckbox);
if (unhookingCheckbox != null) {
unhookingCheckbox.setChecked(initialPitch != initialTempo);
unhookingCheckbox.setOnCheckedChangeListener((compoundButton, isChecked) -> {
if (isChecked) return;
// When unchecked, slide back to the minimum of current tempo or pitch
final double minimum = Math.min(getCurrentPitch(), getCurrentTempo());
setSliders(minimum);
setCurrentPlaybackParameters();
});
}
}
private void setupPresetControl(@NonNull View rootView) {
nightCorePresetText = rootView.findViewById(R.id.presetNightcore);
if (nightCorePresetText != null) {
nightCorePresetText.setOnClickListener(view -> {
final double randomPitch = NIGHTCORE_PITCH_LOWER +
Math.random() * (NIGHTCORE_PITCH_UPPER - NIGHTCORE_PITCH_LOWER);
setTempoSlider(NIGHTCORE_TEMPO);
setPitchSlider(randomPitch);
setCurrentPlaybackParameters();
});
}
resetPresetText = rootView.findViewById(R.id.presetReset);
if (resetPresetText != null) {
resetPresetText.setOnClickListener(view -> {
setTempoSlider(DEFAULT_TEMPO);
setPitchSlider(DEFAULT_PITCH);
setCurrentPlaybackParameters();
});
}
}
/*//////////////////////////////////////////////////////////////////////////
// Sliders
//////////////////////////////////////////////////////////////////////////*/
private SeekBar.OnSeekBarChangeListener getOnTempoChangedListener() {
return new SeekBar.OnSeekBarChangeListener() {
@Override
public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
final double currentTempo = strategy.valueOf(progress);
if (fromUser) {
onTempoSliderUpdated(currentTempo);
setCurrentPlaybackParameters();
}
}
@Override
public void onStartTrackingTouch(SeekBar seekBar) {
// Do Nothing.
}
@Override
public void onStopTrackingTouch(SeekBar seekBar) {
// Do Nothing.
}
};
}
private SeekBar.OnSeekBarChangeListener getOnPitchChangedListener() {
return new SeekBar.OnSeekBarChangeListener() {
@Override
public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
final double currentPitch = strategy.valueOf(progress);
if (fromUser) { // this change is first in chain
onPitchSliderUpdated(currentPitch);
setCurrentPlaybackParameters();
}
}
@Override
public void onStartTrackingTouch(SeekBar seekBar) {
// Do Nothing.
}
@Override
public void onStopTrackingTouch(SeekBar seekBar) {
// Do Nothing.
}
};
}
private void onTempoSliderUpdated(final double newTempo) {
if (unhookingCheckbox == null) return;
if (!unhookingCheckbox.isChecked()) {
setSliders(newTempo);
} else {
setTempoSlider(newTempo);
}
}
private void onPitchSliderUpdated(final double newPitch) {
if (unhookingCheckbox == null) return;
if (!unhookingCheckbox.isChecked()) {
setSliders(newPitch);
} else {
setPitchSlider(newPitch);
}
}
private void setSliders(final double newValue) {
setTempoSlider(newValue);
setPitchSlider(newValue);
}
private void setTempoSlider(final double newTempo) {
if (tempoSlider == null) return;
tempoSlider.setProgress(strategy.progressOf(newTempo));
}
private void setPitchSlider(final double newPitch) {
if (pitchSlider == null) return;
pitchSlider.setProgress(strategy.progressOf(newPitch));
}
/*//////////////////////////////////////////////////////////////////////////
// Helper
//////////////////////////////////////////////////////////////////////////*/
private void setCurrentPlaybackParameters() {
setPlaybackParameters(getCurrentTempo(), getCurrentPitch());
}
private void setPlaybackParameters(final double tempo, final double pitch) {
if (callback != null && tempoCurrentText != null && pitchCurrentText != null) {
if (DEBUG) Log.d(TAG, "Setting playback parameters to " +
"tempo=[" + tempo + "], " +
"pitch=[" + pitch + "]");
tempoCurrentText.setText(PlayerHelper.formatSpeed(tempo));
pitchCurrentText.setText(PlayerHelper.formatPitch(pitch));
callback.onPlaybackParameterChanged((float) tempo, (float) pitch);
}
}
private double getCurrentTempo() {
return tempoSlider == null ? initialTempo : strategy.valueOf(
tempoSlider.getProgress());
}
private double getCurrentPitch() {
return pitchSlider == null ? initialPitch : strategy.valueOf(
pitchSlider.getProgress());
}
@NonNull
private static String getStepUpPercentString(final double percent) {
return STEP_UP_SIGN + PlayerHelper.formatPitch(percent);
}
@NonNull
private static String getStepDownPercentString(final double percent) {
return STEP_DOWN_SIGN + PlayerHelper.formatPitch(percent);
}
}

View File

@@ -0,0 +1,78 @@
package org.schabi.newpipe.player.helper;
import android.content.Context;
import android.support.annotation.NonNull;
import com.google.android.exoplayer2.source.ExtractorMediaSource;
import com.google.android.exoplayer2.source.SingleSampleMediaSource;
import com.google.android.exoplayer2.source.dash.DashMediaSource;
import com.google.android.exoplayer2.source.dash.DefaultDashChunkSource;
import com.google.android.exoplayer2.source.hls.HlsMediaSource;
import com.google.android.exoplayer2.source.smoothstreaming.DefaultSsChunkSource;
import com.google.android.exoplayer2.source.smoothstreaming.SsMediaSource;
import com.google.android.exoplayer2.upstream.DataSource;
import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory;
import com.google.android.exoplayer2.upstream.TransferListener;
public class PlayerDataSource {
private static final int MANIFEST_MINIMUM_RETRY = 5;
private static final int EXTRACTOR_MINIMUM_RETRY = Integer.MAX_VALUE;
private static final int LIVE_STREAM_EDGE_GAP_MILLIS = 10000;
private final DataSource.Factory cacheDataSourceFactory;
private final DataSource.Factory cachelessDataSourceFactory;
public PlayerDataSource(@NonNull final Context context,
@NonNull final String userAgent,
@NonNull final TransferListener<? super DataSource> transferListener) {
cacheDataSourceFactory = new CacheFactory(context, userAgent, transferListener);
cachelessDataSourceFactory = new DefaultDataSourceFactory(context, userAgent, transferListener);
}
public SsMediaSource.Factory getLiveSsMediaSourceFactory() {
return new SsMediaSource.Factory(new DefaultSsChunkSource.Factory(
cachelessDataSourceFactory), cachelessDataSourceFactory)
.setMinLoadableRetryCount(MANIFEST_MINIMUM_RETRY)
.setLivePresentationDelayMs(LIVE_STREAM_EDGE_GAP_MILLIS);
}
public HlsMediaSource.Factory getLiveHlsMediaSourceFactory() {
return new HlsMediaSource.Factory(cachelessDataSourceFactory)
.setAllowChunklessPreparation(true)
.setMinLoadableRetryCount(MANIFEST_MINIMUM_RETRY);
}
public DashMediaSource.Factory getLiveDashMediaSourceFactory() {
return new DashMediaSource.Factory(new DefaultDashChunkSource.Factory(
cachelessDataSourceFactory), cachelessDataSourceFactory)
.setMinLoadableRetryCount(MANIFEST_MINIMUM_RETRY)
.setLivePresentationDelayMs(LIVE_STREAM_EDGE_GAP_MILLIS);
}
public SsMediaSource.Factory getSsMediaSourceFactory() {
return new SsMediaSource.Factory(new DefaultSsChunkSource.Factory(
cacheDataSourceFactory), cacheDataSourceFactory);
}
public HlsMediaSource.Factory getHlsMediaSourceFactory() {
return new HlsMediaSource.Factory(cacheDataSourceFactory);
}
public DashMediaSource.Factory getDashMediaSourceFactory() {
return new DashMediaSource.Factory(new DefaultDashChunkSource.Factory(
cacheDataSourceFactory), cacheDataSourceFactory);
}
public ExtractorMediaSource.Factory getExtractorMediaSourceFactory() {
return new ExtractorMediaSource.Factory(cacheDataSourceFactory)
.setMinLoadableRetryCount(EXTRACTOR_MINIMUM_RETRY);
}
public ExtractorMediaSource.Factory getExtractorMediaSourceFactory(@NonNull final String key) {
return getExtractorMediaSourceFactory().setCustomCacheKey(key);
}
public SingleSampleMediaSource.Factory getSampleMediaSourceFactory() {
return new SingleSampleMediaSource.Factory(cacheDataSourceFactory);
}
}

View File

@@ -2,27 +2,44 @@ 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;
import org.schabi.newpipe.extractor.InfoItem;
import org.schabi.newpipe.extractor.Subtitles;
import org.schabi.newpipe.extractor.stream.AudioStream;
import org.schabi.newpipe.extractor.stream.StreamInfo;
import org.schabi.newpipe.extractor.stream.StreamInfoItem;
import org.schabi.newpipe.extractor.stream.SubtitlesFormat;
import org.schabi.newpipe.extractor.stream.VideoStream;
import org.schabi.newpipe.playlist.PlayQueue;
import org.schabi.newpipe.playlist.PlayQueueItem;
import org.schabi.newpipe.playlist.SinglePlayQueue;
import java.text.DecimalFormat;
import java.text.NumberFormat;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Formatter;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import javax.annotation.Nonnull;
import java.util.Set;
import static com.google.android.exoplayer2.ui.AspectRatioFrameLayout.RESIZE_MODE_FILL;
import static com.google.android.exoplayer2.ui.AspectRatioFrameLayout.RESIZE_MODE_FIT;
import static com.google.android.exoplayer2.ui.AspectRatioFrameLayout.RESIZE_MODE_FIXED_HEIGHT;
import static com.google.android.exoplayer2.ui.AspectRatioFrameLayout.RESIZE_MODE_FIXED_WIDTH;
import static com.google.android.exoplayer2.ui.AspectRatioFrameLayout.RESIZE_MODE_ZOOM;
public class PlayerHelper {
@@ -49,11 +66,11 @@ public class PlayerHelper {
: stringFormatter.format("%02d:%02d", minutes, seconds).toString();
}
public static String formatSpeed(float speed) {
public static String formatSpeed(double speed) {
return speedFormatter.format(speed);
}
public static String formatPitch(float pitch) {
public static String formatPitch(double pitch) {
return pitchFormatter.format(pitch);
}
@@ -69,10 +86,10 @@ public class PlayerHelper {
public static String captionLanguageOf(@NonNull final Context context,
@NonNull final Subtitles subtitles) {
final String displayName = subtitles.getLocale().getDisplayName(subtitles.getLocale());
return displayName + (subtitles.isAutoGenerated() ?
" (" + context.getString(R.string.caption_auto_generated)+ ")" : "");
return displayName + (subtitles.isAutoGenerated() ? " (" + context.getString(R.string.caption_auto_generated)+ ")" : "");
}
@NonNull
public static String resizeTypeOf(@NonNull final Context context,
@AspectRatioFrameLayout.ResizeMode final int resizeMode) {
switch (resizeMode) {
@@ -83,6 +100,58 @@ public class PlayerHelper {
}
}
@NonNull
public static String cacheKeyOf(@NonNull final StreamInfo info, @NonNull VideoStream video) {
return info.getUrl() + video.getResolution() + video.getFormat().getName();
}
@NonNull
public static String cacheKeyOf(@NonNull final StreamInfo info, @NonNull AudioStream audio) {
return info.getUrl() + audio.getAverageBitrate() + audio.getFormat().getName();
}
/**
* Given a {@link StreamInfo} and the existing queue items, provide the
* {@link SinglePlayQueue} consisting of the next video for auto queuing.
* <br><br>
* This method detects and prevents cycle by naively checking if a
* candidate next video's url already exists in the existing items.
* <br><br>
* To select the next video, {@link StreamInfo#getNextVideo()} is first
* checked. If it is nonnull and is not part of the existing items, then
* it will be used as the next video. Otherwise, an random item with
* non-repeating url will be selected from the {@link StreamInfo#getRelatedStreams()}.
* */
@Nullable
public static PlayQueue autoQueueOf(@NonNull final StreamInfo info,
@NonNull final List<PlayQueueItem> existingItems) {
Set<String> urls = new HashSet<>(existingItems.size());
for (final PlayQueueItem item : existingItems) {
urls.add(item.getUrl());
}
final StreamInfoItem nextVideo = info.getNextVideo();
if (nextVideo != null && !urls.contains(nextVideo.getUrl())) {
return new SinglePlayQueue(nextVideo);
}
final List<InfoItem> relatedItems = info.getRelatedStreams();
if (relatedItems == null) return null;
List<StreamInfoItem> autoQueueItems = new ArrayList<>();
for (final InfoItem item : info.getRelatedStreams()) {
if (item instanceof StreamInfoItem && !urls.contains(item.getUrl())) {
autoQueueItems.add((StreamInfoItem) item);
}
}
Collections.shuffle(autoQueueItems);
return autoQueueItems.isEmpty() ? null : new SinglePlayQueue(autoQueueItems.get(0));
}
////////////////////////////////////////////////////////////////////////////
// Settings Resolution
////////////////////////////////////////////////////////////////////////////
public static boolean isResumeAfterAudioFocusGain(@NonNull final Context context) {
return isResumeAfterAudioFocusGain(context, false);
}
@@ -95,10 +164,20 @@ public class PlayerHelper {
return isUsingOldPlayer(context, false);
}
public static boolean isRememberingPopupDimensions(@Nonnull final Context context) {
public static boolean isRememberingPopupDimensions(@NonNull final Context context) {
return isRememberingPopupDimensions(context, true);
}
public static boolean isAutoQueueEnabled(@NonNull final Context context) {
return isAutoQueueEnabled(context, false);
}
@NonNull
public static SeekParameters getSeekParameters(@NonNull final Context context) {
return isUsingInexactSeek(context, false) ?
SeekParameters.CLOSEST_SYNC : SeekParameters.EXACT;
}
public static long getPreferredCacheSize(@NonNull final Context context) {
return 64 * 1024 * 1024L;
}
@@ -107,30 +186,80 @@ public class PlayerHelper {
return 512 * 1024L;
}
public static int getMinBufferMs(@NonNull final Context context) {
return 15000;
/**
* Returns the number of milliseconds the player buffers for before starting playback.
* */
public static int getPlaybackStartBufferMs(@NonNull final Context context) {
return 500;
}
public static int getMaxBufferMs(@NonNull final Context context) {
return 30000;
/**
* Returns the minimum number of milliseconds the player always buffers to after starting
* playback.
* */
public static int getPlaybackMinimumBufferMs(@NonNull final Context context) {
return 25000;
}
public static int getBufferForPlaybackMs(@NonNull final Context context) {
return 2500;
/**
* Returns the maximum/optimal number of milliseconds the player will buffer to once the buffer
* hits the point of {@link #getPlaybackMinimumBufferMs(Context)}.
* */
public static int getPlaybackOptimalBufferMs(@NonNull final Context context) {
return 60000;
}
public static TrackSelection.Factory getQualitySelector(@NonNull final Context context,
@NonNull final BandwidthMeter meter) {
return new AdaptiveTrackSelection.Factory(meter,
AdaptiveTrackSelection.DEFAULT_MAX_INITIAL_BITRATE,
/*bufferDurationRequiredForQualityIncrease=*/1000,
AdaptiveTrackSelection.DEFAULT_MAX_DURATION_FOR_QUALITY_DECREASE_MS,
AdaptiveTrackSelection.DEFAULT_MIN_DURATION_TO_RETAIN_AFTER_DISCARD_MS,
AdaptiveTrackSelection.DEFAULT_BANDWIDTH_FRACTION);
}
public static boolean isUsingDSP(@NonNull final Context context) {
return true;
}
public static int getShutdownFlingVelocity(@Nonnull final Context context) {
public static int getShutdownFlingVelocity(@NonNull final Context context) {
return 10000;
}
public static int getTossFlingVelocity(@Nonnull final Context context) {
public static int getTossFlingVelocity(@NonNull final Context context) {
return 2500;
}
@NonNull
public static CaptionStyleCompat getCaptionStyle(@NonNull final Context context) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT) return CaptionStyleCompat.DEFAULT;
final CaptioningManager captioningManager = (CaptioningManager)
context.getSystemService(Context.CAPTIONING_SERVICE);
if (captioningManager == null || !captioningManager.isEnabled()) {
return CaptionStyleCompat.DEFAULT;
}
return CaptionStyleCompat.createFromCaptionStyle(captioningManager.getUserStyle());
}
/**
* System font scaling:
* Very small - 0.25f, Small - 0.5f, Normal - 1.0f, Large - 1.5f, Very Large - 2.0f
* */
@NonNull
public static float getCaptionScale(@NonNull final Context context) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT) return 1f;
final CaptioningManager captioningManager = (CaptioningManager)
context.getSystemService(Context.CAPTIONING_SERVICE);
if (captioningManager == null || !captioningManager.isEnabled()) {
return 1f;
}
return captioningManager.getFontScale();
}
////////////////////////////////////////////////////////////////////////////
// Private helpers
////////////////////////////////////////////////////////////////////////////
@@ -152,7 +281,15 @@ public class PlayerHelper {
return getPreferences(context).getBoolean(context.getString(R.string.use_old_player_key), b);
}
private static boolean isRememberingPopupDimensions(@Nonnull final Context context, final boolean b) {
private static boolean isRememberingPopupDimensions(@NonNull final Context context, final boolean b) {
return getPreferences(context).getBoolean(context.getString(R.string.popup_remember_size_pos_key), b);
}
private static boolean isUsingInexactSeek(@NonNull final Context context, final boolean b) {
return getPreferences(context).getBoolean(context.getString(R.string.use_inexact_seek_key), b);
}
private static boolean isAutoQueueEnabled(@NonNull final Context context, final boolean b) {
return getPreferences(context).getBoolean(context.getString(R.string.auto_queue_key), b);
}
}

View File

@@ -0,0 +1,45 @@
package org.schabi.newpipe.player.mediasession;
import android.net.Uri;
import android.os.Bundle;
import android.os.ResultReceiver;
import com.google.android.exoplayer2.Player;
import com.google.android.exoplayer2.ext.mediasession.MediaSessionConnector;
public class DummyPlaybackPreparer implements MediaSessionConnector.PlaybackPreparer {
@Override
public long getSupportedPrepareActions() {
return 0;
}
@Override
public void onPrepare() {
}
@Override
public void onPrepareFromMediaId(String mediaId, Bundle extras) {
}
@Override
public void onPrepareFromSearch(String query, Bundle extras) {
}
@Override
public void onPrepareFromUri(Uri uri, Bundle extras) {
}
@Override
public String[] getCommands() {
return new String[0];
}
@Override
public void onCommand(Player player, String command, Bundle extras, ResultReceiver cb) {
}
}

View File

@@ -0,0 +1,17 @@
package org.schabi.newpipe.player.mediasession;
import android.support.v4.media.MediaDescriptionCompat;
public interface MediaSessionCallback {
void onSkipToPrevious();
void onSkipToNext();
void onSkipToIndex(final int index);
int getCurrentPlayingIndex();
int getQueueSize();
MediaDescriptionCompat getQueueMetadata(final int index);
void onPlay();
void onPause();
void onSetShuffle(final boolean isShuffled);
}

View File

@@ -0,0 +1,111 @@
package org.schabi.newpipe.player.mediasession;
import android.os.Bundle;
import android.os.ResultReceiver;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.v4.media.session.MediaSessionCompat;
import com.google.android.exoplayer2.Player;
import com.google.android.exoplayer2.ext.mediasession.MediaSessionConnector;
import com.google.android.exoplayer2.util.Util;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import static android.support.v4.media.session.PlaybackStateCompat.ACTION_SKIP_TO_NEXT;
import static android.support.v4.media.session.PlaybackStateCompat.ACTION_SKIP_TO_PREVIOUS;
import static android.support.v4.media.session.PlaybackStateCompat.ACTION_SKIP_TO_QUEUE_ITEM;
public class PlayQueueNavigator implements MediaSessionConnector.QueueNavigator {
public static final int DEFAULT_MAX_QUEUE_SIZE = 10;
private final MediaSessionCompat mediaSession;
private final MediaSessionCallback callback;
private final int maxQueueSize;
private long activeQueueItemId;
public PlayQueueNavigator(@NonNull final MediaSessionCompat mediaSession,
@NonNull final MediaSessionCallback callback) {
this.mediaSession = mediaSession;
this.callback = callback;
this.maxQueueSize = DEFAULT_MAX_QUEUE_SIZE;
this.activeQueueItemId = MediaSessionCompat.QueueItem.UNKNOWN_ID;
}
@Override
public long getSupportedQueueNavigatorActions(@Nullable Player player) {
return ACTION_SKIP_TO_NEXT | ACTION_SKIP_TO_PREVIOUS | ACTION_SKIP_TO_QUEUE_ITEM;
}
@Override
public void onTimelineChanged(Player player) {
publishFloatingQueueWindow();
}
@Override
public void onCurrentWindowIndexChanged(Player player) {
if (activeQueueItemId == MediaSessionCompat.QueueItem.UNKNOWN_ID
|| player.getCurrentTimeline().getWindowCount() > maxQueueSize) {
publishFloatingQueueWindow();
} else if (!player.getCurrentTimeline().isEmpty()) {
activeQueueItemId = player.getCurrentWindowIndex();
}
}
@Override
public long getActiveQueueItemId(@Nullable Player player) {
return callback.getCurrentPlayingIndex();
}
@Override
public void onSkipToPrevious(Player player) {
callback.onSkipToPrevious();
}
@Override
public void onSkipToQueueItem(Player player, long id) {
callback.onSkipToIndex((int) id);
}
@Override
public void onSkipToNext(Player player) {
callback.onSkipToNext();
}
private void publishFloatingQueueWindow() {
if (callback.getQueueSize() == 0) {
mediaSession.setQueue(Collections.<MediaSessionCompat.QueueItem>emptyList());
activeQueueItemId = MediaSessionCompat.QueueItem.UNKNOWN_ID;
return;
}
// Yes this is almost a copypasta, got a problem with that? =\
int windowCount = callback.getQueueSize();
int currentWindowIndex = callback.getCurrentPlayingIndex();
int queueSize = Math.min(maxQueueSize, windowCount);
int startIndex = Util.constrainValue(currentWindowIndex - ((queueSize - 1) / 2), 0,
windowCount - queueSize);
List<MediaSessionCompat.QueueItem> queue = new ArrayList<>();
for (int i = startIndex; i < startIndex + queueSize; i++) {
queue.add(new MediaSessionCompat.QueueItem(callback.getQueueMetadata(i), i));
}
mediaSession.setQueue(queue);
activeQueueItemId = currentWindowIndex;
}
@Override
public String[] getCommands() {
return new String[0];
}
@Override
public void onCommand(Player player, String command, Bundle extras, ResultReceiver cb) {
}
}

View File

@@ -0,0 +1,31 @@
package org.schabi.newpipe.player.mediasession;
import android.support.v4.media.session.PlaybackStateCompat;
import com.google.android.exoplayer2.Player;
import com.google.android.exoplayer2.ext.mediasession.DefaultPlaybackController;
public class PlayQueuePlaybackController extends DefaultPlaybackController {
private final MediaSessionCallback callback;
public PlayQueuePlaybackController(final MediaSessionCallback callback) {
super();
this.callback = callback;
}
@Override
public void onPlay(Player player) {
callback.onPlay();
}
@Override
public void onPause(Player player) {
callback.onPause();
}
@Override
public void onSetShuffleMode(Player player, int shuffleMode) {
callback.onSetShuffle(shuffleMode == PlaybackStateCompat.SHUFFLE_MODE_ALL
|| shuffleMode == PlaybackStateCompat.SHUFFLE_MODE_GROUP);
}
}

View File

@@ -0,0 +1,106 @@
package org.schabi.newpipe.player.mediasource;
import android.support.annotation.NonNull;
import android.util.Log;
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 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 FailedMediaSourceException error;
private final long retryTimestamp;
public FailedMediaSource(@NonNull final PlayQueueItem playQueueItem,
@NonNull final FailedMediaSourceException error,
final long retryTimestamp) {
this.playQueueItem = playQueueItem;
this.error = error;
this.retryTimestamp = retryTimestamp;
}
/**
* Permanently fail the play queue item associated with this source, with no hope of retrying.
* The error will always be propagated to ExoPlayer.
* */
public FailedMediaSource(@NonNull final PlayQueueItem playQueueItem,
@NonNull final FailedMediaSourceException error) {
this.playQueueItem = playQueueItem;
this.error = error;
this.retryTimestamp = Long.MAX_VALUE;
}
public PlayQueueItem getStream() {
return playQueueItem;
}
public FailedMediaSourceException getError() {
return error;
}
private boolean canRetry() {
return System.currentTimeMillis() >= retryTimestamp;
}
@Override
public void prepareSource(ExoPlayer player, boolean isTopLevelSource, Listener listener) {
Log.e(TAG, "Loading failed source: ", error);
}
@Override
public void maybeThrowSourceInfoRefreshError() throws IOException {
throw new IOException(error);
}
@Override
public MediaPeriod createPeriod(MediaPeriodId id, Allocator allocator) {
return null;
}
@Override
public void releasePeriod(MediaPeriod mediaPeriod) {}
@Override
public void releaseSource() {}
@Override
public boolean shouldBeReplacedWith(@NonNull final PlayQueueItem newIdentity,
final boolean isInterruptable) {
return newIdentity != playQueueItem || canRetry();
}
@Override
public boolean isStreamEqual(@NonNull PlayQueueItem stream) {
return playQueueItem == stream;
}
}

View File

@@ -0,0 +1,71 @@
package org.schabi.newpipe.player.mediasource;
import android.support.annotation.NonNull;
import com.google.android.exoplayer2.ExoPlayer;
import com.google.android.exoplayer2.source.MediaPeriod;
import com.google.android.exoplayer2.source.MediaSource;
import com.google.android.exoplayer2.upstream.Allocator;
import org.schabi.newpipe.playlist.PlayQueueItem;
import java.io.IOException;
public class LoadedMediaSource implements ManagedMediaSource {
private final MediaSource source;
private final PlayQueueItem stream;
private final long expireTimestamp;
public LoadedMediaSource(@NonNull final MediaSource source,
@NonNull final PlayQueueItem stream,
final long expireTimestamp) {
this.source = source;
this.stream = stream;
this.expireTimestamp = expireTimestamp;
}
public PlayQueueItem getStream() {
return stream;
}
private boolean isExpired() {
return System.currentTimeMillis() >= expireTimestamp;
}
@Override
public void prepareSource(ExoPlayer player, boolean isTopLevelSource, Listener listener) {
source.prepareSource(player, isTopLevelSource, listener);
}
@Override
public void maybeThrowSourceInfoRefreshError() throws IOException {
source.maybeThrowSourceInfoRefreshError();
}
@Override
public MediaPeriod createPeriod(MediaPeriodId id, Allocator allocator) {
return source.createPeriod(id, allocator);
}
@Override
public void releasePeriod(MediaPeriod mediaPeriod) {
source.releasePeriod(mediaPeriod);
}
@Override
public void releaseSource() {
source.releaseSource();
}
@Override
public boolean shouldBeReplacedWith(@NonNull PlayQueueItem newIdentity,
final boolean isInterruptable) {
return newIdentity != stream || (isInterruptable && isExpired());
}
@Override
public boolean isStreamEqual(@NonNull PlayQueueItem stream) {
return this.stream == stream;
}
}

View File

@@ -0,0 +1,27 @@
package org.schabi.newpipe.player.mediasource;
import android.support.annotation.NonNull;
import com.google.android.exoplayer2.source.MediaSource;
import org.schabi.newpipe.playlist.PlayQueueItem;
public interface ManagedMediaSource extends MediaSource {
/**
* Determines whether or not this {@link ManagedMediaSource} can be replaced.
*
* @param newIdentity a stream the {@link ManagedMediaSource} should encapsulate over, if
* it is different from the existing stream in the
* {@link ManagedMediaSource}, then it should be replaced.
* @param isInterruptable specifies if this {@link ManagedMediaSource} potentially
* being played.
* */
boolean shouldBeReplacedWith(@NonNull final PlayQueueItem newIdentity,
final boolean isInterruptable);
/**
* Determines if the {@link PlayQueueItem} is the one the
* {@link ManagedMediaSource} encapsulates over.
* */
boolean isStreamEqual(@NonNull final PlayQueueItem stream);
}

View File

@@ -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);
}
}

View File

@@ -0,0 +1,31 @@
package org.schabi.newpipe.player.mediasource;
import android.support.annotation.NonNull;
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 java.io.IOException;
public class PlaceholderMediaSource implements ManagedMediaSource {
// Do nothing, so this will stall the playback
@Override public void prepareSource(ExoPlayer player, boolean isTopLevelSource, Listener listener) {}
@Override public void maybeThrowSourceInfoRefreshError() throws IOException {}
@Override public MediaPeriod createPeriod(MediaPeriodId id, Allocator allocator) { return null; }
@Override public void releasePeriod(MediaPeriod mediaPeriod) {}
@Override public void releaseSource() {}
@Override
public boolean shouldBeReplacedWith(@NonNull PlayQueueItem newIdentity,
final boolean isInterruptable) {
return true;
}
@Override
public boolean isStreamEqual(@NonNull PlayQueueItem stream) {
return false;
}
}

View File

@@ -0,0 +1,77 @@
package org.schabi.newpipe.player.playback;
import android.net.Uri;
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;
public class BasePlayerMediaSession implements MediaSessionCallback {
private BasePlayer player;
public BasePlayerMediaSession(final BasePlayer player) {
this.player = player;
}
@Override
public void onSkipToPrevious() {
player.onPlayPrevious();
}
@Override
public void onSkipToNext() {
player.onPlayNext();
}
@Override
public void onSkipToIndex(int index) {
if (player.getPlayQueue() == null) return;
player.onSelected(player.getPlayQueue().getItem(index));
}
@Override
public int getCurrentPlayingIndex() {
if (player.getPlayQueue() == null) return -1;
return player.getPlayQueue().getIndex();
}
@Override
public int getQueueSize() {
if (player.getPlayQueue() == null) return -1;
return player.getPlayQueue().size();
}
@Override
public MediaDescriptionCompat getQueueMetadata(int index) {
if (player.getPlayQueue() == null || player.getPlayQueue().getItem(index) == null) {
return null;
}
final PlayQueueItem item = player.getPlayQueue().getItem(index);
MediaDescriptionCompat.Builder descriptionBuilder = new MediaDescriptionCompat.Builder()
.setMediaId(String.valueOf(index))
.setTitle(item.getTitle())
.setSubtitle(item.getUploader());
final Uri thumbnailUri = Uri.parse(item.getThumbnailUrl());
if (thumbnailUri != null) descriptionBuilder.setIconUri(thumbnailUri);
return descriptionBuilder.build();
}
@Override
public void onPlay() {
player.onPlay();
}
@Override
public void onPause() {
player.onPause();
}
@Override
public void onSetShuffle(boolean isShuffled) {
player.onShuffleModeEnabledChanged(isShuffled);
}
}

View File

@@ -0,0 +1,114 @@
package org.schabi.newpipe.player.playback;
import android.support.annotation.NonNull;
import android.text.TextUtils;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.ExoPlaybackException;
import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.source.TrackGroup;
import com.google.android.exoplayer2.source.TrackGroupArray;
import com.google.android.exoplayer2.trackselection.DefaultTrackSelector;
import com.google.android.exoplayer2.trackselection.FixedTrackSelection;
import com.google.android.exoplayer2.trackselection.TrackSelection;
import com.google.android.exoplayer2.util.Assertions;
import com.google.android.exoplayer2.util.Util;
/**
* This class allows irregular text language labels for use when selecting text captions and
* is mostly a copy-paste from {@link DefaultTrackSelector}.
*
* This is a hack and should be removed once ExoPlayer fixes language normalization to accept
* a broader set of languages.
* */
public class CustomTrackSelector extends DefaultTrackSelector {
private static final int WITHIN_RENDERER_CAPABILITIES_BONUS = 1000;
private String preferredTextLanguage;
public CustomTrackSelector(TrackSelection.Factory adaptiveTrackSelectionFactory) {
super(adaptiveTrackSelectionFactory);
}
public String getPreferredTextLanguage() {
return preferredTextLanguage;
}
public void setPreferredTextLanguage(@NonNull final String label) {
Assertions.checkNotNull(label);
if (!label.equals(preferredTextLanguage)) {
preferredTextLanguage = label;
invalidate();
}
}
/** @see DefaultTrackSelector#formatHasLanguage(Format, String)*/
protected static boolean formatHasLanguage(Format format, String language) {
return language != null && TextUtils.equals(language, format.language);
}
/** @see DefaultTrackSelector#formatHasNoLanguage(Format)*/
protected static boolean formatHasNoLanguage(Format format) {
return TextUtils.isEmpty(format.language) || formatHasLanguage(format, C.LANGUAGE_UNDETERMINED);
}
/** @see DefaultTrackSelector#selectTextTrack(TrackGroupArray, int[][], Parameters) */
@Override
protected TrackSelection selectTextTrack(TrackGroupArray groups, int[][] formatSupport,
Parameters params) throws ExoPlaybackException {
TrackGroup selectedGroup = null;
int selectedTrackIndex = 0;
int selectedTrackScore = 0;
for (int groupIndex = 0; groupIndex < groups.length; groupIndex++) {
TrackGroup trackGroup = groups.get(groupIndex);
int[] trackFormatSupport = formatSupport[groupIndex];
for (int trackIndex = 0; trackIndex < trackGroup.length; trackIndex++) {
if (isSupported(trackFormatSupport[trackIndex],
params.exceedRendererCapabilitiesIfNecessary)) {
Format format = trackGroup.getFormat(trackIndex);
int maskedSelectionFlags =
format.selectionFlags & ~params.disabledTextTrackSelectionFlags;
boolean isDefault = (maskedSelectionFlags & C.SELECTION_FLAG_DEFAULT) != 0;
boolean isForced = (maskedSelectionFlags & C.SELECTION_FLAG_FORCED) != 0;
int trackScore;
boolean preferredLanguageFound = formatHasLanguage(format, preferredTextLanguage);
if (preferredLanguageFound
|| (params.selectUndeterminedTextLanguage && formatHasNoLanguage(format))) {
if (isDefault) {
trackScore = 8;
} else if (!isForced) {
// Prefer non-forced to forced if a preferred text language has been specified. Where
// both are provided the non-forced track will usually contain the forced subtitles as
// a subset.
trackScore = 6;
} else {
trackScore = 4;
}
trackScore += preferredLanguageFound ? 1 : 0;
} else if (isDefault) {
trackScore = 3;
} else if (isForced) {
if (formatHasLanguage(format, params.preferredAudioLanguage)) {
trackScore = 2;
} else {
trackScore = 1;
}
} else {
// Track should not be selected.
continue;
}
if (isSupported(trackFormatSupport[trackIndex], false)) {
trackScore += WITHIN_RENDERER_CAPABILITIES_BONUS;
}
if (trackScore > selectedTrackScore) {
selectedGroup = trackGroup;
selectedTrackIndex = trackIndex;
selectedTrackScore = trackScore;
}
}
}
}
return selectedGroup == null ? null
: new FixedTrackSelection(selectedGroup, selectedTrackIndex);
}
}

View File

@@ -1,216 +0,0 @@
package org.schabi.newpipe.player.playback;
import android.support.annotation.NonNull;
import android.util.Log;
import com.google.android.exoplayer2.ExoPlayer;
import com.google.android.exoplayer2.source.MediaPeriod;
import com.google.android.exoplayer2.source.MediaSource;
import com.google.android.exoplayer2.upstream.Allocator;
import org.schabi.newpipe.extractor.stream.StreamInfo;
import org.schabi.newpipe.playlist.PlayQueueItem;
import java.io.IOException;
import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.disposables.Disposable;
import io.reactivex.functions.Consumer;
import io.reactivex.functions.Function;
import io.reactivex.schedulers.Schedulers;
/**
* DeferredMediaSource is specifically designed to allow external control over when
* the source metadata are loaded while being compatible with ExoPlayer's playlists.
*
* This media source follows the structure of how NewPipeExtractor's
* {@link org.schabi.newpipe.extractor.stream.StreamInfoItem} is converted into
* {@link org.schabi.newpipe.extractor.stream.StreamInfo}. Once conversion is complete,
* this media source behaves identically as any other native media sources.
* */
public final class DeferredMediaSource implements MediaSource {
private final String TAG = "DeferredMediaSource@" + Integer.toHexString(hashCode());
/**
* This state indicates the {@link DeferredMediaSource} has just been initialized or reset.
* The source must be prepared and loaded again before playback.
* */
public final static int STATE_INIT = 0;
/**
* This state indicates the {@link DeferredMediaSource} has been prepared and is ready to load.
* */
public final static int STATE_PREPARED = 1;
/**
* This state indicates the {@link DeferredMediaSource} has been loaded without errors and
* is ready for playback.
* */
public final static int STATE_LOADED = 2;
public interface Callback {
/**
* Player-specific {@link com.google.android.exoplayer2.source.MediaSource} resolution
* from a given StreamInfo.
* */
MediaSource sourceOf(final PlayQueueItem item, final StreamInfo info);
}
private PlayQueueItem stream;
private Callback callback;
private int state;
private MediaSource mediaSource;
/* Custom internal objects */
private Disposable loader;
private ExoPlayer exoPlayer;
private Listener listener;
private Throwable error;
public DeferredMediaSource(@NonNull final PlayQueueItem stream,
@NonNull final Callback callback) {
this.stream = stream;
this.callback = callback;
this.state = STATE_INIT;
}
/**
* Returns the current state of the {@link DeferredMediaSource}.
*
* @see DeferredMediaSource#STATE_INIT
* @see DeferredMediaSource#STATE_PREPARED
* @see DeferredMediaSource#STATE_LOADED
* */
public int state() {
return state;
}
/**
* Parameters are kept in the class for delayed preparation.
* */
@Override
public void prepareSource(ExoPlayer exoPlayer, boolean isTopLevelSource, Listener listener) {
this.exoPlayer = exoPlayer;
this.listener = listener;
this.state = STATE_PREPARED;
}
/**
* Externally controlled loading. This method fully prepares the source to be used
* like any other native {@link com.google.android.exoplayer2.source.MediaSource}.
*
* Ideally, this should be called after this source has entered PREPARED state and
* called once only.
*
* If loading fails here, an error will be propagated out and result in an
* {@link com.google.android.exoplayer2.ExoPlaybackException ExoPlaybackException},
* which is delegated to the player.
* */
public synchronized void load() {
if (stream == null) {
Log.e(TAG, "Stream Info missing, media source loading terminated.");
return;
}
if (state != STATE_PREPARED || loader != null) return;
Log.d(TAG, "Loading: [" + stream.getTitle() + "] with url: " + stream.getUrl());
loader = stream.getStream()
.map(streamInfo -> onStreamInfoReceived(stream, streamInfo))
.observeOn(AndroidSchedulers.mainThread())
.subscribe(this::onMediaSourceReceived, this::onStreamInfoError);
}
private MediaSource onStreamInfoReceived(@NonNull final PlayQueueItem item,
@NonNull final StreamInfo info) throws Exception {
if (callback == null) {
throw new Exception("No available callback for resolving stream info.");
}
final MediaSource mediaSource = callback.sourceOf(item, info);
if (mediaSource == null) {
throw new Exception("Unable to resolve source from stream info. URL: " + stream.getUrl() +
", audio count: " + info.audio_streams.size() +
", video count: " + info.video_only_streams.size() + info.video_streams.size());
}
return mediaSource;
}
private void onMediaSourceReceived(final MediaSource mediaSource) throws Exception {
if (exoPlayer == null || listener == null || mediaSource == null) {
throw new Exception("MediaSource loading failed. URL: " + stream.getUrl());
}
Log.d(TAG, " Loaded: [" + stream.getTitle() + "] with url: " + stream.getUrl());
state = STATE_LOADED;
this.mediaSource = mediaSource;
this.mediaSource.prepareSource(exoPlayer, false, listener);
}
private void onStreamInfoError(final Throwable throwable) {
Log.e(TAG, "Loading error:", throwable);
error = throwable;
state = STATE_LOADED;
}
/**
* Delegate all errors to the player after {@link #load() load} is complete.
*
* Specifically, this method is called after an exception has occurred during loading or
* {@link com.google.android.exoplayer2.source.MediaSource#prepareSource(ExoPlayer, boolean, Listener) prepareSource}.
* */
@Override
public void maybeThrowSourceInfoRefreshError() throws IOException {
if (error != null) {
throw new IOException(error);
}
if (mediaSource != null) {
mediaSource.maybeThrowSourceInfoRefreshError();
}
}
@Override
public MediaPeriod createPeriod(MediaPeriodId mediaPeriodId, Allocator allocator) {
return mediaSource.createPeriod(mediaPeriodId, allocator);
}
/**
* Releases the media period (buffers).
*
* This may be called after {@link #releaseSource releaseSource}.
* */
@Override
public void releasePeriod(MediaPeriod mediaPeriod) {
mediaSource.releasePeriod(mediaPeriod);
}
/**
* Cleans up all internal custom objects creating during loading.
*
* This method is called when the parent {@link com.google.android.exoplayer2.source.MediaSource}
* is released or when the player is stopped.
*
* This method should not release or set null the resources passed in through the constructor.
* This method should not set null the internal {@link com.google.android.exoplayer2.source.MediaSource}.
* */
@Override
public void releaseSource() {
if (mediaSource != null) {
mediaSource.releaseSource();
}
if (loader != null) {
loader.dispose();
}
/* Do not set mediaSource as null here as it may be called through releasePeriod */
loader = null;
exoPlayer = null;
listener = null;
error = null;
state = STATE_INIT;
}
}

View File

@@ -1,96 +1,162 @@
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 org.reactivestreams.Subscriber;
import org.reactivestreams.Subscription;
import org.schabi.newpipe.extractor.stream.StreamInfo;
import org.schabi.newpipe.player.mediasource.FailedMediaSource;
import org.schabi.newpipe.player.mediasource.LoadedMediaSource;
import org.schabi.newpipe.player.mediasource.ManagedMediaSource;
import org.schabi.newpipe.player.mediasource.ManagedMediaSourcePlaylist;
import org.schabi.newpipe.player.mediasource.PlaceholderMediaSource;
import org.schabi.newpipe.playlist.PlayQueue;
import org.schabi.newpipe.playlist.PlayQueueItem;
import org.schabi.newpipe.playlist.events.MoveEvent;
import org.schabi.newpipe.playlist.events.PlayQueueEvent;
import org.schabi.newpipe.playlist.events.RemoveEvent;
import org.schabi.newpipe.playlist.events.ReorderEvent;
import org.schabi.newpipe.util.ServiceHelper;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import io.reactivex.Observable;
import io.reactivex.Single;
import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.annotations.NonNull;
import io.reactivex.disposables.CompositeDisposable;
import io.reactivex.disposables.Disposable;
import io.reactivex.disposables.SerialDisposable;
import io.reactivex.functions.Consumer;
import io.reactivex.internal.subscriptions.EmptySubscription;
import io.reactivex.schedulers.Schedulers;
import io.reactivex.subjects.PublishSubject;
import static org.schabi.newpipe.player.mediasource.FailedMediaSource.MediaSourceResolutionException;
import static org.schabi.newpipe.player.mediasource.FailedMediaSource.StreamInfoLoadException;
import static org.schabi.newpipe.playlist.PlayQueue.DEBUG;
public class MediaSourceManager {
private final String TAG = "MediaSourceManager@" + Integer.toHexString(hashCode());
// One-side rolling window size for default loading
// Effectively loads windowSize * 2 + 1 streams per call to load, must be greater than 0
private final int windowSize;
private final PlaybackListener playbackListener;
private final PlayQueue playQueue;
@NonNull private final String TAG = "MediaSourceManager@" + hashCode();
// Process only the last load order when receiving a stream of load orders (lessens I/O)
// The higher it is, the less loading occurs during rapid noncritical timeline changes
// Not recommended to go below 100ms
/**
* Determines how many streams before and after the current stream should be loaded.
* The default value (1) ensures seamless playback under typical network settings.
* <br><br>
* The streams after the current will be loaded into the playlist timeline while the
* streams before will only be cached for future usage.
*
* @see #onMediaSourceReceived(PlayQueueItem, ManagedMediaSource)
* */
private final static int WINDOW_SIZE = 1;
@NonNull private final PlaybackListener playbackListener;
@NonNull private final PlayQueue playQueue;
/**
* Determines the gap time between the playback position and the playback duration which
* the {@link #getEdgeIntervalSignal()} begins to request loading.
*
* @see #progressUpdateIntervalMillis
* */
private final long playbackNearEndGapMillis;
/**
* Determines the interval which the {@link #getEdgeIntervalSignal()} waits for between
* each request for loading, once {@link #playbackNearEndGapMillis} has reached.
* */
private final long progressUpdateIntervalMillis;
@NonNull private final Observable<Long> nearEndIntervalSignal;
/**
* Process only the last load order when receiving a stream of load orders (lessens I/O).
* <br><br>
* The higher it is, the less loading occurs during rapid noncritical timeline changes.
* <br><br>
* Not recommended to go below 100ms.
*
* @see #loadDebounced()
* */
private final long loadDebounceMillis;
private final PublishSubject<Long> debouncedLoadSignal;
private final Disposable debouncedLoader;
@NonNull private final Disposable debouncedLoader;
@NonNull private final PublishSubject<Long> debouncedSignal;
private final DeferredMediaSource.Callback sourceBuilder;
@NonNull private Subscription playQueueReactor;
private DynamicConcatenatingMediaSource sources;
/**
* Determines the maximum number of disposables allowed in the {@link #loaderReactor}.
* Once exceeded, new calls to {@link #loadImmediate()} will evict all disposables in the
* {@link #loaderReactor} in order to load a new set of items.
*
* @see #loadImmediate()
* @see #maybeLoadItem(PlayQueueItem)
* */
private final static int MAXIMUM_LOADER_SIZE = WINDOW_SIZE * 2 + 1;
@NonNull private final CompositeDisposable loaderReactor;
@NonNull private final Set<PlayQueueItem> loadingItems;
@NonNull private final SerialDisposable syncReactor;
private Subscription playQueueReactor;
private SerialDisposable syncReactor;
@NonNull private final AtomicBoolean isBlocked;
private PlayQueueItem syncedItem;
private boolean isBlocked;
@NonNull private ManagedMediaSourcePlaylist playlist;
public MediaSourceManager(@NonNull final PlaybackListener listener,
@NonNull final PlayQueue playQueue) {
this(listener, playQueue, 1, 400L);
this(listener, playQueue, /*loadDebounceMillis=*/400L,
/*playbackNearEndGapMillis=*/TimeUnit.MILLISECONDS.convert(30, TimeUnit.SECONDS),
/*progressUpdateIntervalMillis*/TimeUnit.MILLISECONDS.convert(2, TimeUnit.SECONDS));
}
private MediaSourceManager(@NonNull final PlaybackListener listener,
@NonNull final PlayQueue playQueue,
final int windowSize,
final long loadDebounceMillis) {
if (windowSize <= 0) {
throw new UnsupportedOperationException("MediaSourceManager window size must be greater than 0");
final long loadDebounceMillis,
final long playbackNearEndGapMillis,
final long progressUpdateIntervalMillis) {
if (playQueue.getBroadcastReceiver() == null) {
throw new IllegalArgumentException("Play Queue has not been initialized.");
}
if (playbackNearEndGapMillis < progressUpdateIntervalMillis) {
throw new IllegalArgumentException("Playback end gap=[" + playbackNearEndGapMillis +
" ms] must be longer than update interval=[ " + progressUpdateIntervalMillis +
" ms] for them to be useful.");
}
this.playbackListener = listener;
this.playQueue = playQueue;
this.windowSize = windowSize;
this.loadDebounceMillis = loadDebounceMillis;
this.syncReactor = new SerialDisposable();
this.debouncedLoadSignal = PublishSubject.create();
this.playbackNearEndGapMillis = playbackNearEndGapMillis;
this.progressUpdateIntervalMillis = progressUpdateIntervalMillis;
this.nearEndIntervalSignal = getEdgeIntervalSignal();
this.loadDebounceMillis = loadDebounceMillis;
this.debouncedSignal = PublishSubject.create();
this.debouncedLoader = getDebouncedLoader();
this.sourceBuilder = getSourceBuilder();
this.playQueueReactor = EmptySubscription.INSTANCE;
this.loaderReactor = new CompositeDisposable();
this.syncReactor = new SerialDisposable();
this.sources = new DynamicConcatenatingMediaSource();
this.isBlocked = new AtomicBoolean(false);
this.playlist = new ManagedMediaSourcePlaylist();
this.loadingItems = Collections.synchronizedSet(new ArraySet<>());
playQueue.getBroadcastReceiver()
.observeOn(AndroidSchedulers.mainThread())
.subscribe(getReactor());
}
/*//////////////////////////////////////////////////////////////////////////
// DeferredMediaSource listener
//////////////////////////////////////////////////////////////////////////*/
private DeferredMediaSource.Callback getSourceBuilder() {
return playbackListener::sourceOf;
}
/*//////////////////////////////////////////////////////////////////////////
// Exposed Methods
//////////////////////////////////////////////////////////////////////////*/
@@ -98,38 +164,17 @@ public class MediaSourceManager {
* Dispose the manager and releases all message buses and loaders.
* */
public void dispose() {
if (debouncedLoadSignal != null) debouncedLoadSignal.onComplete();
if (debouncedLoader != null) debouncedLoader.dispose();
if (playQueueReactor != null) playQueueReactor.cancel();
if (syncReactor != null) syncReactor.dispose();
if (sources != null) sources.releaseSource();
if (DEBUG) Log.d(TAG, "dispose() called.");
playQueueReactor = null;
syncReactor = null;
syncedItem = null;
sources = null;
debouncedSignal.onComplete();
debouncedLoader.dispose();
playQueueReactor.cancel();
loaderReactor.dispose();
syncReactor.dispose();
playlist.dispose();
}
/**
* Loads the current playing stream and the streams within its windowSize bound.
*
* Unblocks the player once the item at the current index is loaded.
* */
public void load() {
loadDebounced();
}
/**
* Blocks the player and repopulate the sources.
*
* Does not ensure the player is unblocked and should be done explicitly through {@link #load() load}.
* */
public void reset() {
tryBlock();
syncedItem = null;
populateSources();
}
/*//////////////////////////////////////////////////////////////////////////
// Event Reactor
//////////////////////////////////////////////////////////////////////////*/
@@ -138,14 +183,14 @@ public class MediaSourceManager {
return new Subscriber<PlayQueueEvent>() {
@Override
public void onSubscribe(@NonNull Subscription d) {
if (playQueueReactor != null) playQueueReactor.cancel();
playQueueReactor.cancel();
playQueueReactor = d;
playQueueReactor.request(1);
}
@Override
public void onNext(@NonNull PlayQueueEvent playQueueMessage) {
if (playQueueReactor != null) onPlayQueueChanged(playQueueMessage);
onPlayQueueChanged(playQueueMessage);
}
@Override
@@ -158,29 +203,36 @@ public class MediaSourceManager {
private void onPlayQueueChanged(final PlayQueueEvent event) {
if (playQueue.isEmpty() && playQueue.isComplete()) {
playbackListener.shutdown();
playbackListener.onPlaybackShutdown();
return;
}
// Event specific action
switch (event.type()) {
case INIT:
case REORDER:
case ERROR:
reset();
break;
maybeBlock();
case APPEND:
populateSources();
break;
case SELECT:
maybeRenewCurrentIndex();
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;
playlist.move(reorderEvent.getFromSelectedIndex(),
reorderEvent.getToSelectedIndex());
break;
case SELECT:
case RECOVERY:
default:
break;
@@ -191,11 +243,11 @@ public class MediaSourceManager {
case INIT:
case REORDER:
case ERROR:
case SELECT:
loadImmediate(); // low frequency, critical events
break;
case APPEND:
case REMOVE:
case SELECT:
case MOVE:
case RECOVERY:
default:
@@ -204,157 +256,281 @@ public class MediaSourceManager {
}
if (!isPlayQueueReady()) {
tryBlock();
maybeBlock();
playQueue.fetch();
}
if (playQueueReactor != null) playQueueReactor.request(1);
playQueueReactor.request(1);
}
/*//////////////////////////////////////////////////////////////////////////
// Internal Helpers
// Playback Locking
//////////////////////////////////////////////////////////////////////////*/
private boolean isPlayQueueReady() {
return playQueue.isComplete() || playQueue.size() - playQueue.getIndex() > windowSize;
final boolean isWindowLoaded = playQueue.size() - playQueue.getIndex() > WINDOW_SIZE;
return playQueue.isComplete() || isWindowLoaded;
}
private boolean tryBlock() {
if (!isBlocked) {
playbackListener.block();
resetSources();
isBlocked = true;
return true;
private boolean isPlaybackReady() {
if (playlist.size() != playQueue.size()) return false;
final ManagedMediaSource mediaSource = playlist.get(playQueue.getIndex());
if (mediaSource == null) return false;
final PlayQueueItem playQueueItem = playQueue.getItem();
return mediaSource.isStreamEqual(playQueueItem);
}
private void maybeBlock() {
if (DEBUG) Log.d(TAG, "maybeBlock() called.");
if (isBlocked.get()) return;
playbackListener.onPlaybackBlock();
resetSources();
isBlocked.set(true);
}
private void maybeUnblock() {
if (DEBUG) Log.d(TAG, "maybeUnblock() called.");
if (isBlocked.get()) {
isBlocked.set(false);
playbackListener.onPlaybackUnblock(playlist.getParentMediaSource());
}
return false;
}
private boolean tryUnblock() {
if (isPlayQueueReady() && isBlocked && sources != null) {
isBlocked = false;
playbackListener.unblock(sources);
return true;
}
return false;
}
/*//////////////////////////////////////////////////////////////////////////
// Metadata Synchronization
//////////////////////////////////////////////////////////////////////////*/
private void maybeSync() {
if (DEBUG) Log.d(TAG, "maybeSync() called.");
private void sync() {
final PlayQueueItem currentItem = playQueue.getItem();
if (currentItem == null) return;
if (isBlocked.get() || currentItem == null) return;
final Consumer<StreamInfo> onSuccess = info -> syncInternal(currentItem, info);
final Consumer<Throwable> onError = throwable -> {
Log.e(TAG, "Sync error:", throwable);
syncInternal(currentItem, null);
};
final Consumer<Throwable> onError = throwable -> syncInternal(currentItem, null);
if (syncedItem != currentItem) {
syncedItem = currentItem;
final Disposable sync = currentItem.getStream()
.observeOn(AndroidSchedulers.mainThread())
.subscribe(onSuccess, onError);
syncReactor.set(sync);
}
final Disposable sync = currentItem.getStream()
.observeOn(AndroidSchedulers.mainThread())
.subscribe(onSuccess, onError);
syncReactor.set(sync);
}
private void syncInternal(@android.support.annotation.NonNull final PlayQueueItem item,
private void syncInternal(@NonNull final PlayQueueItem item,
@Nullable final StreamInfo info) {
if (playQueue == null || playbackListener == null) return;
// Ensure the current item is up to date with the play queue
if (playQueue.getItem() == item && playQueue.getItem() == syncedItem) {
playbackListener.sync(syncedItem,info);
if (playQueue.getItem() == item) {
playbackListener.onPlaybackSynchronize(item, info);
}
}
private void loadDebounced() {
debouncedLoadSignal.onNext(System.currentTimeMillis());
}
private void loadImmediate() {
// The current item has higher priority
final int currentIndex = playQueue.getIndex();
final PlayQueueItem currentItem = playQueue.getItem(currentIndex);
if (currentItem == null) return;
loadItem(currentItem);
// The rest are just for seamless playback
final int leftBound = Math.max(0, currentIndex - windowSize);
final int rightLimit = currentIndex + windowSize + 1;
final int rightBound = Math.min(playQueue.size(), rightLimit);
final List<PlayQueueItem> items = new ArrayList<>(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)));
for (final PlayQueueItem item: items) loadItem(item);
}
private void loadItem(@Nullable final PlayQueueItem item) {
if (item == null) return;
final int index = playQueue.indexOf(item);
if (index > sources.getSize() - 1) return;
final DeferredMediaSource mediaSource = (DeferredMediaSource) sources.getMediaSource(playQueue.indexOf(item));
if (mediaSource.state() == DeferredMediaSource.STATE_PREPARED) mediaSource.load();
tryUnblock();
if (!isBlocked) sync();
}
private void resetSources() {
if (this.sources != null) this.sources.releaseSource();
this.sources = new DynamicConcatenatingMediaSource();
}
private void populateSources() {
if (sources == null) return;
for (final PlayQueueItem item : playQueue.getStreams()) {
insert(playQueue.indexOf(item), new DeferredMediaSource(item, sourceBuilder));
private synchronized void maybeSynchronizePlayer() {
if (isPlayQueueReady() && isPlaybackReady()) {
maybeUnblock();
maybeSync();
}
}
/*//////////////////////////////////////////////////////////////////////////
// MediaSource Loading
//////////////////////////////////////////////////////////////////////////*/
private Observable<Long> getEdgeIntervalSignal() {
return Observable.interval(progressUpdateIntervalMillis, TimeUnit.MILLISECONDS)
.filter(ignored ->
playbackListener.isApproachingPlaybackEdge(playbackNearEndGapMillis));
}
private Disposable getDebouncedLoader() {
return debouncedLoadSignal
return debouncedSignal.mergeWith(nearEndIntervalSignal)
.debounce(loadDebounceMillis, TimeUnit.MILLISECONDS)
.subscribeOn(Schedulers.single())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(timestamp -> loadImmediate());
}
private void loadDebounced() {
debouncedSignal.onNext(System.currentTimeMillis());
}
private void loadImmediate() {
if (DEBUG) Log.d(TAG, "MediaSource - loadImmediate() called");
final ItemsToLoad itemsToLoad = getItemsToLoad(playQueue, WINDOW_SIZE);
if (itemsToLoad == null) return;
// Evict the previous items being loaded to free up memory, before start loading new ones
maybeClearLoaders();
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) >= playlist.size()) return;
if (!loadingItems.contains(item) && isCorrectionNeeded(item)) {
if (DEBUG) Log.d(TAG, "MediaSource - Loading=[" + item.getTitle() +
"] with url=[" + item.getUrl() + "]");
loadingItems.add(item);
final Disposable loader = getLoadedMediaSource(item)
.observeOn(AndroidSchedulers.mainThread())
/* No exception handling since getLoadedMediaSource guarantees nonnull return */
.subscribe(mediaSource -> onMediaSourceReceived(item, mediaSource));
loaderReactor.add(loader);
}
}
private Single<ManagedMediaSource> getLoadedMediaSource(@NonNull final PlayQueueItem stream) {
return stream.getStream().map(streamInfo -> {
final MediaSource source = playbackListener.sourceOf(stream, streamInfo);
if (source == null) {
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,
new StreamInfoLoadException(throwable)));
}
private void onMediaSourceReceived(@NonNull final PlayQueueItem item,
@NonNull final ManagedMediaSource mediaSource) {
if (DEBUG) Log.d(TAG, "MediaSource - Loaded=[" + item.getTitle() +
"] with url=[" + item.getUrl() + "]");
loadingItems.remove(item);
final int itemIndex = playQueue.indexOf(item);
// Only update the playlist timeline for items at the current index or after.
if (isCorrectionNeeded(item)) {
if (DEBUG) Log.d(TAG, "MediaSource - Updating index=[" + itemIndex + "] with " +
"title=[" + item.getTitle() + "] at url=[" + item.getUrl() + "]");
playlist.update(itemIndex, mediaSource, this::maybeSynchronizePlayer);
}
}
/**
* Checks if the corresponding MediaSource in {@link DynamicConcatenatingMediaSource}
* for a given {@link PlayQueueItem} needs replacement, either due to gapless playback
* readiness or playlist desynchronization.
* <br><br>
* If the given {@link PlayQueueItem} is currently being played and is already loaded,
* then correction is not only needed if the playlist is desynchronized. Otherwise, the
* check depends on the status (e.g. expiration or placeholder) of the
* {@link ManagedMediaSource}.
* */
private boolean isCorrectionNeeded(@NonNull final PlayQueueItem item) {
final int index = playQueue.indexOf(item);
final ManagedMediaSource mediaSource = playlist.get(index);
return mediaSource != null && mediaSource.shouldBeReplacedWith(item,
/*mightBeInProgress=*/index != playQueue.getIndex());
}
/**
* Checks if the current playing index contains an expired {@link ManagedMediaSource}.
* If so, the expired source is replaced by a {@link PlaceholderMediaSource} and
* {@link #loadImmediate()} is called to reload the current item.
* <br><br>
* If not, then the media source at the current index is ready for playback, and
* {@link #maybeSynchronizePlayer()} is called.
* <br><br>
* Under both cases, {@link #maybeSync()} will be called to ensure the listener
* is up-to-date.
* */
private void maybeRenewCurrentIndex() {
final int currentIndex = playQueue.getIndex();
final ManagedMediaSource currentSource = playlist.get(currentIndex);
if (currentSource == null) return;
final PlayQueueItem currentItem = playQueue.getItem();
if (!currentSource.shouldBeReplacedWith(currentItem, /*canInterruptOnRenew=*/true)) {
maybeSynchronizePlayer();
return;
}
if (DEBUG) Log.d(TAG, "MediaSource - Reloading currently playing, " +
"index=[" + currentIndex + "], item=[" + currentItem.getTitle() + "]");
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();
}
}
/*//////////////////////////////////////////////////////////////////////////
// Media Source List Manipulation
// MediaSource Playlist Helpers
//////////////////////////////////////////////////////////////////////////*/
/**
* Inserts a source into {@link DynamicConcatenatingMediaSource} with position
* in respect to the play queue.
*
* If the play queue index already exists, then the insert is ignored.
* */
private void insert(final int queueIndex, final DeferredMediaSource source) {
if (sources == null) return;
if (queueIndex < 0 || queueIndex < sources.getSize()) return;
private void resetSources() {
if (DEBUG) Log.d(TAG, "resetSources() called.");
sources.addMediaSource(queueIndex, source);
playlist.dispose();
playlist = new ManagedMediaSourcePlaylist();
}
/**
* Removes a source from {@link DynamicConcatenatingMediaSource} with the given play queue index.
*
* If the play queue index does not exist, the removal is ignored.
* */
private void remove(final int queueIndex) {
if (sources == null) return;
if (queueIndex < 0 || queueIndex > sources.getSize()) return;
sources.removeMediaSource(queueIndex);
private void populateSources() {
if (DEBUG) Log.d(TAG, "populateSources() called.");
while (playlist.size() < playQueue.size()) {
playlist.expand();
}
}
private void move(final int source, final int target) {
if (sources == null) return;
if (source < 0 || target < 0) return;
if (source >= sources.getSize() || target >= sources.getSize()) return;
/*//////////////////////////////////////////////////////////////////////////
// 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;
sources.moveMediaSource(source, target);
// 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));
// 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);
}
private static class ItemsToLoad {
@NonNull final private PlayQueueItem center;
@NonNull final private Collection<PlayQueueItem> neighbors;
ItemsToLoad(@NonNull final PlayQueueItem center,
@NonNull final Collection<PlayQueueItem> neighbors) {
this.center = center;
this.neighbors = neighbors;
}
}
}

View File

@@ -11,6 +11,16 @@ import org.schabi.newpipe.playlist.PlayQueueItem;
import java.util.List;
public interface PlaybackListener {
/**
* 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 isApproachingPlaybackEdge(final long timeToEndMillis);
/**
* Called when the stream at the current queue index is not ready yet.
* Signals to the listener to block the player from playing anything and notify the source
@@ -18,7 +28,7 @@ public interface PlaybackListener {
*
* May be called at any time.
* */
void block();
void onPlaybackBlock();
/**
* Called when the stream at the current queue index is ready.
@@ -26,18 +36,16 @@ public interface PlaybackListener {
*
* May be called only when the player is blocked.
* */
void unblock(final MediaSource mediaSource);
void onPlaybackUnblock(final MediaSource mediaSource);
/**
* Called when the queue index is refreshed.
* Signals to the listener to synchronize the player's window to the manager's
* window.
*
* Occurs once only per play queue item change.
*
* May be called only after unblock is called.
* May be called anytime at any amount once unblock is called.
* */
void sync(@NonNull final PlayQueueItem item, @Nullable final StreamInfo info);
void onPlaybackSynchronize(@NonNull final PlayQueueItem item, @Nullable final StreamInfo info);
/**
* Requests the listener to resolve a stream info into a media source
@@ -55,5 +63,5 @@ public interface PlaybackListener {
*
* May be called at any time.
* */
void shutdown();
void onPlaybackShutdown();
}

View File

@@ -26,13 +26,13 @@ abstract class AbstractInfoPlayQueue<T extends ListInfo, U extends InfoItem> ext
transient Disposable fetchReactor;
AbstractInfoPlayQueue(final U item) {
this(item.getServiceId(), item.getUrl(), null, Collections.<InfoItem>emptyList(), 0);
this(item.getServiceId(), item.getUrl(), null, Collections.<StreamInfoItem>emptyList(), 0);
}
AbstractInfoPlayQueue(final int serviceId,
final String url,
final String nextPageUrl,
final List<InfoItem> streams,
final List<StreamInfoItem> streams,
final int index) {
super(index, extractListItems(streams));
@@ -65,10 +65,10 @@ abstract class AbstractInfoPlayQueue<T extends ListInfo, U extends InfoItem> ext
@Override
public void onSuccess(@NonNull T result) {
isInitial = false;
if (!result.has_more_streams) isComplete = true;
nextUrl = result.next_streams_url;
if (!result.hasNextPage()) isComplete = true;
nextUrl = result.getNextPageUrl();
append(extractListItems(result.related_streams));
append(extractListItems(result.getRelatedItems()));
fetchReactor.dispose();
fetchReactor = null;
@@ -83,8 +83,8 @@ abstract class AbstractInfoPlayQueue<T extends ListInfo, U extends InfoItem> ext
};
}
SingleObserver<ListExtractor.NextItemsResult> getNextItemsObserver() {
return new SingleObserver<ListExtractor.NextItemsResult>() {
SingleObserver<ListExtractor.InfoItemsPage> getNextPageObserver() {
return new SingleObserver<ListExtractor.InfoItemsPage>() {
@Override
public void onSubscribe(@NonNull Disposable d) {
if (isComplete || isInitial || (fetchReactor != null && !fetchReactor.isDisposed())) {
@@ -95,11 +95,11 @@ abstract class AbstractInfoPlayQueue<T extends ListInfo, U extends InfoItem> ext
}
@Override
public void onSuccess(@NonNull ListExtractor.NextItemsResult result) {
if (!result.hasMoreStreams()) isComplete = true;
nextUrl = result.nextItemsUrl;
public void onSuccess(@NonNull ListExtractor.InfoItemsPage result) {
if (!result.hasNextPage()) isComplete = true;
nextUrl = result.getNextPageUrl();
append(extractListItems(result.nextItemsList));
append(extractListItems(result.getItems()));
fetchReactor.dispose();
fetchReactor = null;
@@ -118,9 +118,10 @@ abstract class AbstractInfoPlayQueue<T extends ListInfo, U extends InfoItem> ext
public void dispose() {
super.dispose();
if (fetchReactor != null) fetchReactor.dispose();
fetchReactor = null;
}
private static List<PlayQueueItem> extractListItems(final List<InfoItem> infos) {
private static List<PlayQueueItem> extractListItems(final List<StreamInfoItem> infos) {
List<PlayQueueItem> result = new ArrayList<>();
for (final InfoItem stream : infos) {
if (stream instanceof StreamInfoItem) {

View File

@@ -3,6 +3,7 @@ package org.schabi.newpipe.playlist;
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;
import org.schabi.newpipe.util.ExtractorHelper;
import java.util.List;
@@ -16,13 +17,13 @@ public final class ChannelPlayQueue extends AbstractInfoPlayQueue<ChannelInfo, C
}
public ChannelPlayQueue(final ChannelInfo info) {
this(info.getServiceId(), info.getUrl(), info.getNextStreamsUrl(), info.getRelatedStreams(), 0);
this(info.getServiceId(), info.getUrl(), info.getNextPageUrl(), info.getRelatedItems(), 0);
}
public ChannelPlayQueue(final int serviceId,
final String url,
final String nextPageUrl,
final List<InfoItem> streams,
final List<StreamInfoItem> streams,
final int index) {
super(serviceId, url, nextPageUrl, streams, index);
}
@@ -43,7 +44,7 @@ public final class ChannelPlayQueue extends AbstractInfoPlayQueue<ChannelInfo, C
ExtractorHelper.getMoreChannelItems(this.serviceId, this.baseUrl, this.nextUrl)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(getNextItemsObserver());
.subscribe(getNextPageObserver());
}
}
}

View File

@@ -1,6 +1,7 @@
package org.schabi.newpipe.playlist;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.util.Log;
import org.reactivestreams.Subscriber;
@@ -44,7 +45,7 @@ public abstract class PlayQueue implements Serializable {
private ArrayList<PlayQueueItem> backup;
private ArrayList<PlayQueueItem> streams;
private final AtomicInteger queueIndex;
@NonNull private final AtomicInteger queueIndex;
private transient BehaviorSubject<PlayQueueEvent> eventBroadcast;
private transient Flowable<PlayQueueEvent> broadcastReceiver;
@@ -83,6 +84,7 @@ public abstract class PlayQueue implements Serializable {
if (eventBroadcast != null) eventBroadcast.onComplete();
if (reportingReactor != null) reportingReactor.cancel();
eventBroadcast = null;
broadcastReceiver = null;
reportingReactor = null;
}
@@ -131,7 +133,7 @@ public abstract class PlayQueue implements Serializable {
* Returns the index of the given item using referential equality.
* May be null despite play queue contains identical item.
* */
public int indexOf(final PlayQueueItem item) {
public int indexOf(@NonNull final PlayQueueItem item) {
// referential equality, can't think of a better way to do this
// todo: better than this
return streams.indexOf(item);
@@ -170,7 +172,7 @@ public abstract class PlayQueue implements Serializable {
* Returns the play queue's update broadcast.
* May be null if the play queue message bus is not initialized.
* */
@NonNull
@Nullable
public Flowable<PlayQueueEvent> getBroadcastReceiver() {
return broadcastReceiver;
}
@@ -211,7 +213,7 @@ public abstract class PlayQueue implements Serializable {
*
* @see #append(List items)
* */
public synchronized void append(final PlayQueueItem... items) {
public synchronized void append(@NonNull final PlayQueueItem... items) {
append(Arrays.asList(items));
}
@@ -223,7 +225,7 @@ public abstract class PlayQueue implements Serializable {
*
* Will emit a {@link AppendEvent} on any given context.
* */
public synchronized void append(final List<PlayQueueItem> items) {
public synchronized void append(@NonNull final List<PlayQueueItem> items) {
List<PlayQueueItem> itemList = new ArrayList<>(items);
if (isShuffled()) {
@@ -349,6 +351,7 @@ public abstract class PlayQueue implements Serializable {
if (backup == null) {
backup = new ArrayList<>(streams);
}
final int originIndex = getIndex();
final PlayQueueItem current = getItem();
Collections.shuffle(streams);
@@ -358,7 +361,7 @@ public abstract class PlayQueue implements Serializable {
}
queueIndex.set(0);
broadcast(new ReorderEvent());
broadcast(new ReorderEvent(originIndex, queueIndex.get()));
}
/**
@@ -371,6 +374,7 @@ public abstract class PlayQueue implements Serializable {
* */
public synchronized void unshuffle() {
if (backup == null) return;
final int originIndex = getIndex();
final PlayQueueItem current = getItem();
streams.clear();
@@ -384,14 +388,14 @@ public abstract class PlayQueue implements Serializable {
queueIndex.set(0);
}
broadcast(new ReorderEvent());
broadcast(new ReorderEvent(originIndex, queueIndex.get()));
}
/*//////////////////////////////////////////////////////////////////////////
// Rx Broadcast
//////////////////////////////////////////////////////////////////////////*/
private void broadcast(final PlayQueueEvent event) {
private void broadcast(@NonNull final PlayQueueEvent event) {
if (eventBroadcast != null) {
eventBroadcast.onNext(event);
}

View File

@@ -63,22 +63,18 @@ public class PlayQueueAdapter extends RecyclerView.Adapter<RecyclerView.ViewHold
}
public PlayQueueAdapter(final Context context, final PlayQueue playQueue) {
if (playQueue.getBroadcastReceiver() == null) {
throw new IllegalStateException("Play Queue has not been initialized.");
}
this.playQueueItemBuilder = new PlayQueueItemBuilder(context);
this.playQueue = playQueue;
startReactor();
playQueue.getBroadcastReceiver().toObservable().subscribe(getReactor());
}
public void setSelectedListener(final PlayQueueItemBuilder.OnSelectedListener listener) {
playQueueItemBuilder.setOnSelectedListener(listener);
}
public void unsetSelectedListener() {
playQueueItemBuilder.setOnSelectedListener(null);
}
private void startReactor() {
final Observer<PlayQueueEvent> observer = new Observer<PlayQueueEvent>() {
private Observer<PlayQueueEvent> getReactor() {
return new Observer<PlayQueueEvent>() {
@Override
public void onSubscribe(@NonNull Disposable d) {
if (playQueueReactor != null) playQueueReactor.dispose();
@@ -99,7 +95,6 @@ public class PlayQueueAdapter extends RecyclerView.Adapter<RecyclerView.ViewHold
}
};
playQueue.getBroadcastReceiver().toObservable().subscribe(observer);
}
private void onPlayQueueChanged(final PlayQueueEvent message) {
@@ -146,6 +141,14 @@ public class PlayQueueAdapter extends RecyclerView.Adapter<RecyclerView.ViewHold
playQueueReactor = null;
}
public void setSelectedListener(final PlayQueueItemBuilder.OnSelectedListener listener) {
playQueueItemBuilder.setOnSelectedListener(listener);
}
public void unsetSelectedListener() {
playQueueItemBuilder.setOnSelectedListener(null);
}
public void setFooter(View footer) {
this.footer = footer;
notifyItemChanged(playQueue.size());

View File

@@ -11,30 +11,29 @@ import org.schabi.newpipe.util.ExtractorHelper;
import java.io.Serializable;
import io.reactivex.Single;
import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.functions.Consumer;
import io.reactivex.schedulers.Schedulers;
public class PlayQueueItem implements Serializable {
final public static long RECOVERY_UNSET = Long.MIN_VALUE;
public final static long RECOVERY_UNSET = Long.MIN_VALUE;
private final static String EMPTY_STRING = "";
final private String title;
final private String url;
@NonNull final private String title;
@NonNull final private String url;
final private int serviceId;
final private long duration;
final private String thumbnailUrl;
final private String uploader;
final private StreamType streamType;
@NonNull final private String thumbnailUrl;
@NonNull final private String uploader;
@NonNull final private StreamType streamType;
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) {
@@ -42,15 +41,16 @@ public class PlayQueueItem implements Serializable {
item.getThumbnailUrl(), item.getUploaderName(), item.getStreamType());
}
private PlayQueueItem(final String name, final String url, final int serviceId,
final long duration, final String thumbnailUrl, final String uploader,
final StreamType streamType) {
this.title = name;
this.url = url;
private PlayQueueItem(@Nullable final String name, @Nullable final String url,
final int serviceId, final long duration,
@Nullable final String thumbnailUrl, @Nullable final String uploader,
@NonNull final StreamType streamType) {
this.title = name != null ? name : EMPTY_STRING;
this.url = url != null ? url : EMPTY_STRING;
this.serviceId = serviceId;
this.duration = duration;
this.thumbnailUrl = thumbnailUrl;
this.uploader = uploader;
this.thumbnailUrl = thumbnailUrl != null ? thumbnailUrl : EMPTY_STRING;
this.uploader = uploader != null ? uploader : EMPTY_STRING;
this.streamType = streamType;
this.recoveryPosition = RECOVERY_UNSET;
@@ -84,6 +84,7 @@ public class PlayQueueItem implements Serializable {
return uploader;
}
@NonNull
public StreamType getStreamType() {
return streamType;
}
@@ -99,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);

View File

@@ -1,28 +1,22 @@
package org.schabi.newpipe.playlist;
import android.content.Context;
import android.graphics.Bitmap;
import android.text.TextUtils;
import android.view.MotionEvent;
import android.view.View;
import com.nostra13.universalimageloader.core.DisplayImageOptions;
import com.nostra13.universalimageloader.core.ImageLoader;
import com.nostra13.universalimageloader.core.assist.ImageScaleType;
import com.nostra13.universalimageloader.core.process.BitmapProcessor;
import org.schabi.newpipe.R;
import org.schabi.newpipe.extractor.NewPipe;
import org.schabi.newpipe.util.ImageDisplayConstants;
import org.schabi.newpipe.util.Localization;
public class PlayQueueItemBuilder {
private static final String TAG = PlayQueueItemBuilder.class.toString();
private final int thumbnailWidthPx;
private final int thumbnailHeightPx;
private final DisplayImageOptions imageOptions;
public interface OnSelectedListener {
void selected(PlayQueueItem item, View view);
void held(PlayQueueItem item, View view);
@@ -31,11 +25,7 @@ public class PlayQueueItemBuilder {
private OnSelectedListener onItemClickListener;
public PlayQueueItemBuilder(final Context context) {
thumbnailWidthPx = context.getResources().getDimensionPixelSize(R.dimen.play_queue_thumbnail_width);
thumbnailHeightPx = context.getResources().getDimensionPixelSize(R.dimen.play_queue_thumbnail_height);
imageOptions = buildImageOptions(thumbnailWidthPx, thumbnailHeightPx);
}
public PlayQueueItemBuilder(final Context context) {}
public void setOnSelectedListener(OnSelectedListener listener) {
this.onItemClickListener = listener;
@@ -43,7 +33,8 @@ public class PlayQueueItemBuilder {
public void buildStreamInfoItem(final PlayQueueItemHolder holder, final PlayQueueItem item) {
if (!TextUtils.isEmpty(item.getTitle())) holder.itemVideoTitleView.setText(item.getTitle());
if (!TextUtils.isEmpty(item.getUploader())) holder.itemAdditionalDetailsView.setText(item.getUploader());
holder.itemAdditionalDetailsView.setText(Localization.concatenateStrings(item.getUploader(),
NewPipe.getNameOfService(item.getServiceId())));
if (item.getDuration() > 0) {
holder.itemDurationView.setText(Localization.getDurationString(item.getDuration()));
@@ -51,7 +42,8 @@ public class PlayQueueItemBuilder {
holder.itemDurationView.setVisibility(View.GONE);
}
ImageLoader.getInstance().displayImage(item.getThumbnailUrl(), holder.itemThumbnailView, imageOptions);
ImageLoader.getInstance().displayImage(item.getThumbnailUrl(), holder.itemThumbnailView,
ImageDisplayConstants.DISPLAY_THUMBNAIL_OPTIONS);
holder.itemRoot.setOnClickListener(view -> {
if (onItemClickListener != null) {
@@ -81,23 +73,4 @@ public class PlayQueueItemBuilder {
return false;
};
}
private DisplayImageOptions buildImageOptions(final int widthPx, final int heightPx) {
final BitmapProcessor bitmapProcessor = bitmap -> {
final Bitmap resizedBitmap = Bitmap.createScaledBitmap(bitmap, widthPx, heightPx, false);
bitmap.recycle();
return resizedBitmap;
};
return new DisplayImageOptions.Builder()
.showImageOnFail(R.drawable.dummy_thumbnail)
.showImageForEmptyUri(R.drawable.dummy_thumbnail)
.showImageOnLoading(R.drawable.dummy_thumbnail)
.bitmapConfig(Bitmap.Config.RGB_565) // Users won't be able to see much anyways
.preProcessor(bitmapProcessor)
.imageScaleType(ImageScaleType.EXACTLY)
.cacheInMemory(true)
.cacheOnDisk(true)
.build();
}
}

View File

@@ -0,0 +1,52 @@
package org.schabi.newpipe.playlist;
import android.support.v7.widget.RecyclerView;
import android.support.v7.widget.helper.ItemTouchHelper;
public abstract class PlayQueueItemTouchCallback extends ItemTouchHelper.SimpleCallback {
private static final int MINIMUM_INITIAL_DRAG_VELOCITY = 10;
private static final int MAXIMUM_INITIAL_DRAG_VELOCITY = 25;
public PlayQueueItemTouchCallback() {
super(ItemTouchHelper.UP | ItemTouchHelper.DOWN, 0);
}
public abstract void onMove(final int sourceIndex, final int targetIndex);
@Override
public int interpolateOutOfBoundsScroll(RecyclerView recyclerView, int viewSize,
int viewSizeOutOfBounds, int totalSize,
long msSinceStartScroll) {
final int standardSpeed = super.interpolateOutOfBoundsScroll(recyclerView, viewSize,
viewSizeOutOfBounds, totalSize, msSinceStartScroll);
final int clampedAbsVelocity = Math.max(MINIMUM_INITIAL_DRAG_VELOCITY,
Math.min(Math.abs(standardSpeed), MAXIMUM_INITIAL_DRAG_VELOCITY));
return clampedAbsVelocity * (int) Math.signum(viewSizeOutOfBounds);
}
@Override
public boolean onMove(RecyclerView recyclerView, RecyclerView.ViewHolder source,
RecyclerView.ViewHolder target) {
if (source.getItemViewType() != target.getItemViewType()) {
return false;
}
final int sourceIndex = source.getLayoutPosition();
final int targetIndex = target.getLayoutPosition();
onMove(sourceIndex, targetIndex);
return true;
}
@Override
public boolean isLongPressDragEnabled() {
return false;
}
@Override
public boolean isItemViewSwipeEnabled() {
return false;
}
@Override
public void onSwiped(RecyclerView.ViewHolder viewHolder, int swipeDir) {}
}

View File

@@ -3,6 +3,7 @@ package org.schabi.newpipe.playlist;
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;
import org.schabi.newpipe.util.ExtractorHelper;
import java.util.List;
@@ -16,13 +17,13 @@ public final class PlaylistPlayQueue extends AbstractInfoPlayQueue<PlaylistInfo,
}
public PlaylistPlayQueue(final PlaylistInfo info) {
this(info.getServiceId(), info.getUrl(), info.getNextStreamsUrl(), info.getRelatedStreams(), 0);
this(info.getServiceId(), info.getUrl(), info.getNextPageUrl(), info.getRelatedItems(), 0);
}
public PlaylistPlayQueue(final int serviceId,
final String url,
final String nextPageUrl,
final List<InfoItem> streams,
final List<StreamInfoItem> streams,
final int index) {
super(serviceId, url, nextPageUrl, streams, index);
}
@@ -43,7 +44,7 @@ public final class PlaylistPlayQueue extends AbstractInfoPlayQueue<PlaylistInfo,
ExtractorHelper.getMorePlaylistItems(this.serviceId, this.baseUrl, this.nextUrl)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(getNextItemsObserver());
.subscribe(getNextPageObserver());
}
}
}

View File

@@ -1,12 +1,24 @@
package org.schabi.newpipe.playlist.events;
public class ReorderEvent implements PlayQueueEvent {
private final int fromSelectedIndex;
private final int toSelectedIndex;
@Override
public PlayQueueEventType type() {
return PlayQueueEventType.REORDER;
}
public ReorderEvent() {
public ReorderEvent(final int fromSelectedIndex, final int toSelectedIndex) {
this.fromSelectedIndex = fromSelectedIndex;
this.toSelectedIndex = toSelectedIndex;
}
public int getFromSelectedIndex() {
return fromSelectedIndex;
}
public int getToSelectedIndex() {
return toSelectedIndex;
}
}

View File

@@ -77,12 +77,10 @@ public class ErrorActivity extends AppCompatActivity {
public static final String ERROR_EMAIL_ADDRESS = "crashreport@newpipe.schabi.org";
public static final String ERROR_EMAIL_SUBJECT = "Exception in NewPipe " + BuildConfig.VERSION_NAME;
Thread globIpRangeThread;
private String[] errorList;
private ErrorInfo errorInfo;
private Class returnActivity;
private String currentTimeStamp;
private String globIpRange;
// views
private TextView errorView;
private EditText userCommentBox;
@@ -91,7 +89,8 @@ public class ErrorActivity extends AppCompatActivity {
private TextView errorMessageView;
public static void reportUiError(final AppCompatActivity activity, final Throwable el) {
reportError(activity, el, activity.getClass(), null, ErrorInfo.make(UserAction.UI_ERROR, "none", "", R.string.app_ui_crash));
reportError(activity, el, activity.getClass(), null,
ErrorInfo.make(UserAction.UI_ERROR, "none", "", R.string.app_ui_crash));
}
public static void reportError(final Context context, final List<Throwable> el,
@@ -99,12 +98,8 @@ public class ErrorActivity extends AppCompatActivity {
if (rootView != null) {
Snackbar.make(rootView, R.string.error_snackbar_message, 15 * 1000)
.setActionTextColor(Color.YELLOW)
.setAction(R.string.error_snackbar_action, new View.OnClickListener() {
@Override
public void onClick(View v) {
startErrorActivity(returnActivity, context, errorInfo, el);
}
}).show();
.setAction(R.string.error_snackbar_action, v ->
startErrorActivity(returnActivity, context, errorInfo, el)).show();
} else {
startErrorActivity(returnActivity, context, errorInfo, el);
}
@@ -222,10 +217,6 @@ public class ErrorActivity extends AppCompatActivity {
startActivity(Intent.createChooser(i, "Send Email"));
});
reportButton.setEnabled(false);
globIpRangeThread = new Thread(new IpRangeRequester());
globIpRangeThread.start();
// normal bugreport
buildInfo(errorInfo);
@@ -342,8 +333,7 @@ public class ErrorActivity extends AppCompatActivity {
.put("package", getPackageName())
.put("version", BuildConfig.VERSION_NAME)
.put("os", getOsString())
.put("time", currentTimeStamp)
.put("ip_range", globIpRange);
.put("time", currentTimeStamp);
JSONArray exceptionArray = new JSONArray();
if (errorList != null) {
@@ -454,41 +444,4 @@ public class ErrorActivity extends AppCompatActivity {
dest.writeInt(this.message);
}
}
private class IpRangeRequester implements Runnable {
Handler h = new Handler();
public void run() {
String ipRange = "none";
try {
Downloader dl = Downloader.getInstance();
String ip = dl.download("https://ipv4.icanhazip.com");
ipRange = Parser.matchGroup1("([0-9]*\\.[0-9]*\\.)[0-9]*\\.[0-9]*", ip)
+ "0.0";
} catch (Throwable e) {
Log.w(TAG, "Error while error: could not get iprange", e);
} finally {
h.post(new IpRangeReturnRunnable(ipRange));
}
}
}
private class IpRangeReturnRunnable implements Runnable {
String ipRange;
public IpRangeReturnRunnable(String ipRange) {
this.ipRange = ipRange;
}
public void run() {
globIpRange = ipRange;
if (infoView != null) {
String text = infoView.getText().toString();
text += "\n" + globIpRange;
infoView.setText(text);
reportButton.setEnabled(true);
}
}
}
}

View File

@@ -13,7 +13,8 @@ public enum UserAction {
GET_SUGGESTIONS("get suggestions"),
REQUESTED_STREAM("requested stream"),
REQUESTED_CHANNEL("requested channel"),
REQUESTED_PLAYLIST("requested playlist");
REQUESTED_PLAYLIST("requested playlist"),
REQUESTED_KIOSK("requested kiosk");
private final String message;

View File

@@ -1,17 +1,25 @@
package org.schabi.newpipe.settings;
import android.content.Intent;
import android.os.Build;
import android.os.Bundle;
import android.provider.Settings;
import android.support.annotation.Nullable;
import android.support.v7.preference.Preference;
import android.util.Log;
import org.schabi.newpipe.R;
import org.schabi.newpipe.util.Constants;
public class AppearanceSettingsFragment extends BasePreferenceFragment {
private final static boolean CAPTIONING_SETTINGS_ACCESSIBLE =
Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT;
/**
* Theme that was applied when the settings was opened (or recreated after a theme change)
*/
private String startThemeKey;
private String captionSettingsKey;
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
@@ -19,6 +27,12 @@ public class AppearanceSettingsFragment extends BasePreferenceFragment {
String themeKey = getString(R.string.theme_key);
startThemeKey = defaultPreferences.getString(themeKey, getString(R.string.default_theme_value));
findPreference(themeKey).setOnPreferenceChangeListener(themePreferenceChange);
captionSettingsKey = getString(R.string.caption_settings_key);
if (!CAPTIONING_SETTINGS_ACCESSIBLE) {
final Preference captionSettings = findPreference(captionSettingsKey);
getPreferenceScreen().removePreference(captionSettings);
}
}
@Override
@@ -26,13 +40,23 @@ public class AppearanceSettingsFragment extends BasePreferenceFragment {
addPreferencesFromResource(R.xml.appearance_settings);
}
@Override
public boolean onPreferenceTreeClick(Preference preference) {
if (preference.getKey().equals(captionSettingsKey) && CAPTIONING_SETTINGS_ACCESSIBLE) {
startActivity(new Intent(Settings.ACTION_CAPTIONING_SETTINGS));
}
return super.onPreferenceTreeClick(preference);
}
private Preference.OnPreferenceChangeListener themePreferenceChange = new Preference.OnPreferenceChangeListener() {
@Override
public boolean onPreferenceChange(Preference preference, Object newValue) {
defaultPreferences.edit().putBoolean(Constants.KEY_THEME_CHANGE, true).apply();
defaultPreferences.edit().putString(getString(R.string.theme_key), newValue.toString()).apply();
if (!newValue.equals(startThemeKey)) { // If it's not the current theme
if (!newValue.equals(startThemeKey) && getActivity() != null) {
// If it's not the current theme
getActivity().recreate();
}

View File

@@ -5,11 +5,16 @@ import android.app.AlertDialog;
import android.content.DialogInterface;
import android.content.Intent;
import android.os.Bundle;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.v7.preference.ListPreference;
import android.support.v7.preference.Preference;
import android.util.Log;
import android.widget.Toast;
import com.nononsenseapps.filepicker.Utils;
import com.nostra13.universalimageloader.core.ImageLoader;
import org.schabi.newpipe.R;
import org.schabi.newpipe.extractor.NewPipe;
import org.schabi.newpipe.extractor.StreamingService;
@@ -34,8 +39,6 @@ import java.util.zip.ZipFile;
import java.util.zip.ZipInputStream;
import java.util.zip.ZipOutputStream;
import javax.annotation.Nonnull;
public class ContentSettingsFragment extends BasePreferenceFragment {
private static final int REQUEST_IMPORT_PATH = 8945;
@@ -46,6 +49,29 @@ public class ContentSettingsFragment extends BasePreferenceFragment {
private File newpipe_db;
private File newpipe_db_journal;
private String thumbnailLoadToggleKey;
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
thumbnailLoadToggleKey = getString(R.string.download_thumbnail_key);
}
@Override
public boolean onPreferenceTreeClick(Preference preference) {
if (preference.getKey().equals(thumbnailLoadToggleKey)) {
final ImageLoader imageLoader = ImageLoader.getInstance();
imageLoader.stop();
imageLoader.clearDiskCache();
imageLoader.clearMemoryCache();
imageLoader.resume();
Toast.makeText(preference.getContext(), R.string.thumbnail_cache_wipe_complete_notice,
Toast.LENGTH_SHORT).show();
}
return super.onPreferenceTreeClick(preference);
}
@Override
public void onCreatePreferences(Bundle savedInstanceState, String rootKey) {
@@ -140,15 +166,15 @@ public class ContentSettingsFragment extends BasePreferenceFragment {
}
@Override
public void onActivityResult(int requestCode, int resultCode, @Nonnull Intent data) {
public void onActivityResult(int requestCode, int resultCode, @NonNull Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if (DEBUG) {
Log.d(TAG, "onActivityResult() called with: requestCode = [" + requestCode + "], resultCode = [" + resultCode + "], data = [" + data + "]");
}
if ((requestCode == REQUEST_IMPORT_PATH || requestCode == REQUEST_EXPORT_PATH)
&& resultCode == Activity.RESULT_OK) {
String path = data.getData().getPath();
&& resultCode == Activity.RESULT_OK && data.getData() != null) {
String path = Utils.getFileForUri(data.getData()).getAbsolutePath();
if (requestCode == REQUEST_EXPORT_PATH) {
SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMdd_HHmmss", Locale.US);
exportDatabase(path + "/NewPipeData-" + sdf.format(new Date()) + ".zip");

View File

@@ -7,6 +7,8 @@ import android.support.annotation.Nullable;
import android.support.v7.preference.Preference;
import android.util.Log;
import com.nononsenseapps.filepicker.Utils;
import org.schabi.newpipe.R;
import org.schabi.newpipe.util.FilePickerActivityHelper;
@@ -69,9 +71,11 @@ public class DownloadSettingsFragment extends BasePreferenceFragment {
Log.d(TAG, "onActivityResult() called with: requestCode = [" + requestCode + "], resultCode = [" + resultCode + "], data = [" + data + "]");
}
if ((requestCode == REQUEST_DOWNLOAD_PATH || requestCode == REQUEST_DOWNLOAD_AUDIO_PATH) && resultCode == Activity.RESULT_OK) {
if ((requestCode == REQUEST_DOWNLOAD_PATH || requestCode == REQUEST_DOWNLOAD_AUDIO_PATH)
&& resultCode == Activity.RESULT_OK && data.getData() != null) {
String key = getString(requestCode == REQUEST_DOWNLOAD_PATH ? R.string.download_path_key : R.string.download_path_audio_key);
String path = data.getData().getPath();
String path = Utils.getFileForUri(data.getData()).getAbsolutePath();
defaultPreferences.edit().putString(key, path).apply();
updatePreferencesSummary();
}

View File

@@ -1,12 +1,35 @@
package org.schabi.newpipe.settings;
import android.os.Bundle;
import android.support.annotation.Nullable;
import android.support.v7.preference.Preference;
import android.widget.Toast;
import org.schabi.newpipe.R;
import org.schabi.newpipe.util.InfoCache;
public class HistorySettingsFragment extends BasePreferenceFragment {
private String cacheWipeKey;
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
cacheWipeKey = getString(R.string.metadata_cache_wipe_key);
}
@Override
public void onCreatePreferences(Bundle savedInstanceState, String rootKey) {
addPreferencesFromResource(R.xml.history_settings);
}
@Override
public boolean onPreferenceTreeClick(Preference preference) {
if (preference.getKey().equals(cacheWipeKey)) {
InfoCache.getInstance().clearCache();
Toast.makeText(preference.getContext(), R.string.metadata_cache_wipe_complete_notice,
Toast.LENGTH_SHORT).show();
}
return super.onPreferenceTreeClick(preference);
}
}

Some files were not shown because too many files have changed in this diff Show More