1
0
mirror of https://github.com/TeamNewPipe/NewPipe synced 2026-01-15 05:58:02 +00:00

Compare commits

..

162 Commits

Author SHA1 Message Date
E T
f893edeb82 Translated using Weblate (Turkish)
Currently translated at 100.0% (205 of 205 strings)
2017-09-18 01:11:35 +02:00
Weblate
64e4adef29 Merge remote-tracking branch 'origin/master' 2017-09-16 10:36:43 +02:00
E T
c1fe03aab6 Translated using Weblate (Turkish)
Currently translated at 100.0% (205 of 205 strings)
2017-09-16 10:36:32 +02:00
Christian Schabesberger
4b0a071a35 Merge pull request #691 from TeamNewPipe/minify
enable minify
2017-09-13 21:07:55 +02:00
Christian Schabesberger
4ef69969a7 enable minify 2017-09-12 13:32:53 +02:00
Weblate
c5885d9ce4 Merge remote-tracking branch 'origin/master' 2017-09-11 23:15:20 +02:00
Allan Nordhøy
4c2d705311 Translated using Weblate (Norwegian Bokmål)
Currently translated at 100.0% (205 of 205 strings)
2017-09-11 23:15:16 +02:00
Mauricio Colli
939017ada9 Update extractor version 2017-09-11 11:21:01 -03:00
Matej U
322c5c05cf Translated using Weblate (Slovenian)
Currently translated at 100.0% (205 of 205 strings)
2017-09-11 11:29:26 +02:00
Jonas
cd174b8ce9 Translated using Weblate (French)
Currently translated at 100.0% (205 of 205 strings)
2017-09-11 10:12:00 +02:00
Oscar Hemelaar
f5999f28e9 Translated using Weblate (French)
Currently translated at 97.0% (199 of 205 strings)
2017-09-11 10:06:02 +02:00
Jonas
428a754bfc Translated using Weblate (French)
Currently translated at 96.5% (198 of 205 strings)
2017-09-11 10:05:38 +02:00
Weblate
9ba2a9d907 Merge remote-tracking branch 'origin/master' 2017-09-09 22:34:46 +02:00
Eduardo Caron
71a22348c9 Translated using Weblate (Portuguese (Brazil))
Currently translated at 100.0% (205 of 205 strings)
2017-09-09 22:34:45 +02:00
Tobias Groza
4ac0db1869 Translated using Weblate (French)
Currently translated at 80.9% (166 of 205 strings)
2017-09-09 22:34:36 +02:00
Nathan Follens
bca9035302 Translated using Weblate (Dutch)
Currently translated at 100.0% (205 of 205 strings)
2017-09-09 22:34:25 +02:00
Christian Schabesberger
33b316d72f update extractor to not throw channelextractor exception 2017-09-09 22:22:27 +02:00
Christian Schabesberger
6068043fd4 Fix pullrequest teamplate markdown issue 2017-09-09 19:04:30 +02:00
Weblate
2bf4032a6f Merge remote-tracking branch 'origin/master' 2017-09-09 00:46:59 +02:00
Enrico Monese
5e659e3f46 Translated using Weblate (Italian)
Currently translated at 97.0% (199 of 205 strings)
2017-09-09 00:46:56 +02:00
Mauricio Colli
23f70d6b8b Update extractor 2017-09-08 11:34:02 -03:00
Osoitz
ddcf2c5206 Translated using Weblate (Basque)
Currently translated at 100.0% (205 of 205 strings)
2017-09-08 15:44:27 +02:00
Tobias Groza
6b88349c77 Translated using Weblate (German)
Currently translated at 98.0% (201 of 205 strings)
2017-09-07 18:46:03 +02:00
Marian Hanzel
4eb2d3805d Translated using Weblate (Slovak)
Currently translated at 84.8% (174 of 205 strings)
2017-09-06 00:48:04 +02:00
Gaman Gabriel
6cdddc4e9f Translated using Weblate (Romanian)
Currently translated at 77.5% (159 of 205 strings)
2017-09-05 18:48:21 +02:00
Freddy Morán Jr
406d844722 Translated using Weblate (Spanish)
Currently translated at 100.0% (205 of 205 strings)
2017-09-05 14:47:09 +02:00
Mladen Pejaković
09ee0357c7 Translated using Weblate (Serbian)
Currently translated at 100.0% (205 of 205 strings)
2017-09-04 16:23:20 +02:00
Mauricio Colli
f603f63361 Another fix for Weblate 2017-09-04 09:31:53 -03:00
Mauricio Colli
720bb5d615 Fix strings for Weblate 2017-09-04 09:26:00 -03:00
Mauricio Colli
eebd76661d Translated using Weblate (Portuguese (Brazil))
Currently translated at 98.5% (203 of 206 strings)
2017-09-04 14:10:52 +02:00
Mauricio Colli
7885ae5d3f Merge pull request #669 from mauriciocolli/refactor-newpipe
Update extractor and refactored NewPipe
2017-09-04 08:50:21 -03:00
Christian Schabesberger
08257cc5dd Merge pull request #671 from TobiGr/readme-typos
Fix typos in README
2017-09-04 01:39:23 +02:00
TobiGr
27f8144bb8 fix typos in README 2017-09-04 00:39:33 +02:00
Mauricio Colli
146d4a8365 Update extractor and refactored NewPipe 2017-09-03 13:57:12 -03:00
Walter White
bddd9b3409 Translated using Weblate (French)
Currently translated at 83.6% (164 of 196 strings)
2017-09-03 06:04:15 +02:00
Mauricio Colli
0d4d83f3a6 Translated using Weblate (English)
Currently translated at 100.0% (196 of 196 strings)
2017-09-03 06:04:14 +02:00
Anton Shestakov
4bb0bb4ff0 Translated using Weblate (Russian)
Currently translated at 97.9% (192 of 196 strings)
2017-09-02 18:48:04 +02:00
Mauricio Colli
2d75968532 Translated using Weblate (English)
Currently translated at 100.0% (196 of 196 strings)
2017-09-02 00:45:25 +02:00
Nathan Follens
aa0e759168 Translated using Weblate (Dutch)
Currently translated at 100.0% (196 of 196 strings)
2017-09-01 23:47:19 +02:00
Eduardo Caron
cdae745c00 Translated using Weblate (Portuguese (Brazil))
Currently translated at 100.0% (196 of 196 strings)
2017-09-01 22:54:42 +02:00
Freddy Morán Jr
a8b6af1f03 Translated using Weblate (Spanish)
Currently translated at 100.0% (196 of 196 strings)
2017-09-01 06:58:52 +02:00
Mauricio Colli
c7106073b4 Translated using Weblate (German)
Currently translated at 90.8% (178 of 196 strings)
2017-09-01 00:17:13 +02:00
Mauricio Colli
d96139798f Translated using Weblate (English)
Currently translated at 100.0% (196 of 196 strings)
2017-09-01 00:17:13 +02:00
nautilusx
bd560644fe Translated using Weblate (German)
Currently translated at 97.9% (192 of 196 strings)
2017-08-31 22:09:52 +02:00
Tobias Groza
f6772c4138 Translated using Weblate (German)
Currently translated at 88.7% (174 of 196 strings)
2017-08-31 12:51:23 +02:00
Mauricio Colli
dd733640e5 Translated using Weblate (Portuguese (Brazil))
Currently translated at 86.7% (170 of 196 strings)
2017-08-31 05:20:12 +02:00
Allan Nordhøy
75a0c0a527 Translated using Weblate (Norwegian Bokmål)
Currently translated at 78.0% (153 of 196 strings)
2017-08-31 05:20:12 +02:00
Mauricio Colli
a5925875a7 Translated using Weblate (English)
Currently translated at 100.0% (196 of 196 strings)
2017-08-31 05:20:02 +02:00
Allan Nordhøy
84157f9247 Translated using Weblate (English)
Currently translated at 100.0% (196 of 196 strings)

Note that "Billion" is often "Milliard" in other languages.
2017-08-31 05:17:26 +02:00
Allan Nordhøy
fcf3ed7881 Translated using Weblate (English)
Currently translated at 100.0% (196 of 196 strings)
2017-08-31 03:45:01 +02:00
theVikac
dac3be69d6 Translated using Weblate (Croatian)
Currently translated at 100.0% (196 of 196 strings)
2017-08-30 09:15:40 +02:00
haseoxth
5118e5cceb Translated using Weblate (Chinese (Simplified))
Currently translated at 100.0% (196 of 196 strings)
2017-08-30 08:15:06 +02:00
Janusz May
7ca0d20dda Translated using Weblate (Polish)
Currently translated at 98.9% (194 of 196 strings)
2017-08-29 15:47:28 +02:00
Marian Hanzel
378222e484 Translated using Weblate (Slovak)
Currently translated at 100.0% (196 of 196 strings)
2017-08-29 08:41:45 +02:00
mueller-ma
812bec205c Translated using Weblate (German)
Currently translated at 98.4% (193 of 196 strings)
2017-08-28 12:46:04 +02:00
Matej U
2ee8803e33 Translated using Weblate (Slovenian)
Currently translated at 100.0% (196 of 196 strings)
2017-08-27 12:48:39 +02:00
wb9688
007616ca42 Translated using Weblate (Dutch)
Currently translated at 100.0% (196 of 196 strings)
2017-08-27 09:45:09 +02:00
Osoitz
f3b235e4a7 Translated using Weblate (Basque)
Currently translated at 95.9% (188 of 196 strings)
2017-08-26 15:44:19 +02:00
074d4be4ba Translated using Weblate (Dutch)
Currently translated at 100.0% (196 of 196 strings)
2017-08-26 09:34:35 +02:00
wb9688
03cd48257b Translated using Weblate (Dutch)
Currently translated at 100.0% (196 of 196 strings)
2017-08-26 09:33:21 +02:00
zmni
546db83de3 Translated using Weblate (Indonesian)
Currently translated at 95.4% (187 of 196 strings)
2017-08-25 18:46:09 +02:00
Matej U
cc11a39f7e Translated using Weblate (Slovenian)
Currently translated at 99.4% (195 of 196 strings)
2017-08-24 00:48:01 +02:00
Eduardo Caron
a83b365afd Translated using Weblate (Portuguese (Brazil))
Currently translated at 100.0% (196 of 196 strings)
2017-08-23 15:22:27 +02:00
Freddy Morán Jr
6a6cfac603 Translated using Weblate (Spanish)
Currently translated at 100.0% (196 of 196 strings)
2017-08-22 01:27:56 +02:00
Weblate
5270cedc56 Merge remote-tracking branch 'origin/master' 2017-08-20 21:45:05 +02:00
Michalis Nikolaidis
2ac833d2a6 Translated using Weblate (Greek)
Currently translated at 22.6% (44 of 194 strings)
2017-08-20 21:45:04 +02:00
Blader
f7be210b12 Translated using Weblate (Dutch)
Currently translated at 93.2% (181 of 194 strings)
2017-08-20 21:44:58 +02:00
Christian Schabesberger
24774d7921 Merge pull request #656 from karyogamy/exoplayer_update
Updated ExoPlayer to r2.5.1
2017-08-18 22:05:31 +02:00
John Zhen M
2b2e954b84 -Updated ExoPlayer to r2.5.1.
-Fixes some more deprecations due to Exoplayer and Android O notification updates.
2017-08-18 11:07:57 -07:00
Tonelico
85108be686 Android O Notification Building Fix (#655)
-Added simple notification channel for Android O.
-Fixes notification building failure for background and popup player on Android O.
-Reduce notification channel importance to low to avoid making noise on every notification update.
2017-08-18 09:05:31 -03:00
Oscar Hemelaar
9fbac943bb Translated using Weblate (French)
Currently translated at 98.4% (191 of 194 strings)
2017-08-18 00:45:31 +02:00
theVikac
d901eaa8d2 Translated using Weblate (Croatian)
Currently translated at 100.0% (194 of 194 strings)
2017-08-17 12:44:50 +02:00
mueller-ma
5fb0e8d007 Translated using Weblate (German)
Currently translated at 98.4% (191 of 194 strings)

In german there is no 's
2017-08-16 21:45:40 +02:00
theVikac
cd5c76cb76 Translated using Weblate (Croatian)
Currently translated at 100.0% (194 of 194 strings)
2017-08-16 10:56:32 +02:00
theVikac
b2a64f8bf3 Added translation using Weblate (Croatian) 2017-08-16 10:50:18 +02:00
mueller-ma
b91acc9de5 Translated using Weblate (German)
Currently translated at 96.9% (188 of 194 strings)
2017-08-15 19:40:37 +02:00
Weblate
07836c7ffa Merge remote-tracking branch 'origin/master' 2017-08-15 19:40:01 +02:00
Tobias Groza
8b30a0d7cc Translated using Weblate (German)
Currently translated at 96.9% (188 of 194 strings)
2017-08-15 19:39:58 +02:00
Mauricio Colli
13a3e2feb2 Merge pull request #654 from coffeemakr/feature-add-components
Add licenses for RxJava
2017-08-15 13:10:11 -03:00
Coffeemakr
f4dca71497 Add licenses for RxJava (and bindings) 2017-08-15 17:21:11 +02:00
Gian Maria Viglianti
e4c6e87338 Translated using Weblate (Italian)
Currently translated at 100.0% (194 of 194 strings)
2017-08-14 18:46:46 +02:00
Eduardo Caron
b2862520fd Translated using Weblate (Portuguese (Brazil))
Currently translated at 100.0% (194 of 194 strings)
2017-08-14 18:34:54 +02:00
Lucas Friederich
bb29f1ea95 Translated using Weblate (Portuguese (Brazil))
Currently translated at 96.3% (187 of 194 strings)
2017-08-14 18:32:58 +02:00
Freddy Morán Jr
69b92757f4 Translated using Weblate (Spanish)
Currently translated at 100.0% (194 of 194 strings)
2017-08-14 03:46:52 +02:00
Mladen Pejaković
62ae2652c7 Translated using Weblate (Serbian)
Currently translated at 95.3% (185 of 194 strings)
2017-08-14 00:46:35 +02:00
6f6633050b Translated using Weblate (Italian)
Currently translated at 100.0% (194 of 194 strings)
2017-08-13 17:22:20 +02:00
Gian Maria Viglianti
d37bfd6651 Translated using Weblate (Italian)
Currently translated at 100.0% (194 of 194 strings)
2017-08-13 17:21:56 +02:00
55a17a8a83 Translated using Weblate (Italian)
Currently translated at 100.0% (194 of 194 strings)
2017-08-13 17:21:04 +02:00
Gian Maria Viglianti
422b06b72d Translated using Weblate (Italian)
Currently translated at 100.0% (194 of 194 strings)
2017-08-13 17:17:58 +02:00
Freddy Morán Jr
873a1d1c3b Translated using Weblate (Spanish)
Currently translated at 100.0% (194 of 194 strings)
2017-08-13 03:06:27 +02:00
Weblate
44d06767b5 Merge remote-tracking branch 'origin/master' 2017-08-12 18:45:10 +02:00
Thomas Lavend'Homme
3f4379afbf Translated using Weblate (French)
Currently translated at 98.9% (181 of 183 strings)
2017-08-12 18:45:06 +02:00
Cyril Müller
c0515de6b7 Add search and watch history (#626)
Add search and watch history

* Make MainActicity a single task
* Remove some casting
* SearchFragment: start searching when created with query
* Handle settings change in onResume
* History: Log pop up and background playback
* History: Add swipe to remove functionallity
* Enable history by default
* Use stream item
* Store more information about the stream
* Integrate history database into AppDatabase
* Remove redundant casts
* Re-enable date converters
* History: Use Rx Java and run DB in background
 * Also make HistoryDAO extend BasicDAO
* History: RX-ify swipe to remove
* Sort history entries by creation date
* History: Set toolbar title
* Don't repeat history entries
  * Introduced setters so we can update entries in the database
  * If the latest entry has the same (main) values, just update it
2017-08-12 01:50:25 -03:00
09159ec245 Translated using Weblate (French)
Currently translated at 93.9% (172 of 183 strings)
2017-08-11 17:05:11 +02:00
Thomas Lavend'Homme
ab0bf80888 Translated using Weblate (French)
Currently translated at 93.4% (171 of 183 strings)
2017-08-11 17:04:40 +02:00
74b8eaed04 Translated using Weblate (French)
Currently translated at 86.3% (158 of 183 strings)
2017-08-11 16:05:34 +02:00
d64b5ccae2 Translated using Weblate (French)
Currently translated at 85.7% (157 of 183 strings)
2017-08-11 15:58:59 +02:00
3883212ca7 Translated using Weblate (French)
Currently translated at 85.2% (156 of 183 strings)
2017-08-11 15:56:20 +02:00
1cec41dba0 Translated using Weblate (French)
Currently translated at 84.6% (155 of 183 strings)
2017-08-11 15:56:01 +02:00
d00c0ff60c Translated using Weblate (French)
Currently translated at 84.1% (154 of 183 strings)
2017-08-11 15:55:42 +02:00
1d597c5f5b Translated using Weblate (French)
Currently translated at 83.6% (153 of 183 strings)
2017-08-11 15:54:41 +02:00
c004ef7d0b Translated using Weblate (French)
Currently translated at 83.0% (152 of 183 strings)
2017-08-11 15:54:22 +02:00
Freddy Morán Jr
1bfb977b28 Translated using Weblate (Spanish)
Currently translated at 100.0% (183 of 183 strings)
2017-08-10 19:18:55 +02:00
Osoitz
04138047f6 Translated using Weblate (Basque)
Currently translated at 100.0% (183 of 183 strings)
2017-08-10 14:03:01 +02:00
zmni
147e99a915 Translated using Weblate (Indonesian)
Currently translated at 97.2% (178 of 183 strings)
2017-08-09 15:45:45 +02:00
Matej U
6f7d162edc Translated using Weblate (Slovenian)
Currently translated at 97.8% (179 of 183 strings)
2017-08-09 12:46:52 +02:00
Weblate
910aee4b73 Merge remote-tracking branch 'origin/master' 2017-08-07 20:03:29 +02:00
mueller-ma
77e376751a Translated using Weblate (German)
Currently translated at 96.7% (177 of 183 strings)
2017-08-07 20:03:28 +02:00
Eduardo Caron
7e1264ac44 Translated using Weblate (Portuguese (Brazil))
Currently translated at 100.0% (183 of 183 strings)
2017-08-07 20:03:26 +02:00
Christian Schabesberger
8f3a4a5a04 moved on to version v0.10.0 2017-08-07 18:16:19 +02:00
2d2689362d Translated using Weblate (German)
Currently translated at 93.9% (172 of 183 strings)
2017-08-07 17:52:56 +02:00
Coffeemaker
913500a6de Translated using Weblate (German)
Currently translated at 93.9% (172 of 183 strings)
2017-08-07 17:51:43 +02:00
Weblate
81d7c54a55 Merge remote-tracking branch 'origin/master' 2017-08-07 17:50:22 +02:00
mueller-ma
4501d5a8fc Translated using Weblate (German)
Currently translated at 99.4% (172 of 173 strings)
2017-08-07 17:50:16 +02:00
Tonelico
becc90409f Added option to resume on audio focus regain. (#624) 2017-08-07 10:04:36 -03:00
Tonelico
10c4f7b465 Added basic channel subscription and feed pages (#620)
Added basic channel subscription and feed pages

- Room Persistence for sqlite support.
- RxJava2 for reactive async support.
- Stetho for database inspection support.
- Enabled Multidex for debug build.
2017-08-07 10:02:30 -03:00
Janusz May
cb5b0ff764 Translated using Weblate (Polish)
Currently translated at 98.8% (171 of 173 strings)
2017-08-04 18:46:15 +02:00
Wout B
277a817eaf Translated using Weblate (Dutch)
Currently translated at 100.0% (173 of 173 strings)
2017-08-04 17:07:20 +02:00
zmni
fbac6f6a57 Translated using Weblate (Indonesian)
Currently translated at 100.0% (173 of 173 strings)
2017-08-04 15:58:58 +02:00
monolifed
b1b0d6ceb6 Translated using Weblate (Turkish)
Currently translated at 100.0% (173 of 173 strings)
2017-08-03 21:38:20 +02:00
Eduardo Caron
07421542df Translated using Weblate (Portuguese (Brazil))
Currently translated at 100.0% (173 of 173 strings)
2017-08-03 14:19:56 +02:00
Weblate
74a8139ac9 Merge remote-tracking branch 'origin/master' 2017-08-03 14:16:37 +02:00
Osoitz
77eb76fefb Translated using Weblate (Basque)
Currently translated at 100.0% (167 of 167 strings)
2017-08-03 14:16:35 +02:00
Eduardo Caron
42a450c61f Translated using Weblate (Portuguese (Brazil))
Currently translated at 100.0% (167 of 167 strings)
2017-08-03 14:16:26 +02:00
TheAssassin
9e2ed10144 Fix bugs in CONTRIBUTING.md
Spelling and grammar bugs, a few additions here and there, make it sound like a team project instead of a one-man show.
2017-08-02 23:34:46 +02:00
Christian Schabesberger
27e5ce7f9b update sentry linkg 2017-08-02 23:19:34 +02:00
Mauricio Colli
f7b322da49 Fix audio focus bug 2017-08-02 14:14:45 -03:00
Mauricio Colli
16ad13c962 Update travis' android and build-tools version 2017-08-01 23:18:47 -03:00
Mauricio Colli
a8880bbc0a Merge pull request #642 from karyogamy/sdk_26_updrade
Upgrade target sdk and support library version to 26
2017-08-01 22:58:54 -03:00
Mauricio Colli
f020b88db3 Move maven repository declaration
- Remove redundant jcenter (already included in the "global application" build.gradle)
2017-08-01 22:56:04 -03:00
Mauricio Colli
a4f5c9e2a3 Merge pull request #636 from coffeemakr/feature-renaming-patterns
Add renaming filename preferences
2017-08-01 22:03:19 -03:00
Mauricio Colli
96d3841dba Fix issues #636 2017-08-01 21:56:51 -03:00
John Zhen M
39277d569f - Updated target, build tools and support libraries version to 26.
- Added dependency repositories jcenter and maven.google.com.
- Changed deprecated ActionBarActivity to AppCompatActivity.
2017-08-01 14:54:32 -07:00
Bruno Guerreiro
75b5bc03f7 Translated using Weblate (Portuguese)
Currently translated at 100.0% (167 of 167 strings)
2017-08-01 18:37:57 +02:00
Osoitz
01e17d9c2e Translated using Weblate (Basque)
Currently translated at 100.0% (167 of 167 strings)
2017-08-01 11:20:14 +02:00
Emilio Loi
8047ff96a8 Translated using Weblate (Italian)
Currently translated at 100.0% (167 of 167 strings)
2017-07-29 15:45:43 +02:00
Gian Maria Viglianti
0e737a2f3b Translated using Weblate (Italian)
Currently translated at 100.0% (167 of 167 strings)
2017-07-28 13:10:51 +02:00
Emilio Loi
854579d814 Translated using Weblate (Italian)
Currently translated at 100.0% (167 of 167 strings)
2017-07-28 13:10:42 +02:00
Emilio Loi
f3cbd99e45 Translated using Weblate (Italian)
Currently translated at 100.0% (167 of 167 strings)

"Cancellare" is "to clear" in writing context
2017-07-28 13:07:02 +02:00
Emilio Loi
c13ed3f347 Translated using Weblate (Italian)
Currently translated at 100.0% (167 of 167 strings)

There is enough space to more descriptive with this text
2017-07-28 13:05:33 +02:00
Emilio Loi
7409b58546 Translated using Weblate (Italian)
Currently translated at 99.4% (167 of 167 strings)
2017-07-28 13:00:17 +02:00
Emilio Loi
404ebb8810 Translated using Weblate (Italian)
Currently translated at 99.4% (166 of 167 strings)

I've inverted position of sentence "help is always welcome" from end to start, merging "The more is done the better it gets!" with the first part of the paragraph
2017-07-28 13:00:13 +02:00
Emilio Loi
b4a9a300c5 Translated using Weblate (Italian)
Currently translated at 97.6% (163 of 167 strings)
2017-07-28 12:58:37 +02:00
Fekrod
aa637e6a2b Translated using Weblate (Vietnamese)
Currently translated at 100.0% (167 of 167 strings)
2017-07-27 04:27:32 +02:00
Fekrod
85132e1e6e Added translation using Weblate (Vietnamese) 2017-07-27 02:59:08 +02:00
Eduardo Caron
f0aa5a4646 Translated using Weblate (Portuguese (Brazil))
Currently translated at 98.8% (165 of 167 strings)
2017-07-26 15:46:21 +02:00
Coffeemakr
b0479d0bd9 Add renaming filename patterns 2017-07-24 13:01:24 +02:00
riotism
e475b888aa Translated using Weblate (Chinese (Hong Kong))
Currently translated at 100.0% (167 of 167 strings)
2017-07-23 21:44:23 +02:00
Nigel
a8a3bbd9f5 Translated using Weblate (Chinese (Hong Kong))
Currently translated at 100.0% (167 of 167 strings)
2017-07-22 20:38:39 +02:00
riotism
f3cbd17598 Translated using Weblate (Chinese (Hong Kong))
Currently translated at 100.0% (167 of 167 strings)
2017-07-22 20:38:09 +02:00
riotism
3d7e4dc286 Translated using Weblate (Chinese (Hong Kong))
Currently translated at 100.0% (167 of 167 strings)
2017-07-22 20:35:53 +02:00
2e98729527 Translated using Weblate (German)
Currently translated at 100.0% (167 of 167 strings)
2017-07-20 12:45:26 +02:00
Coffeemaker
3ac838a7ec Translated using Weblate (German)
Currently translated at 99.4% (167 of 167 strings)
2017-07-19 11:42:47 +02:00
Weblate
a1bd5bfc00 Merge remote-tracking branch 'origin/master' 2017-07-19 11:42:41 +02:00
Matej U
b99571a797 Translated using Weblate (Slovenian)
Currently translated at 98.8% (165 of 167 strings)
2017-07-19 11:42:41 +02:00
43dec8914c Translated using Weblate (German)
Currently translated at 99.4% (166 of 167 strings)
2017-07-19 11:42:39 +02:00
Christian Schabesberger
5b5cb78595 Merge pull request #633 from coffeemakr/fix-time-links
Prevent "time links" from beeing clicked
2017-07-19 11:38:07 +02:00
Coffeemakr
cb8c919609 Prevent time links from beeing clicked 2017-07-19 10:26:26 +02:00
Anton Shestakov
be3810dfed Translated using Weblate (Russian)
Currently translated at 100.0% (167 of 167 strings)
2017-07-19 07:58:41 +02:00
Weblate
b961b59ad4 Merge remote-tracking branch 'origin/master' 2017-07-17 23:14:27 +02:00
Matej U
d2fbb86d8a Translated using Weblate (Slovenian)
Currently translated at 98.2% (164 of 167 strings)
2017-07-17 23:14:27 +02:00
Mauricio Colli
782c6211d0 Added translation using Weblate (Lithuanian) 2017-07-17 23:14:24 +02:00
267 changed files with 11736 additions and 6182 deletions

View File

@@ -1,40 +1,42 @@
NewPipe contribution guidelines
===============================
READ THIS GUIDELINES CAREFULLY BEFORE CONTRIBUTING.
PLEASE READ THESE GUIDELINES CAREFULLY BEFORE ANY CONTRIBUTION!
## Crash reporting
Do not report crashes in the GitHub issue tracker. NewPipe has an automated crash report system that will ask you to send a report if a crash occurs.
Do not report crashes in the GitHub issue tracker. NewPipe has an automated crash report system that will ask you to send a report via e-mail when a crash occurs. This contains all the data we need for debugging, and allows you to even add a comment to it. You'll see exactly what is sent, the system is 100% transparent.
## Issue reporting/feature request
## Issue reporting/feature requests
* Search the [existing issues](https://github.com/theScrabi/NewPipe/issues) first to make sure your issue/feature hasn't been reported/requested before
* Check if this issue/feature is already fixed/implemented in the repository
* If you are an android/java developer you are always welcome to fix/implement an issue/a feature yourself
* Use english
* 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
* 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.
## Bugfixing
* If you want to help NewPipe getting bug free, you can send me a mail to tnp@newpipe.schabi.org to let me know that you intent to help, and than register at our [sentry](https://support.schabi.org) setup.
## Bug Fixing
* If you want to help NewPipe to become free of bugs (this is our utopic goal for NewPipe), you can send us an email to tnp@newpipe.schabi.org to let me know that you intend to help. We'll send you further instructions. You may, on request, register at our [Sentry](https://sentry.schabi.org) instance (see section "Crash reporting" for more information.
## Translation
* NewPipe can be translated on [weblate](https://hosted.weblate.org/projects/newpipe/strings/)
* NewPipe can be translated via [Weblate](https://hosted.weblate.org/projects/newpipe/strings/). You can log in there with your GitHub account.
## Code contribution
* Stick to NewPipe style guidelines (just look the other code and than do it the same way :) )
* Do not bring nonfree software/binary blobs into the project (keep it google free)
* Stick to [f-droid contribution guidelines](https://f-droid.org/wiki/page/Inclusion_Policy)
* Make changes on a separate branch, not on the master branch (Feature-branching)
* When submitting changes, you agree that your code will be licensed under GPLv3
* Please test (compile and run) your code before you submit changes!!!
* Try to figure out you selves why CI fails, or why a merge request collides
* Please maintain your code after you contributed it.
* Respond yourselves if someone request changes or notifies issues
* Stick to NewPipe's style conventions (well, just look the other code and then do it the same way :))
* Do not bring non-free software (e.g., binary blobs) into the project. Also, make sure you do not introduce Google libraries.
* Stick to [F-Droid contribution guidelines](https://f-droid.org/wiki/page/Inclusion_Policy)
* Make changes on a separate branch, not on the master branch. This is commonly known as *feature branch workflow*. You may then send your
* When submitting changes, you confirm that your code is licensed under the terms of the [GNU General Public License v3](https://www.gnu.org/licenses/gpl-3.0.html).
* Please test (compile and run) your code before you submit changes! Ideally, provide test feedback in the PR description. Untested code will **not** be merged!
* Try to figure out yourself why builds on our CI fail.
* Make sure your PR is up-to-date with the rest of the code. Often, a simple click on "Update branch" will do the job, but if not, you are asked to merge the master branch manually and resolve the problems on your own. That will make the maintainers' jobs way easier.
* Please show intention to maintain your features and code after you contributed it. Unmaintained code is a hassle for the core developers, and just adds work. If you do not intend to maintain features you contributed, please think again about sumission, or clearly state that in the description of your PR.
* Respond yourselves if someone requests changes or otherwise raises issues about your PRs.
## Communication
* WE DO NOW HAVE A MAILING LIST: [newpipe@list.schabi.org](https://list.schabi.org/cgi-bin/mailman/listinfo/newpipe).
* If you want to get in contact with me or one of our other contributors you can send me an email at tnp(at)schabi.org
* Feel free to post suggestions, changes, ideas etc!
* There is an IRC channel on Freenode which is regularly visited by the core team and other developers: [#newpipe](irc:irc.freenode.net/newpipe). [Click here for Webchat](https://webchat.freenode.net/?channels=newpipe)!
* If you want to get in touch with the core team or one of our other contributors you can send an email to tnp(at)schabi.org. Please do not send issue reports, they will be ignored and remain unanswered! Use the GitHub issue tracker described above!
* Feel free to post suggestions, changes, ideas etc. on GitHub, IRC or the mailing list!

View File

@@ -1 +1 @@
[ ] I carefully reed the [contribution guidelines](https://github.com/TeamNewPipe/NewPipe/blob/HEAD/.github/CONTRIBUTING.md) and agree to them.
- [ ] I carefully reed the [contribution guidelines](https://github.com/TeamNewPipe/NewPipe/blob/HEAD/.github/CONTRIBUTING.md) and agree to them.

View File

@@ -5,10 +5,10 @@ android:
components:
# The BuildTools version used by NewPipe
- tools
- build-tools-25.0.2
- build-tools-26.0.1
# The SDK version used to compile NewPipe
- android-25
- android-26
# Additional components
- extra-android-m2repository
@@ -17,4 +17,3 @@ script: ./gradlew -Dorg.gradle.jvmargs=-Xmx1536m assembleDebug lintDebug testDeb
licenses:
- '.+'

View File

@@ -1,7 +1,7 @@
WARNING: PUTTING NEWPIPE OR ANY FORK OF IT INTO GOOGLE PLAYSTORE VIOLATES THEIR TERMS OF CONDITIONS.
# NewPipe
NewPipe: A free lightweight Youtube frontend for Android.
NewPipe: A free lightweight YouTube frontend for Android.
[![NewPipe](app/src/main/res/mipmap-xhdpi/ic_launcher.png)](https://newpipe.schabi.org)
[![F-Droid](https://f-droid.org/wiki/images/0/06/F-Droid-button_get-it-on.png)](https://f-droid.org/packages/org.schabi.newpipe/)
@@ -70,7 +70,7 @@ NewPipe does not use any Google framework libraries, or the YouTube API. It only
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, translation, design changes, code cleaning, or real heavy code changes, help is always welcome.
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!
If you'd like to get involved, check our [contribution notes](.github/CONTRIBUTING.md).

View File

@@ -1,23 +1,31 @@
apply plugin: 'com.android.application'
android {
compileSdkVersion 25
buildToolsVersion '25.0.2'
compileSdkVersion 26
buildToolsVersion '26.0.1'
defaultConfig {
applicationId "org.schabi.newpipe"
minSdkVersion 15
targetSdkVersion 25
versionCode 37
versionName "0.9.10"
targetSdkVersion 26
versionCode 38
versionName "0.10.0"
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
vectorDrawables.useSupportLibrary = true
}
buildTypes {
release {
minifyEnabled false
minifyEnabled true
shrinkResources true
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
debug {
multiDexEnabled true
debuggable true
applicationIdSuffix ".debug"
}
}
lintOptions {
@@ -37,25 +45,38 @@ dependencies {
exclude module: 'support-annotations'
}
compile 'com.github.TeamNewPipe:NewPipeExtractor:7ae274b'
testCompile 'junit:junit:4.12'
testCompile 'org.mockito:mockito-core:1.10.19'
testCompile 'org.json:json:20160810'
compile 'com.android.support:appcompat-v7:25.3.1'
compile 'com.android.support:support-v4:25.3.1'
compile 'com.android.support:design:25.3.1'
compile 'com.android.support:recyclerview-v7:25.3.1'
compile 'com.android.support:appcompat-v7:26.0.1'
compile 'com.android.support:support-v4:26.0.1'
compile 'com.android.support:design:26.0.1'
compile 'com.android.support:recyclerview-v7:26.0.1'
compile 'com.android.support:preference-v14:26.0.1'
compile 'com.google.code.gson:gson:2.7'
compile 'org.jsoup:jsoup:1.8.3'
compile 'org.mozilla:rhino:1.7.7'
compile 'ch.acra:acra:4.9.0'
compile 'info.guardianproject.netcipher:netcipher:1.2'
compile 'com.nostra13.universalimageloader:universal-image-loader:1.9.5'
compile 'de.hdodenhof:circleimageview:2.0.0'
compile 'de.hdodenhof:circleimageview:2.1.0'
compile 'com.github.nirhart:parallaxscroll:1.0'
compile 'com.nononsenseapps:filepicker:3.0.0'
compile 'com.google.android.exoplayer:exoplayer:r2.4.2'
compile 'com.google.android.exoplayer:exoplayer:r2.5.1'
debugCompile 'com.facebook.stetho:stetho:1.5.0'
debugCompile 'com.facebook.stetho:stetho-urlconnection:1.5.0'
debugCompile 'com.android.support:multidex:1.0.1'
compile 'io.reactivex.rxjava2:rxjava:2.1.2'
compile 'io.reactivex.rxjava2:rxandroid:2.0.1'
compile 'com.jakewharton.rxbinding2:rxbinding:2.0.0'
compile 'android.arch.persistence.room:runtime:1.0.0-alpha8'
compile 'android.arch.persistence.room:rxjava2:1.0.0-alpha8'
annotationProcessor 'android.arch.persistence.room:compiler:1.0.0-alpha8'
compile 'frankiesardo:icepick:3.2.0'
provided 'frankiesardo:icepick-processor:3.2.0'
}

View File

@@ -15,3 +15,13 @@
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}
-dontobfuscate
-keep class org.mozilla.javascript.** { *; }
-keep class org.mozilla.classfile.ClassFileWriter
-keep class com.google.android.exoplayer2.** { *; }
-dontwarn org.mozilla.javascript.tools.**
-dontwarn android.arch.util.paging.CountedDataSource
-dontwarn android.arch.persistence.room.paging.LimitOffsetDataSource

View File

@@ -0,0 +1,17 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
package="org.schabi.newpipe">
<application
android:name=".DebugApp"
android:label="NewPipe Debug"
tools:replace="android:name, android:label">
<activity
android:name=".MainActivity"
android:label="NewPipe Debug"
tools:replace="android:label"/>
</application>
</manifest>

View File

@@ -0,0 +1,45 @@
package org.schabi.newpipe;
import android.content.Context;
import android.support.multidex.MultiDex;
import com.facebook.stetho.Stetho;
public class DebugApp extends App {
private static final String TAG = DebugApp.class.toString();
@Override
protected void attachBaseContext(Context base) {
super.attachBaseContext(base);
MultiDex.install(this);
}
@Override
public void onCreate() {
super.onCreate();
initStetho();
}
private void initStetho() {
// Create an InitializerBuilder
Stetho.InitializerBuilder initializerBuilder =
Stetho.newInitializerBuilder(this);
// Enable Chrome DevTools
initializerBuilder.enableWebKitInspector(
Stetho.defaultInspectorModulesProvider(this)
);
// Enable command line interface
initializerBuilder.enableDumpapp(
Stetho.defaultDumperPluginsProvider(getApplicationContext())
);
// Use the InitializerBuilder to generate an Initializer
Stetho.Initializer initializer = initializerBuilder.build();
// Initialize Stetho with the Initializer
Stetho.initialize(initializer);
}
}

View File

@@ -16,11 +16,12 @@
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:logo="@mipmap/ic_launcher"
android:theme="@style/AppTheme"
android:theme="@style/DarkTheme"
tools:ignore="AllowBackup">
<activity
android:name=".MainActivity"
android:label="@string/app_name">
android:label="@string/app_name"
android:launchMode="singleTask">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
@@ -28,7 +29,7 @@
</activity>
<activity
android:name=".player.PlayVideoActivity"
android:name=".player.old.PlayVideoActivity"
android:configChanges="orientation|keyboardHidden|screenSize"
android:theme="@style/VideoPlayerTheme"
tools:ignore="UnusedAttribute"/>
@@ -51,6 +52,15 @@
<activity
android:name=".settings.SettingsActivity"
android:label="@string/settings"/>
<activity
android:name=".about.AboutActivity"
android:label="@string/title_activity_about"/>
<activity
android:name=".history.HistoryActivity"
android:label="@string/title_activity_history"/>
<activity
android:name=".PanicResponderActivity"
android:launchMode="singleInstance"
@@ -62,6 +72,7 @@
<category android:name="android.intent.category.DEFAULT"/>
</intent-filter>
</activity>
<activity
android:name=".ExitActivity"
android:label="@string/general_error"
@@ -72,8 +83,7 @@
<activity
android:name=".download.DownloadActivity"
android:label="@string/app_name"
android:launchMode="singleTask"
android:theme="@style/AppTheme"/>
android:launchMode="singleTask"/>
<service android:name="us.shandian.giga.service.DownloadManagerService"/>
@@ -82,6 +92,7 @@
android:label="@string/app_name"
android:launchMode="singleTop"
android:theme="@style/FilePickerTheme"/>
<activity
android:name=".ReCaptchaActivity"
android:label="@string/reCaptchaActivity"/>
@@ -121,6 +132,8 @@
<!-- channel prefix -->
<data android:pathPrefix="/channel/"/>
<data android:pathPrefix="/user/"/>
<!-- playlist prefix -->
<data android:pathPrefix="/playlist"/>
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.VIEW"/>
@@ -154,12 +167,11 @@
<data android:mimeType="text/plain"/>
</intent-filter>
</activity>
<activity
android:name=".RouterPopupActivity"
android:label="@string/popup_mode_share_menu_title"
android:taskAffinity=""
android:theme="@android:style/Theme.NoDisplay"
android:label="@string/popup_mode_share_menu_title">
android:theme="@android:style/Theme.NoDisplay">
<intent-filter>
<action android:name="android.intent.action.VIEW"/>
<action android:name="android.media.action.MEDIA_PLAY_FROM_SEARCH"/>
@@ -209,11 +221,5 @@
<data android:mimeType="text/plain"/>
</intent-filter>
</activity>
<activity
android:name=".about.AboutActivity"
android:label="@string/title_activity_about"
android:theme="@style/AppTheme">
</activity>
</application>
</manifest>
</manifest>

View File

@@ -1,6 +1,6 @@
package org.schabi.newpipe;
/**
/*
* Created by Christian Schabesberger on 24.12.15.
*
* Copyright (C) Christian Schabesberger 2015 <chris.schabesberger@mailbox.org>

View File

@@ -1,7 +1,11 @@
package org.schabi.newpipe;
import android.app.Application;
import android.app.NotificationChannel;
import android.app.NotificationManager;
import android.content.Context;
import android.os.Build;
import android.util.Log;
import com.nostra13.universalimageloader.core.ImageLoader;
import com.nostra13.universalimageloader.core.ImageLoaderConfiguration;
@@ -16,12 +20,20 @@ import org.schabi.newpipe.report.AcraReportSenderFactory;
import org.schabi.newpipe.report.ErrorActivity;
import org.schabi.newpipe.report.UserAction;
import org.schabi.newpipe.settings.SettingsActivity;
import org.schabi.newpipe.util.ThemeHelper;
import org.schabi.newpipe.util.ExtractorHelper;
import org.schabi.newpipe.util.StateSaver;
import info.guardianproject.netcipher.NetCipher;
import info.guardianproject.netcipher.proxy.OrbotHelper;
import java.io.IOException;
import java.io.InterruptedIOException;
import java.net.SocketException;
/**
import io.reactivex.annotations.NonNull;
import io.reactivex.exceptions.CompositeException;
import io.reactivex.exceptions.UndeliverableException;
import io.reactivex.functions.Consumer;
import io.reactivex.plugins.RxJavaPlugins;
/*
* Copyright (C) Hans-Christoph Steiner 2016 <hans@eds.org>
* App.java is part of NewPipe.
*
@@ -40,73 +52,101 @@ import info.guardianproject.netcipher.proxy.OrbotHelper;
*/
public class App extends Application {
private static final String TAG = App.class.toString();
protected static final String TAG = App.class.toString();
private static boolean useTor;
@SuppressWarnings("unchecked")
private static final Class<? extends ReportSenderFactory>[] reportSenderFactoryClasses = new Class[]{AcraReportSenderFactory.class};
final Class<? extends ReportSenderFactory>[] reportSenderFactoryClasses
= new Class[]{AcraReportSenderFactory.class};
@Override
protected void attachBaseContext(Context base) {
super.attachBaseContext(base);
initACRA();
}
@Override
public void onCreate() {
super.onCreate();
// init crashreport
try {
final ACRAConfiguration acraConfig = new ConfigurationBuilder(this)
.setReportSenderFactoryClasses(reportSenderFactoryClasses)
.build();
ACRA.init(this, acraConfig);
} catch(ACRAConfigurationException ace) {
ace.printStackTrace();
ErrorActivity.reportError(this, ace, null, null,
ErrorActivity.ErrorInfo.make(UserAction.SEARCHED,"none",
"Could not initialize ACRA crash report", R.string.app_ui_crash));
}
// Initialize settings first because others inits can use its values
SettingsActivity.initSettings(this);
//init NewPipe
NewPipe.init(Downloader.getInstance());
NewPipeDatabase.init(this);
StateSaver.init(this);
initNotificationChannel();
// Initialize image loader
ImageLoaderConfiguration config = new ImageLoaderConfiguration.Builder(this).build();
ImageLoader.getInstance().init(config);
/*
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this);
if(prefs.getBoolean(getString(R.string.use_tor_key), false)) {
OrbotHelper.requestStartTor(this);
configureTor(true);
} else {
configureTor(false);
}*/
configureTor(false);
// DO NOT REMOVE THIS FUNCTION!!!
// Otherwise downloadPathPreference has invalid value.
SettingsActivity.initSettings(this);
ThemeHelper.setTheme(getApplicationContext());
configureRxJavaErrorHandler();
}
/**
* Set the proxy settings based on whether Tor should be enabled or not.
*/
public static void configureTor(boolean enabled) {
useTor = enabled;
if (useTor) {
NetCipher.useTor();
} else {
NetCipher.setProxy(null);
private void configureRxJavaErrorHandler() {
// https://github.com/ReactiveX/RxJava/wiki/What's-different-in-2.0#error-handling
RxJavaPlugins.setErrorHandler(new Consumer<Throwable>() {
@Override
public void accept(@NonNull Throwable throwable) throws Exception {
Log.e(TAG, "RxJavaPlugins.ErrorHandler called with -> : throwable = [" + throwable.getClass().getName() + "]");
if (throwable instanceof UndeliverableException) {
// As UndeliverableException is a wrapper, get the cause of it to get the "real" exception
throwable = throwable.getCause();
}
if (throwable instanceof CompositeException) {
for (Throwable element : ((CompositeException) throwable).getExceptions()) {
if (checkThrowable(element)) return;
}
}
if (checkThrowable(throwable)) return;
// Throw uncaught exception that will trigger the report system
Thread.currentThread().getUncaughtExceptionHandler()
.uncaughtException(Thread.currentThread(), throwable);
}
private boolean checkThrowable(@NonNull Throwable throwable) {
// Don't crash the application over a simple network problem
return ExtractorHelper.hasAssignableCauseThrowable(throwable,
IOException.class, SocketException.class, InterruptedException.class, InterruptedIOException.class);
}
});
}
private void initACRA() {
try {
final ACRAConfiguration acraConfig = new ConfigurationBuilder(this)
.setReportSenderFactoryClasses(reportSenderFactoryClasses)
.setBuildConfigClass(BuildConfig.class)
.build();
ACRA.init(this, acraConfig);
} catch (ACRAConfigurationException ace) {
ace.printStackTrace();
ErrorActivity.reportError(this, ace, null, null, ErrorActivity.ErrorInfo.make(UserAction.SOMETHING_ELSE, "none",
"Could not initialize ACRA crash report", R.string.app_ui_crash));
}
}
public static void checkStartTor(Context context) {
if (useTor) {
OrbotHelper.requestStartTor(context);
public void initNotificationChannel() {
if (Build.VERSION.SDK_INT < android.os.Build.VERSION_CODES.O) {
return;
}
}
public static boolean isUsingTor() {
return useTor;
final String id = getString(R.string.notification_channel_id);
final CharSequence name = getString(R.string.notification_channel_name);
final String description = getString(R.string.notification_channel_description);
// Keep this below DEFAULT to avoid making noise on every notification update
final int importance = NotificationManager.IMPORTANCE_LOW;
NotificationChannel mChannel = new NotificationChannel(id, name, importance);
mChannel.setDescription(description);
NotificationManager mNotificationManager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
mNotificationManager.createNotificationChannel(mChannel);
}
}

View File

@@ -0,0 +1,121 @@
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 icepick.Icepick;
public abstract class BaseFragment extends Fragment {
protected final String TAG = getClass().getSimpleName() + "@" + Integer.toHexString(hashCode());
protected boolean DEBUG = MainActivity.DEBUG;
protected AppCompatActivity activity;
public static final ImageLoader imageLoader = ImageLoader.getInstance();
/*//////////////////////////////////////////////////////////////////////////
// Fragment's Lifecycle
//////////////////////////////////////////////////////////////////////////*/
@Override
public void onAttach(Context context) {
super.onAttach(context);
activity = (AppCompatActivity) context;
}
@Override
public void onDetach() {
super.onDetach();
activity = null;
}
@Override
public void onCreate(Bundle savedInstanceState) {
if (DEBUG) Log.d(TAG, "onCreate() called with: savedInstanceState = [" + savedInstanceState + "]");
super.onCreate(savedInstanceState);
Icepick.restoreInstanceState(this, savedInstanceState);
if (savedInstanceState != null) onRestoreInstanceState(savedInstanceState);
}
@Override
public void onViewCreated(View rootView, Bundle savedInstanceState) {
super.onViewCreated(rootView, savedInstanceState);
if (DEBUG) {
Log.d(TAG, "onViewCreated() called with: rootView = [" + rootView + "], savedInstanceState = [" + savedInstanceState + "]");
}
initViews(rootView, savedInstanceState);
initListeners();
}
@Override
public void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
Icepick.saveInstanceState(this, outState);
}
protected void onRestoreInstanceState(@NonNull Bundle savedInstanceState) {
}
/*//////////////////////////////////////////////////////////////////////////
// Init
//////////////////////////////////////////////////////////////////////////*/
protected void initViews(View rootView, Bundle savedInstanceState) {
}
protected void initListeners() {
}
/*//////////////////////////////////////////////////////////////////////////
// Utils
//////////////////////////////////////////////////////////////////////////*/
protected final int resolveResourceIdFromAttr(@AttrRes int attr) {
TypedArray a = activity.getTheme().obtainStyledAttributes(new int[]{attr});
int attributeResourceId = a.getResourceId(0, 0);
a.recycle();
return attributeResourceId;
}
/*//////////////////////////////////////////////////////////////////////////
// DisplayImageOptions default configurations
//////////////////////////////////////////////////////////////////////////*/
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();
}

View File

@@ -1,20 +1,24 @@
package org.schabi.newpipe;
import android.util.Log;
import org.schabi.newpipe.extractor.exceptions.ReCaptchaException;
import org.schabi.newpipe.util.ExtractorHelper;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.InterruptedIOException;
import java.net.URL;
import java.net.UnknownHostException;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import javax.net.ssl.HttpsURLConnection;
/**
/*
* Created by Christian Schabesberger on 28.01.16.
*
* Copyright (C) Christian Schabesberger 2016 <chris.schabesberger@mailbox.org>
@@ -35,16 +39,17 @@ import javax.net.ssl.HttpsURLConnection;
*/
public class Downloader implements org.schabi.newpipe.extractor.Downloader {
private static final String USER_AGENT = "Mozilla/5.0 (Windows NT 6.1; WOW64; rv:43.0) Gecko/20100101 Firefox/43.0";
public static final String USER_AGENT = "Mozilla/5.0 (Windows NT 6.1; WOW64; rv:43.0) Gecko/20100101 Firefox/43.0";
private static String mCookies = "";
private static Downloader instance = null;
private Downloader() {}
private Downloader() {
}
public static Downloader getInstance() {
if(instance == null) {
if (instance == null) {
synchronized (Downloader.class) {
if (instance == null) {
instance = new Downloader();
@@ -62,41 +67,66 @@ public class Downloader implements org.schabi.newpipe.extractor.Downloader {
return Downloader.mCookies;
}
/**Download the text file at the supplied URL as in download(String),
/**
* Download the text file at the supplied URL as in download(String),
* but set the HTTP header field "Accept-Language" to the supplied string.
* @param siteUrl the URL of the text file to return the contents of
*
* @param siteUrl the URL of the text file to return the contents of
* @param language the language (usually a 2-character code) to set as the preferred language
* @return the contents of the specified text file*/
* @return the contents of the specified text file
*/
@Override
public String download(String siteUrl, String language) throws IOException, ReCaptchaException {
Map<String, String> requestProperties = new HashMap<>();
requestProperties.put("Accept-Language", language);
return download(siteUrl, requestProperties);
}
/**Download the text file at the supplied URL as in download(String),
* but set the HTTP header field "Accept-Language" to the supplied string.
* @param siteUrl the URL of the text file to return the contents of
/**
* Download the text file at the supplied URL as in download(String),
* but set the HTTP headers included in the customProperties map.
*
* @param siteUrl the URL of the text file to return the contents of
* @param customProperties set request header properties
* @return the contents of the specified text file
* @throws IOException*/
* @throws IOException
*/
@Override
public String download(String siteUrl, Map<String, String> customProperties) throws IOException, ReCaptchaException {
URL url = new URL(siteUrl);
HttpsURLConnection con = (HttpsURLConnection) url.openConnection();
Iterator it = customProperties.entrySet().iterator();
while(it.hasNext()) {
Map.Entry pair = (Map.Entry)it.next();
con.setRequestProperty((String)pair.getKey(), (String)pair.getValue());
while (it.hasNext()) {
Map.Entry pair = (Map.Entry) it.next();
con.setRequestProperty((String) pair.getKey(), (String) pair.getValue());
}
return dl(con);
}
/**Common functionality between download(String url) and download(String url, String language)*/
/**
* Download (via HTTP) the text file located at the supplied URL, and return its contents.
* Primarily intended for downloading web pages.
*
* @param siteUrl the URL of the text file to download
* @return the contents of the specified text file
*/
@Override
public String download(String siteUrl) throws IOException, ReCaptchaException {
URL url = new URL(siteUrl);
HttpsURLConnection con = (HttpsURLConnection) url.openConnection();
//HttpsURLConnection con = NetCipher.getHttpsURLConnection(url);
return dl(con);
}
/**
* Common functionality between download(String url) and download(String url, String language)
*/
private static String dl(HttpsURLConnection con) throws IOException, ReCaptchaException {
StringBuilder response = new StringBuilder();
BufferedReader in = null;
try {
con.setReadTimeout(30 * 1000);// 30s
con.setRequestMethod("GET");
con.setRequestProperty("User-Agent", USER_AGENT);
@@ -104,17 +134,22 @@ public class Downloader implements org.schabi.newpipe.extractor.Downloader {
con.setRequestProperty("Cookie", getCookies());
}
in = new BufferedReader(
new InputStreamReader(con.getInputStream()));
in = new BufferedReader(new InputStreamReader(con.getInputStream()));
for (Map.Entry<String, List<String>> entry : con.getHeaderFields().entrySet()) {
System.err.println(entry.getKey() + ": " + entry.getValue());
}
String inputLine;
while((inputLine = in.readLine()) != null) {
while ((inputLine = in.readLine()) != null) {
response.append(inputLine);
}
} catch(UnknownHostException uhe) {//thrown when there's no internet connection
throw new IOException("unknown host or no network", uhe);
//Toast.makeText(getActivity(), uhe.getMessage(), Toast.LENGTH_LONG).show();
} catch(Exception e) {
} catch (Exception e) {
Log.e("Downloader", "dl() ----- Exception thrown → " + e.getClass().getName());
if (ExtractorHelper.isInterruptedCaused(e)) {
throw new InterruptedIOException(e.getMessage());
}
/*
* HTTP 429 == Too Many Request
* Receive from Youtube.com = ReCaptcha challenge request
@@ -123,24 +158,14 @@ public class Downloader implements org.schabi.newpipe.extractor.Downloader {
if (con.getResponseCode() == 429) {
throw new ReCaptchaException("reCaptcha Challenge requested");
}
throw new IOException(e);
throw new IOException(con.getResponseCode() + " " + con.getResponseMessage(), e);
} finally {
if(in != null) {
if (in != null) {
in.close();
}
}
return response.toString();
}
/**Download (via HTTP) the text file located at the supplied URL, and return its contents.
* Primarily intended for downloading web pages.
* @param siteUrl the URL of the text file to download
* @return the contents of the specified text file*/
public String download(String siteUrl) throws IOException, ReCaptchaException {
URL url = new URL(siteUrl);
HttpsURLConnection con = (HttpsURLConnection) url.openConnection();
//HttpsURLConnection con = NetCipher.getHttpsURLConnection(url);
return dl(con);
}
}

View File

@@ -7,7 +7,7 @@ import android.content.Intent;
import android.os.Build;
import android.os.Bundle;
/**
/*
* Copyright (C) Hans-Christoph Steiner 2016 <hans@eds.org>
* ExitActivity.java is part of NewPipe.
*

View File

@@ -1,65 +0,0 @@
package org.schabi.newpipe;
import android.content.Context;
import android.graphics.Bitmap;
import android.view.View;
import com.nostra13.universalimageloader.core.assist.FailReason;
import com.nostra13.universalimageloader.core.listener.ImageLoadingListener;
import org.schabi.newpipe.extractor.NewPipe;
import org.schabi.newpipe.report.ErrorActivity;
import org.schabi.newpipe.report.UserAction;
/**
* Created by Christian Schabesberger on 01.08.16.
*
* Copyright (C) Christian Schabesberger 2015 <chris.schabesberger@mailbox.org>
* StreamInfoItemViewCreator.java is part of NewPipe.
*
* NewPipe is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* NewPipe is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with NewPipe. If not, see <http://www.gnu.org/licenses/>.
*/
public class ImageErrorLoadingListener implements ImageLoadingListener {
private int serviceId = -1;
private Context context = null;
private View rootView = null;
public ImageErrorLoadingListener(Context context, View rootView, int serviceId) {
this.context = context;
this.serviceId= serviceId;
this.rootView = rootView;
}
@Override
public void onLoadingStarted(String imageUri, View view) {}
@Override
public void onLoadingFailed(String imageUri, View view, FailReason failReason) {
ErrorActivity.reportError(context,
failReason.getCause(), null, rootView,
ErrorActivity.ErrorInfo.make(UserAction.LOAD_IMAGE,
NewPipe.getNameOfService(serviceId), imageUri,
R.string.could_not_load_image));
}
@Override
public void onLoadingComplete(String imageUri, View view, Bitmap loadedImage) {
}
@Override
public void onLoadingCancelled(String imageUri, View view) {}
}

View File

@@ -23,7 +23,10 @@ package org.schabi.newpipe;
import android.content.Intent;
import android.content.SharedPreferences;
import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
import android.preference.PreferenceManager;
import android.support.annotation.NonNull;
import android.support.v4.app.Fragment;
import android.support.v7.app.ActionBar;
import android.support.v7.app.AppCompatActivity;
@@ -34,19 +37,37 @@ import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import org.schabi.newpipe.download.DownloadActivity;
import org.schabi.newpipe.database.AppDatabase;
import org.schabi.newpipe.database.history.dao.HistoryDAO;
import org.schabi.newpipe.database.history.dao.SearchHistoryDAO;
import org.schabi.newpipe.database.history.dao.WatchHistoryDAO;
import org.schabi.newpipe.database.history.model.HistoryEntry;
import org.schabi.newpipe.database.history.model.SearchHistoryEntry;
import org.schabi.newpipe.database.history.model.WatchHistoryEntry;
import org.schabi.newpipe.extractor.StreamingService;
import org.schabi.newpipe.extractor.stream.AudioStream;
import org.schabi.newpipe.extractor.stream.StreamInfo;
import org.schabi.newpipe.extractor.stream.VideoStream;
import org.schabi.newpipe.fragments.BackPressable;
import org.schabi.newpipe.fragments.detail.VideoDetailFragment;
import org.schabi.newpipe.fragments.search.SearchFragment;
import org.schabi.newpipe.settings.SettingsActivity;
import org.schabi.newpipe.fragments.list.search.SearchFragment;
import org.schabi.newpipe.history.HistoryListener;
import org.schabi.newpipe.util.Constants;
import org.schabi.newpipe.util.NavigationHelper;
import org.schabi.newpipe.util.PermissionHelper;
import org.schabi.newpipe.util.StateSaver;
import org.schabi.newpipe.util.ThemeHelper;
public class MainActivity extends AppCompatActivity {
import java.util.Date;
import io.reactivex.disposables.Disposable;
import io.reactivex.functions.Consumer;
import io.reactivex.schedulers.Schedulers;
import io.reactivex.subjects.PublishSubject;
public class MainActivity extends AppCompatActivity implements HistoryListener {
private static final String TAG = "MainActivity";
public static final boolean DEBUG = false;
private SharedPreferences sharedPreferences;
/*//////////////////////////////////////////////////////////////////////////
// Activity's LifeCycle
@@ -63,8 +84,21 @@ public class MainActivity extends AppCompatActivity {
initFragments();
}
Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
Toolbar toolbar = findViewById(R.id.toolbar);
setSupportActionBar(toolbar);
sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this);
initHistory();
}
@Override
protected void onDestroy() {
super.onDestroy();
if (!isChangingConfigurations()) {
StateSaver.clearStateFiles();
}
disposeHistory();
}
@Override
@@ -75,7 +109,14 @@ public class MainActivity extends AppCompatActivity {
if (sharedPreferences.getBoolean(Constants.KEY_THEME_CHANGE, false)) {
if (DEBUG) Log.d(TAG, "Theme has changed, recreating activity...");
sharedPreferences.edit().putBoolean(Constants.KEY_THEME_CHANGE, false).apply();
this.recreate();
// https://stackoverflow.com/questions/10844112/runtimeexception-performing-pause-of-activity-that-is-not-resumed
// Briefly, let the activity resume properly posting the recreate call to end of the message queue
new Handler(Looper.getMainLooper()).post(new Runnable() {
@Override
public void run() {
MainActivity.this.recreate();
}
});
}
}
@@ -100,7 +141,10 @@ public class MainActivity extends AppCompatActivity {
if (DEBUG) Log.d(TAG, "onBackPressed() called");
Fragment fragment = getSupportFragmentManager().findFragmentById(R.id.fragment_holder);
if (fragment instanceof VideoDetailFragment) if (((VideoDetailFragment) fragment).onActivityBackPressed()) return;
// If current fragment implements BackPressable (i.e. can/wanna handle back press) delegate the back press to it
if (fragment instanceof BackPressable) {
if (((BackPressable) fragment).onBackPressed()) return;
}
if (getSupportFragmentManager().getBackStackEntryCount() == 1) {
@@ -143,20 +187,20 @@ public class MainActivity extends AppCompatActivity {
int id = item.getItemId();
switch (id) {
case android.R.id.home: {
case android.R.id.home:
NavigationHelper.gotoMainFragment(getSupportFragmentManager());
return true;
}
case R.id.action_settings: {
case R.id.action_settings:
NavigationHelper.openSettings(this);
return true;
}
case R.id.action_show_downloads: {
case R.id.action_show_downloads:
return NavigationHelper.openDownloads(this);
}
case R.id.action_about:
NavigationHelper.openAbout(this);
return true;
case R.id.action_history:
NavigationHelper.openHistory(this);
return true;
default:
return super.onOptionsItemSelected(item);
}
@@ -167,6 +211,8 @@ public class MainActivity extends AppCompatActivity {
//////////////////////////////////////////////////////////////////////////*/
private void initFragments() {
if (DEBUG) Log.d(TAG, "initFragments() called");
StateSaver.clearStateFiles();
if (getIntent() != null && getIntent().hasExtra(Constants.KEY_LINK_TYPE)) {
handleIntent(getIntent());
} else NavigationHelper.gotoMainFragment(getSupportFragmentManager());
@@ -191,6 +237,9 @@ public class MainActivity extends AppCompatActivity {
case CHANNEL:
NavigationHelper.openChannelFragment(getSupportFragmentManager(), serviceId, url, title);
break;
case PLAYLIST:
NavigationHelper.openPlaylistFragment(getSupportFragmentManager(), serviceId, url, title);
break;
}
} else if (intent.hasExtra(Constants.KEY_OPEN_SEARCH)) {
String searchQuery = intent.getStringExtra(Constants.KEY_QUERY);
@@ -201,4 +250,75 @@ public class MainActivity extends AppCompatActivity {
NavigationHelper.gotoMainFragment(getSupportFragmentManager());
}
}
/*//////////////////////////////////////////////////////////////////////////
// History
//////////////////////////////////////////////////////////////////////////*/
private WatchHistoryDAO watchHistoryDAO;
private SearchHistoryDAO searchHistoryDAO;
private PublishSubject<HistoryEntry> historyEntrySubject;
private Disposable disposable;
private void initHistory() {
final AppDatabase database = NewPipeDatabase.getInstance();
watchHistoryDAO = database.watchHistoryDAO();
searchHistoryDAO = database.searchHistoryDAO();
historyEntrySubject = PublishSubject.create();
disposable = historyEntrySubject
.observeOn(Schedulers.io())
.subscribe(getHistoryEntryConsumer());
}
private void disposeHistory() {
if (disposable != null) disposable.dispose();
watchHistoryDAO = null;
searchHistoryDAO = null;
}
@NonNull
private Consumer<HistoryEntry> getHistoryEntryConsumer() {
return new Consumer<HistoryEntry>() {
@Override
public void accept(HistoryEntry historyEntry) throws Exception {
//noinspection unchecked
HistoryDAO<HistoryEntry> historyDAO = (HistoryDAO<HistoryEntry>)
(historyEntry instanceof SearchHistoryEntry ? searchHistoryDAO : watchHistoryDAO);
HistoryEntry latestEntry = historyDAO.getLatestEntry();
if (historyEntry.hasEqualValues(latestEntry)) {
latestEntry.setCreationDate(historyEntry.getCreationDate());
historyDAO.update(latestEntry);
} else {
historyDAO.insert(historyEntry);
}
}
};
}
private void addWatchHistoryEntry(StreamInfo streamInfo) {
if (sharedPreferences.getBoolean(getString(R.string.enable_watch_history_key), true)) {
WatchHistoryEntry entry = new WatchHistoryEntry(streamInfo);
historyEntrySubject.onNext(entry);
}
}
@Override
public void onVideoPlayed(StreamInfo streamInfo, VideoStream videoStream) {
addWatchHistoryEntry(streamInfo);
}
@Override
public void onAudioPlayed(StreamInfo streamInfo, AudioStream audioStream) {
addWatchHistoryEntry(streamInfo);
}
@Override
public void onSearch(int serviceId, String query) {
// Add search history entry
if (sharedPreferences.getBoolean(getString(R.string.enable_search_history_key), true)) {
SearchHistoryEntry searchHistoryEntry = new SearchHistoryEntry(new Date(), serviceId, query);
historyEntrySubject.onNext(searchHistoryEntry);
}
}
}

View File

@@ -0,0 +1,31 @@
package org.schabi.newpipe;
import android.arch.persistence.room.Room;
import android.content.Context;
import android.support.annotation.NonNull;
import org.schabi.newpipe.database.AppDatabase;
import static org.schabi.newpipe.database.AppDatabase.DATABASE_NAME;
public final class NewPipeDatabase {
private static AppDatabase databaseInstance;
private NewPipeDatabase() {
//no instance
}
public static void init(Context context) {
databaseInstance = Room.databaseBuilder(context.getApplicationContext(),
AppDatabase.class, DATABASE_NAME
).build();
}
@NonNull
public static AppDatabase getInstance() {
if (databaseInstance == null) throw new RuntimeException("Database not initialized");
return databaseInstance;
}
}

View File

@@ -6,9 +6,8 @@ import android.app.Activity;
import android.content.Intent;
import android.os.Build;
import android.os.Bundle;
import android.media.AudioManager;
/**
/*
* Copyright (C) Hans-Christoph Steiner 2016 <hans@eds.org>
* PanicResponderActivity.java is part of NewPipe.
*

View File

@@ -16,7 +16,7 @@ import android.webkit.WebSettings;
import android.webkit.WebView;
import android.webkit.WebViewClient;
/**
/*
* Created by beneth <bmauduit@beneth.fr> on 06.12.16.
*
* Copyright (C) Christian Schabesberger 2015 <chris.schabesberger@mailbox.org>
@@ -49,7 +49,7 @@ public class ReCaptchaActivity extends AppCompatActivity {
// Set return to Cancel by default
setResult(RESULT_CANCELED);
Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
Toolbar toolbar = findViewById(R.id.toolbar);
setSupportActionBar(toolbar);
ActionBar actionBar = getSupportActionBar();
@@ -59,7 +59,7 @@ public class ReCaptchaActivity extends AppCompatActivity {
actionBar.setDisplayShowTitleEnabled(true);
}
WebView myWebView = (WebView) findViewById(R.id.reCaptchaWebView);
WebView myWebView = findViewById(R.id.reCaptchaWebView);
// Enable Javascript
WebSettings webSettings = myWebView.getSettings();

View File

@@ -1,8 +1,8 @@
package org.schabi.newpipe;
import android.app.Activity;
import android.content.Intent;
import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import android.widget.Toast;
import org.schabi.newpipe.util.NavigationHelper;
@@ -32,7 +32,7 @@ import java.util.HashSet;
* This Acitivty is designed to route share/open intents to the specified service, and
* to the part of the service which can handle the url.
*/
public class RouterActivity extends Activity {
public class RouterActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
@@ -40,8 +40,6 @@ public class RouterActivity extends Activity {
String videoUrl = getUrl(getIntent());
handleUrl(videoUrl);
finish();
}
protected void handleUrl(String url) {
@@ -50,6 +48,8 @@ public class RouterActivity extends Activity {
} catch (Exception e) {
Toast.makeText(this, R.string.url_not_supported_toast, Toast.LENGTH_LONG).show();
}
finish();
}
/*//////////////////////////////////////////////////////////////////////////

View File

@@ -6,6 +6,7 @@ import android.widget.Toast;
import org.schabi.newpipe.extractor.NewPipe;
import org.schabi.newpipe.extractor.StreamingService;
import org.schabi.newpipe.extractor.exceptions.ExtractionException;
import org.schabi.newpipe.player.PopupVideoPlayer;
import org.schabi.newpipe.util.Constants;
import org.schabi.newpipe.util.PermissionHelper;
@@ -22,8 +23,10 @@ public class RouterPopupActivity extends RouterActivity {
Toast.makeText(this, R.string.msg_popup_permission, Toast.LENGTH_LONG).show();
return;
}
StreamingService service = NewPipe.getServiceByUrl(url);
if (service == null) {
StreamingService service;
try {
service = NewPipe.getServiceByUrl(url);
} catch (ExtractionException e) {
Toast.makeText(this, R.string.url_not_supported_toast, Toast.LENGTH_LONG).show();
return;
}
@@ -40,5 +43,7 @@ public class RouterPopupActivity extends RouterActivity {
callIntent.putExtra(Constants.KEY_URL, url);
callIntent.putExtra(Constants.KEY_SERVICE_ID, service.getServiceId());
startService(callIntent);
finish();
}
}

View File

@@ -36,11 +36,13 @@ public class AboutActivity extends AppCompatActivity {
new SoftwareComponent("Rhino", "2015", "Mozilla", "https://www.mozilla.org/rhino/", StandardLicenses.MPL2),
new SoftwareComponent("ACRA", "2013", "Kevin Gaudin", "http://www.acra.ch", StandardLicenses.APACHE2),
new SoftwareComponent("Universal Image Loader", "2011 - 2015", "Sergey Tarasevich", "https://github.com/nostra13/Android-Universal-Image-Loader", StandardLicenses.APACHE2),
new SoftwareComponent("Netcipher", "2015", "The Guardian Project", "https://guardianproject.info/code/netcipher/", StandardLicenses.APACHE2),
new SoftwareComponent("CircleImageView", "2014 - 2017", "Henning Dodenhof", "https://github.com/hdodenhof/CircleImageView", StandardLicenses.APACHE2),
new SoftwareComponent("ParalaxScrollView", "2014", "Nir Hartmann", "https://github.com/nirhart/ParallaxScroll", StandardLicenses.MIT),
new SoftwareComponent("NoNonsense-FilePicker", "2016", "Jonas Kalderstam", "https://github.com/spacecowboy/NoNonsense-FilePicker", StandardLicenses.MPL2),
new SoftwareComponent("ExoPlayer", "2014-2017", "Google Inc", "https://github.com/google/ExoPlayer", StandardLicenses.APACHE2)
new SoftwareComponent("ExoPlayer", "2014-2017", "Google Inc", "https://github.com/google/ExoPlayer", StandardLicenses.APACHE2),
new SoftwareComponent("RxAndroid", "2015", "The RxAndroid authors", "https://github.com/ReactiveX/RxAndroid", StandardLicenses.APACHE2),
new SoftwareComponent("RxJava", "2016-present", "RxJava Contributors", "https://github.com/ReactiveX/RxJava", StandardLicenses.APACHE2),
new SoftwareComponent("RxBinding", "2015", "Jake Wharton", "https://github.com/JakeWharton/RxBinding", StandardLicenses.APACHE2)
};
/**
@@ -65,7 +67,7 @@ public class AboutActivity extends AppCompatActivity {
setContentView(R.layout.activity_about);
Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
Toolbar toolbar = findViewById(R.id.toolbar);
setSupportActionBar(toolbar);
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
// Create the adapter that will return a fragment for each of the three
@@ -73,10 +75,10 @@ public class AboutActivity extends AppCompatActivity {
mSectionsPagerAdapter = new SectionsPagerAdapter(getSupportFragmentManager());
// Set up the ViewPager with the sections adapter.
mViewPager = (ViewPager) findViewById(R.id.container);
mViewPager = findViewById(R.id.container);
mViewPager.setAdapter(mSectionsPagerAdapter);
TabLayout tabLayout = (TabLayout) findViewById(R.id.tabs);
TabLayout tabLayout = findViewById(R.id.tabs);
tabLayout.setupWithViewPager(mViewPager);
}
@@ -127,7 +129,7 @@ public class AboutActivity extends AppCompatActivity {
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
View rootView = inflater.inflate(R.layout.fragment_about, container, false);
TextView version = (TextView) rootView.findViewById(R.id.app_version);
TextView version = rootView.findViewById(R.id.app_version);
version.setText(BuildConfig.VERSION_NAME);
View githubLink = rootView.findViewById(R.id.github_link);

View File

@@ -3,9 +3,6 @@ package org.schabi.newpipe.about;
import android.net.Uri;
import android.os.Parcel;
import android.os.Parcelable;
import android.text.TextUtils;
import java.net.URLEncoder;
/**
* A software license

View File

@@ -87,12 +87,12 @@ public class LicenseFragment extends Fragment {
@Override
public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
View rootView = inflater.inflate(R.layout.fragment_licenses, container, false);
ViewGroup softwareComponentsView = (ViewGroup) rootView.findViewById(R.id.software_components);
ViewGroup softwareComponentsView = rootView.findViewById(R.id.software_components);
for (final SoftwareComponent component : softwareComponents) {
View componentView = inflater.inflate(R.layout.item_software_component, container, false);
TextView softwareName = (TextView) componentView.findViewById(R.id.name);
TextView copyright = (TextView) componentView.findViewById(R.id.copyright);
TextView softwareName = componentView.findViewById(R.id.name);
TextView copyright = componentView.findViewById(R.id.copyright);
softwareName.setText(component.getName());
copyright.setText(getContext().getString(R.string.copyright,
component.getYears(),

View File

@@ -0,0 +1,26 @@
package org.schabi.newpipe.database;
import android.arch.persistence.room.Database;
import android.arch.persistence.room.RoomDatabase;
import android.arch.persistence.room.TypeConverters;
import org.schabi.newpipe.database.history.Converters;
import org.schabi.newpipe.database.history.dao.SearchHistoryDAO;
import org.schabi.newpipe.database.history.dao.WatchHistoryDAO;
import org.schabi.newpipe.database.history.model.SearchHistoryEntry;
import org.schabi.newpipe.database.history.model.WatchHistoryEntry;
import org.schabi.newpipe.database.subscription.SubscriptionDAO;
import org.schabi.newpipe.database.subscription.SubscriptionEntity;
@TypeConverters({Converters.class})
@Database(entities = {SubscriptionEntity.class, WatchHistoryEntry.class, SearchHistoryEntry.class}, version = 1, exportSchema = false)
public abstract class AppDatabase extends RoomDatabase {
public static final String DATABASE_NAME = "newpipe.db";
public abstract SubscriptionDAO subscriptionDAO();
public abstract WatchHistoryDAO watchHistoryDAO();
public abstract SearchHistoryDAO searchHistoryDAO();
}

View File

@@ -0,0 +1,49 @@
package org.schabi.newpipe.database;
import android.arch.persistence.room.Dao;
import android.arch.persistence.room.Delete;
import android.arch.persistence.room.Insert;
import android.arch.persistence.room.OnConflictStrategy;
import android.arch.persistence.room.Update;
import java.util.Collection;
import java.util.List;
import io.reactivex.Flowable;
@Dao
public interface BasicDAO<Entity> {
/* Inserts */
@Insert(onConflict = OnConflictStrategy.FAIL)
long insert(final Entity entity);
@Insert(onConflict = OnConflictStrategy.FAIL)
List<Long> insertAll(final Entity... entities);
@Insert(onConflict = OnConflictStrategy.FAIL)
List<Long> insertAll(final Collection<Entity> entities);
@Insert(onConflict = OnConflictStrategy.REPLACE)
long upsert(final Entity entity);
/* Searches */
Flowable<List<Entity>> getAll();
Flowable<List<Entity>> listByService(int serviceId);
/* Deletes */
@Delete
int delete(final Entity entity);
@Delete
int delete(final Collection<Entity> entities);
int deleteAll();
/* Updates */
@Update
int update(final Entity entity);
@Update
int update(final Collection<Entity> entities);
}

View File

@@ -0,0 +1,28 @@
package org.schabi.newpipe.database.history;
import android.arch.persistence.room.TypeConverter;
import java.util.Date;
public class Converters {
/**
* Convert a long value to a date
* @param value the long value
* @return the date
*/
@TypeConverter
public static Date fromTimestamp(Long value) {
return value == null ? null : new Date(value);
}
/**
* Convert a date to a long value
* @param date the date
* @return the long value
*/
@TypeConverter
public static Long dateToTimestamp(Date date) {
return date == null ? null : date.getTime();
}
}

View File

@@ -0,0 +1,7 @@
package org.schabi.newpipe.database.history.dao;
import org.schabi.newpipe.database.BasicDAO;
public interface HistoryDAO<T> extends BasicDAO<T> {
T getLatestEntry();
}

View File

@@ -0,0 +1,37 @@
package org.schabi.newpipe.database.history.dao;
import android.arch.persistence.room.Dao;
import android.arch.persistence.room.Query;
import org.schabi.newpipe.database.history.model.SearchHistoryEntry;
import java.util.List;
import io.reactivex.Flowable;
import static org.schabi.newpipe.database.history.model.SearchHistoryEntry.CREATION_DATE;
import static org.schabi.newpipe.database.history.model.SearchHistoryEntry.ID;
import static org.schabi.newpipe.database.history.model.SearchHistoryEntry.SERVICE_ID;
import static org.schabi.newpipe.database.history.model.SearchHistoryEntry.TABLE_NAME;
@Dao
public interface SearchHistoryDAO extends HistoryDAO<SearchHistoryEntry> {
String ORDER_BY_CREATION_DATE = " ORDER BY " + CREATION_DATE + " DESC";
@Query("SELECT * FROM " + TABLE_NAME + " WHERE " + ID + " = (SELECT MAX(" + ID + ") FROM " + TABLE_NAME + ")")
@Override
SearchHistoryEntry getLatestEntry();
@Query("DELETE FROM " + TABLE_NAME)
@Override
int deleteAll();
@Query("SELECT * FROM " + TABLE_NAME + ORDER_BY_CREATION_DATE)
@Override
Flowable<List<SearchHistoryEntry>> getAll();
@Query("SELECT * FROM " + TABLE_NAME + " WHERE " + SERVICE_ID + " = :serviceId" + ORDER_BY_CREATION_DATE)
@Override
Flowable<List<SearchHistoryEntry>> listByService(int serviceId);
}

View File

@@ -0,0 +1,37 @@
package org.schabi.newpipe.database.history.dao;
import android.arch.persistence.room.Dao;
import android.arch.persistence.room.Query;
import org.schabi.newpipe.database.history.model.WatchHistoryEntry;
import java.util.List;
import io.reactivex.Flowable;
import static org.schabi.newpipe.database.history.model.WatchHistoryEntry.CREATION_DATE;
import static org.schabi.newpipe.database.history.model.WatchHistoryEntry.ID;
import static org.schabi.newpipe.database.history.model.WatchHistoryEntry.SERVICE_ID;
import static org.schabi.newpipe.database.history.model.WatchHistoryEntry.TABLE_NAME;
@Dao
public interface WatchHistoryDAO extends HistoryDAO<WatchHistoryEntry> {
String ORDER_BY_CREATION_DATE = " ORDER BY " + CREATION_DATE + " DESC";
@Query("SELECT * FROM " + TABLE_NAME + " WHERE " + ID + " = (SELECT MAX(" + ID + ") FROM " + TABLE_NAME + ")")
@Override
WatchHistoryEntry getLatestEntry();
@Query("DELETE FROM " + TABLE_NAME)
@Override
int deleteAll();
@Query("SELECT * FROM " + TABLE_NAME + ORDER_BY_CREATION_DATE)
@Override
Flowable<List<WatchHistoryEntry>> getAll();
@Query("SELECT * FROM " + TABLE_NAME + " WHERE " + SERVICE_ID + " = :serviceId" + ORDER_BY_CREATION_DATE)
@Override
Flowable<List<WatchHistoryEntry>> listByService(int serviceId);
}

View File

@@ -0,0 +1,60 @@
package org.schabi.newpipe.database.history.model;
import android.arch.persistence.room.ColumnInfo;
import android.arch.persistence.room.Entity;
import android.arch.persistence.room.Ignore;
import android.arch.persistence.room.PrimaryKey;
import java.util.Date;
@Entity
public abstract class HistoryEntry {
public static final String ID = "id";
public static final String SERVICE_ID = "service_id";
public static final String CREATION_DATE = "creation_date";
@ColumnInfo(name = CREATION_DATE)
private Date creationDate;
@ColumnInfo(name = SERVICE_ID)
private int serviceId;
@ColumnInfo(name = ID)
@PrimaryKey(autoGenerate = true)
private long id;
public HistoryEntry(Date creationDate, int serviceId) {
this.serviceId = serviceId;
this.creationDate = creationDate;
}
public long getId() {
return id;
}
public void setId(long id) {
this.id = id;
}
public Date getCreationDate() {
return creationDate;
}
public void setCreationDate(Date creationDate) {
this.creationDate = creationDate;
}
public int getServiceId() {
return serviceId;
}
public void setServiceId(int serviceId) {
this.serviceId = serviceId;
}
@Ignore
public boolean hasEqualValues(HistoryEntry otherEntry) {
return otherEntry != null && getServiceId() == otherEntry.getServiceId();
}
}

View File

@@ -0,0 +1,37 @@
package org.schabi.newpipe.database.history.model;
import android.arch.persistence.room.ColumnInfo;
import android.arch.persistence.room.Entity;
import android.arch.persistence.room.Ignore;
import java.util.Date;
@Entity(tableName = SearchHistoryEntry.TABLE_NAME)
public class SearchHistoryEntry extends HistoryEntry {
public static final String TABLE_NAME = "search_history";
public static final String SEARCH = "search";
@ColumnInfo(name = SEARCH)
private String search;
public SearchHistoryEntry(Date creationDate, int serviceId, String search) {
super(creationDate, serviceId);
this.search = search;
}
public String getSearch() {
return search;
}
public void setSearch(String search) {
this.search = search;
}
@Ignore
@Override
public boolean hasEqualValues(HistoryEntry otherEntry) {
return otherEntry instanceof SearchHistoryEntry && super.hasEqualValues(otherEntry)
&& getSearch().equals(((SearchHistoryEntry) otherEntry).getSearch());
}
}

View File

@@ -0,0 +1,109 @@
package org.schabi.newpipe.database.history.model;
import android.arch.persistence.room.ColumnInfo;
import android.arch.persistence.room.Entity;
import android.arch.persistence.room.Ignore;
import org.schabi.newpipe.extractor.stream.StreamInfo;
import java.util.Date;
@Entity(tableName = WatchHistoryEntry.TABLE_NAME)
public class WatchHistoryEntry extends HistoryEntry {
public static final String TABLE_NAME = "watch_history";
public static final String TITLE = "title";
public static final String URL = "url";
public static final String STREAM_ID = "stream_id";
public static final String THUMBNAIL_URL = "thumbnail_url";
public static final String UPLOADER = "uploader";
public static final String DURATION = "duration";
@ColumnInfo(name = TITLE)
private String title;
@ColumnInfo(name = URL)
private String url;
@ColumnInfo(name = STREAM_ID)
private String streamId;
@ColumnInfo(name = THUMBNAIL_URL)
private String thumbnailURL;
@ColumnInfo(name = UPLOADER)
private String uploader;
@ColumnInfo(name = DURATION)
private long duration;
public WatchHistoryEntry(Date creationDate, int serviceId, String title, String url, String streamId, String thumbnailURL, String uploader, long duration) {
super(creationDate, serviceId);
this.title = title;
this.url = url;
this.streamId = streamId;
this.thumbnailURL = thumbnailURL;
this.uploader = uploader;
this.duration = duration;
}
public WatchHistoryEntry(StreamInfo streamInfo) {
this(new Date(), streamInfo.service_id, streamInfo.name, streamInfo.url,
streamInfo.id, streamInfo.thumbnail_url, streamInfo.uploader_name, streamInfo.duration);
}
public String getUrl() {
return url;
}
public void setUrl(String url) {
this.url = url;
}
public String getTitle() {
return title;
}
public void setTitle(String title) {
this.title = title;
}
public String getStreamId() {
return streamId;
}
public void setStreamId(String streamId) {
this.streamId = streamId;
}
public String getThumbnailURL() {
return thumbnailURL;
}
public void setThumbnailURL(String thumbnailURL) {
this.thumbnailURL = thumbnailURL;
}
public String getUploader() {
return uploader;
}
public void setUploader(String uploader) {
this.uploader = uploader;
}
public long getDuration() {
return duration;
}
public void setDuration(int duration) {
this.duration = duration;
}
@Ignore
@Override
public boolean hasEqualValues(HistoryEntry otherEntry) {
return otherEntry instanceof WatchHistoryEntry && super.hasEqualValues(otherEntry)
&& getUrl().equals(((WatchHistoryEntry) otherEntry).getUrl());
}
}

View File

@@ -0,0 +1,34 @@
package org.schabi.newpipe.database.subscription;
import android.arch.persistence.room.Dao;
import android.arch.persistence.room.Query;
import org.schabi.newpipe.database.BasicDAO;
import java.util.List;
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_URL;
@Dao
public interface SubscriptionDAO extends BasicDAO<SubscriptionEntity> {
@Override
@Query("SELECT * FROM " + SUBSCRIPTION_TABLE)
Flowable<List<SubscriptionEntity>> getAll();
@Override
@Query("DELETE FROM " + SUBSCRIPTION_TABLE)
int deleteAll();
@Override
@Query("SELECT * FROM " + SUBSCRIPTION_TABLE + " WHERE " + SUBSCRIPTION_SERVICE_ID + " = :serviceId")
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);
}

View File

@@ -0,0 +1,127 @@
package org.schabi.newpipe.database.subscription;
import android.arch.persistence.room.ColumnInfo;
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 org.schabi.newpipe.extractor.channel.ChannelInfoItem;
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_URL;
@Entity(tableName = SUBSCRIPTION_TABLE,
indices = {@Index(value = {SUBSCRIPTION_SERVICE_ID, SUBSCRIPTION_URL}, unique = true)})
public class SubscriptionEntity {
final static String SUBSCRIPTION_TABLE = "subscriptions";
final static String SUBSCRIPTION_SERVICE_ID = "service_id";
final static String SUBSCRIPTION_URL = "url";
final static String SUBSCRIPTION_NAME = "name";
final static String SUBSCRIPTION_AVATAR_URL = "avatar_url";
final static String SUBSCRIPTION_SUBSCRIBER_COUNT = "subscriber_count";
final static String SUBSCRIPTION_DESCRIPTION = "description";
@PrimaryKey(autoGenerate = true)
private long uid = 0;
@ColumnInfo(name = SUBSCRIPTION_SERVICE_ID)
private int serviceId = -1;
@ColumnInfo(name = SUBSCRIPTION_URL)
private String url;
@ColumnInfo(name = SUBSCRIPTION_NAME)
private String name;
@ColumnInfo(name = SUBSCRIPTION_AVATAR_URL)
private String avatarUrl;
@ColumnInfo(name = SUBSCRIPTION_SUBSCRIBER_COUNT)
private Long subscriberCount;
@ColumnInfo(name = SUBSCRIPTION_DESCRIPTION)
private String description;
public long getUid() {
return uid;
}
/* Keep this package-private since UID should always be auto generated by Room impl */
void setUid(long uid) {
this.uid = uid;
}
public int getServiceId() {
return serviceId;
}
public void setServiceId(int serviceId) {
this.serviceId = serviceId;
}
public String getUrl() {
return url;
}
public void setUrl(String url) {
this.url = url;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getAvatarUrl() {
return avatarUrl;
}
public void setAvatarUrl(String avatarUrl) {
this.avatarUrl = avatarUrl;
}
public Long getSubscriberCount() {
return subscriberCount;
}
public void setSubscriberCount(Long subscriberCount) {
this.subscriberCount = subscriberCount;
}
public String getDescription() {
return description;
}
public void setDescription(String description) {
this.description = description;
}
@Ignore
public void setData(final String name,
final String avatarUrl,
final String description,
final Long subscriberCount) {
this.setName(name);
this.setAvatarUrl(avatarUrl);
this.setDescription(description);
this.setSubscriberCount(subscriberCount);
}
@Ignore
public ChannelInfoItem toChannelInfoItem() {
ChannelInfoItem item = new ChannelInfoItem();
item.url = getUrl();
item.service_id = getServiceId();
item.name = getName();
item.thumbnail_url = getAvatarUrl();
item.subscriber_count = getSubscriberCount();
item.description = getDescription();
return item;
}
}

View File

@@ -32,7 +32,7 @@ public class DownloadActivity extends AppCompatActivity {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_downloader);
Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
Toolbar toolbar = findViewById(R.id.toolbar);
setSupportActionBar(toolbar);
ActionBar actionBar = getSupportActionBar();

View File

@@ -22,14 +22,15 @@ import android.widget.TextView;
import org.schabi.newpipe.MainActivity;
import org.schabi.newpipe.R;
import org.schabi.newpipe.extractor.MediaFormat;
import org.schabi.newpipe.extractor.stream_info.AudioStream;
import org.schabi.newpipe.extractor.stream_info.StreamInfo;
import org.schabi.newpipe.extractor.stream_info.VideoStream;
import org.schabi.newpipe.extractor.stream.AudioStream;
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.ThemeHelper;
import org.schabi.newpipe.util.Utils;
import java.io.Serializable;
import java.util.ArrayList;
@@ -106,19 +107,19 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck
@Override
public void onViewCreated(View view, @Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
nameEditText = ((EditText) view.findViewById(R.id.file_name));
nameEditText.setText(createFileName(currentInfo.title));
selectedAudioIndex = Utils.getPreferredAudioFormat(getContext(), currentInfo.audio_streams);
nameEditText = view.findViewById(R.id.file_name);
nameEditText.setText(FilenameUtils.createFilename(getContext(), currentInfo.name));
selectedAudioIndex = ListHelper.getDefaultAudioFormat(getContext(), currentInfo.audio_streams);
streamsSpinner = (Spinner) view.findViewById(R.id.quality_spinner);
streamsSpinner = view.findViewById(R.id.quality_spinner);
streamsSpinner.setOnItemSelectedListener(this);
threadsCountTextView = (TextView) view.findViewById(R.id.threads_count);
threadsSeekBar = (SeekBar) view.findViewById(R.id.threads);
radioVideoAudioGroup = (RadioGroup) view.findViewById(R.id.video_audio_group);
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((Toolbar) view.findViewById(R.id.toolbar));
initToolbar(view.<Toolbar>findViewById(R.id.toolbar));
checkDownloadOptions(view);
setupVideoSpinner(sortedStreamVideosList, streamsSpinner);
@@ -134,12 +135,10 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck
@Override
public void onStartTrackingTouch(SeekBar p1) {
}
@Override
public void onStopTrackingTouch(SeekBar p1) {
}
});
}
@@ -184,7 +183,7 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck
String[] items = new String[audioStreams.size()];
for (int i = 0; i < audioStreams.size(); i++) {
AudioStream audioStream = audioStreams.get(i);
items[i] = MediaFormat.getNameById(audioStream.format) + " " + audioStream.avgBitrate + "kbps";
items[i] = MediaFormat.getNameById(audioStream.format) + " " + audioStream.average_bitrate + "kbps";
}
ArrayAdapter<String> itemAdapter = new ArrayAdapter<>(getContext(), android.R.layout.simple_spinner_dropdown_item, items);
@@ -240,8 +239,8 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck
//////////////////////////////////////////////////////////////////////////*/
protected void checkDownloadOptions(View view) {
RadioButton audioButton = (RadioButton) view.findViewById(R.id.audio_button);
RadioButton videoButton = (RadioButton) view.findViewById(R.id.video_button);
RadioButton audioButton = view.findViewById(R.id.audio_button);
RadioButton videoButton = view.findViewById(R.id.video_button);
if (currentInfo.audio_streams == null || currentInfo.audio_streams.size() == 0) {
audioButton.setVisibility(View.GONE);
@@ -252,30 +251,12 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck
}
}
/**
* #143 #44 #42 #22: make sure that the filename does not contain illegal chars.
* This should fix some of the "cannot download" problems.
*/
private String createFileName(String fileName) {
// from http://eng-przemelek.blogspot.de/2009/07/how-to-create-valid-file-name.html
List<String> forbiddenCharsPatterns = new ArrayList<>();
forbiddenCharsPatterns.add("[:]+"); // Mac OS, but it looks that also Windows XP
forbiddenCharsPatterns.add("[\\*\"/\\\\\\[\\]\\:\\;\\|\\=\\,]+"); // Windows
forbiddenCharsPatterns.add("[^\\w\\d\\.]+"); // last chance... only latin letters and digits
String nameToTest = fileName;
for (String pattern : forbiddenCharsPatterns) {
nameToTest = nameToTest.replaceAll(pattern, "_");
}
return nameToTest;
}
private void downloadSelected() {
String url, location;
String fileName = nameEditText.getText().toString().trim();
if (fileName.isEmpty()) fileName = createFileName(currentInfo.title);
if (fileName.isEmpty()) fileName = FilenameUtils.createFilename(getContext(), currentInfo.name);
boolean isAudio = radioVideoAudioGroup.getCheckedRadioButtonId() == R.id.audio_button;
url = isAudio ? currentInfo.audio_streams.get(selectedAudioIndex).url : sortedStreamVideosList.get(selectedVideoIndex).url;

View File

@@ -0,0 +1,13 @@
package org.schabi.newpipe.fragments;
/**
* Indicates that the current fragment can handle back presses
*/
public interface BackPressable {
/**
* A back press was delegated to this fragment
*
* @return if the back press was handled
*/
boolean onBackPressed();
}

View File

@@ -1,177 +0,0 @@
package org.schabi.newpipe.fragments;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Rect;
import android.os.Bundle;
import android.support.annotation.AttrRes;
import android.support.v4.app.Fragment;
import android.support.v4.view.ViewCompat;
import android.support.v7.app.AppCompatActivity;
import android.support.v7.widget.Toolbar;
import android.util.Log;
import android.view.Gravity;
import android.view.View;
import android.widget.Button;
import android.widget.ProgressBar;
import android.widget.TextView;
import android.widget.Toast;
import com.nostra13.universalimageloader.core.DisplayImageOptions;
import com.nostra13.universalimageloader.core.ImageLoader;
import com.nostra13.universalimageloader.core.display.FadeInBitmapDisplayer;
import org.schabi.newpipe.MainActivity;
import org.schabi.newpipe.R;
import java.util.concurrent.atomic.AtomicBoolean;
import static org.schabi.newpipe.util.AnimationUtils.animateView;
public abstract class BaseFragment extends Fragment {
protected final String TAG = "BaseFragment@" + Integer.toHexString(hashCode());
protected static final boolean DEBUG = MainActivity.DEBUG;
protected AppCompatActivity activity;
protected AtomicBoolean isLoading = new AtomicBoolean(false);
protected AtomicBoolean wasLoading = new AtomicBoolean(false);
protected static final ImageLoader imageLoader = ImageLoader.getInstance();
protected static final DisplayImageOptions displayImageOptions =
new DisplayImageOptions.Builder().displayer(new FadeInBitmapDisplayer(400)).cacheInMemory(false).build();
/*//////////////////////////////////////////////////////////////////////////
// Views
//////////////////////////////////////////////////////////////////////////*/
protected Toolbar toolbar;
protected View errorPanel;
protected Button errorButtonRetry;
protected TextView errorTextView;
protected ProgressBar loadingProgressBar;
//protected SwipeRefreshLayout swipeRefreshLayout;
/*//////////////////////////////////////////////////////////////////////////
// Fragment's Lifecycle
//////////////////////////////////////////////////////////////////////////*/
@Override
public void onAttach(Context context) {
super.onAttach(context);
if (DEBUG) Log.d(TAG, "onAttach() called with: context = [" + context + "]");
activity = (AppCompatActivity) context;
}
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
if (DEBUG) Log.d(TAG, "onCreate() called with: savedInstanceState = [" + savedInstanceState + "]");
isLoading.set(false);
setHasOptionsMenu(true);
}
@Override
public void onViewCreated(View rootView, Bundle savedInstanceState) {
if (DEBUG) Log.d(TAG, "onViewCreated() called with: rootView = [" + rootView + "], savedInstanceState = [" + savedInstanceState + "]");
initViews(rootView, savedInstanceState);
initListeners();
wasLoading.set(false);
}
@Override
public void onDestroyView() {
super.onDestroyView();
if (DEBUG) Log.d(TAG, "onDestroyView() called");
toolbar = null;
errorPanel = null;
errorButtonRetry = null;
errorTextView = null;
}
/*//////////////////////////////////////////////////////////////////////////
// Init
//////////////////////////////////////////////////////////////////////////*/
protected void initViews(View rootView, Bundle savedInstanceState) {
toolbar = (Toolbar) activity.findViewById(R.id.toolbar);
loadingProgressBar = (ProgressBar) rootView.findViewById(R.id.loading_progress_bar);
//swipeRefreshLayout = (SwipeRefreshLayout) rootView.findViewById(R.id.swipe_refresh);
errorPanel = rootView.findViewById(R.id.error_panel);
errorButtonRetry = (Button) rootView.findViewById(R.id.error_button_retry);
errorTextView = (TextView) rootView.findViewById(R.id.error_message_view);
}
protected void initListeners() {
errorButtonRetry.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
onRetryButtonClicked();
}
});
}
protected abstract void reloadContent();
protected void onRetryButtonClicked() {
if (DEBUG) Log.d(TAG, "onRetryButtonClicked() called");
reloadContent();
}
/*//////////////////////////////////////////////////////////////////////////
// Utils
//////////////////////////////////////////////////////////////////////////*/
protected void setErrorMessage(String message, boolean showRetryButton) {
if (errorTextView == null || activity == null) return;
errorTextView.setText(message);
if (showRetryButton) animateView(errorButtonRetry, true, 300);
else animateView(errorButtonRetry, false, 0);
animateView(errorPanel, true, 300);
isLoading.set(false);
animateView(loadingProgressBar, false, 200);
}
protected int getResourceIdFromAttr(@AttrRes int attr) {
TypedArray a = activity.getTheme().obtainStyledAttributes(new int[]{attr});
int attributeResourceId = a.getResourceId(0, 0);
a.recycle();
return attributeResourceId;
}
public static void showMenuTooltip(View v, String message) {
final int[] screenPos = new int[2];
final Rect displayFrame = new Rect();
v.getLocationOnScreen(screenPos);
v.getWindowVisibleDisplayFrame(displayFrame);
final Context context = v.getContext();
final int width = v.getWidth();
final int height = v.getHeight();
final int midy = screenPos[1] + height / 2;
int referenceX = screenPos[0] + width / 2;
if (ViewCompat.getLayoutDirection(v) == View.LAYOUT_DIRECTION_LTR) {
final int screenWidth = context.getResources().getDisplayMetrics().widthPixels;
referenceX = screenWidth - referenceX; // mirror
}
Toast cheatSheet = Toast.makeText(context, message, Toast.LENGTH_SHORT);
if (midy < displayFrame.height()) {
// Show along the top; follow action buttons
cheatSheet.setGravity(Gravity.TOP | Gravity.END, referenceX,
screenPos[1] + height - displayFrame.top);
} else {
// Show along the bottom center
cheatSheet.setGravity(Gravity.BOTTOM | Gravity.CENTER_HORIZONTAL, 0, height);
}
cheatSheet.show();
}
}

View File

@@ -0,0 +1,235 @@
package org.schabi.newpipe.fragments;
import android.content.Intent;
import android.os.Bundle;
import android.support.annotation.Nullable;
import android.support.annotation.StringRes;
import android.util.Log;
import android.view.View;
import android.widget.Button;
import android.widget.ProgressBar;
import android.widget.TextView;
import android.widget.Toast;
import com.jakewharton.rxbinding2.view.RxView;
import org.schabi.newpipe.BaseFragment;
import org.schabi.newpipe.MainActivity;
import org.schabi.newpipe.R;
import org.schabi.newpipe.ReCaptchaActivity;
import org.schabi.newpipe.extractor.exceptions.ReCaptchaException;
import org.schabi.newpipe.report.ErrorActivity;
import org.schabi.newpipe.report.UserAction;
import org.schabi.newpipe.util.ExtractorHelper;
import org.schabi.newpipe.util.InfoCache;
import java.io.IOException;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import icepick.State;
import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.functions.Consumer;
import static org.schabi.newpipe.util.AnimationUtils.animateView;
public abstract class BaseStateFragment<I> extends BaseFragment implements ViewContract<I> {
@State
protected AtomicBoolean wasLoading = new AtomicBoolean();
protected AtomicBoolean isLoading = new AtomicBoolean();
@Nullable
protected View emptyStateView;
@Nullable
protected ProgressBar loadingProgressBar;
protected View errorPanelRoot;
protected Button errorButtonRetry;
protected TextView errorTextView;
@Override
public void onViewCreated(View rootView, Bundle savedInstanceState) {
super.onViewCreated(rootView, savedInstanceState);
doInitialLoadLogic();
}
@Override
public void onPause() {
super.onPause();
wasLoading.set(isLoading.get());
}
/*//////////////////////////////////////////////////////////////////////////
// Init
//////////////////////////////////////////////////////////////////////////*/
@Override
protected void initViews(View rootView, Bundle savedInstanceState) {
super.initViews(rootView, savedInstanceState);
emptyStateView = rootView.findViewById(R.id.empty_state_view);
loadingProgressBar = rootView.findViewById(R.id.loading_progress_bar);
errorPanelRoot = rootView.findViewById(R.id.error_panel);
errorButtonRetry = rootView.findViewById(R.id.error_button_retry);
errorTextView = rootView.findViewById(R.id.error_message_view);
}
@Override
protected void initListeners() {
super.initListeners();
RxView.clicks(errorButtonRetry)
.debounce(300, TimeUnit.MILLISECONDS)
.observeOn(AndroidSchedulers.mainThread())
.subscribe(new Consumer<Object>() {
@Override
public void accept(Object o) throws Exception {
onRetryButtonClicked();
}
});
}
protected void onRetryButtonClicked() {
reloadContent();
}
public void reloadContent() {
startLoading(true);
}
/*//////////////////////////////////////////////////////////////////////////
// Load
//////////////////////////////////////////////////////////////////////////*/
protected void doInitialLoadLogic() {
startLoading(true);
}
protected void startLoading(boolean forceLoad) {
if (DEBUG) Log.d(TAG, "startLoading() called with: forceLoad = [" + forceLoad + "]");
showLoading();
isLoading.set(true);
}
/*//////////////////////////////////////////////////////////////////////////
// Contract
//////////////////////////////////////////////////////////////////////////*/
@Override
public void showLoading() {
if (emptyStateView != null) animateView(emptyStateView, false, 150);
if (loadingProgressBar != null) animateView(loadingProgressBar, true, 400);
animateView(errorPanelRoot, false, 150);
}
@Override
public void hideLoading() {
if (emptyStateView != null) animateView(emptyStateView, false, 150);
if (loadingProgressBar != null) animateView(loadingProgressBar, false, 0);
animateView(errorPanelRoot, false, 150);
}
@Override
public void showEmptyState() {
isLoading.set(false);
if (emptyStateView != null) animateView(emptyStateView, true, 200);
if (loadingProgressBar != null) animateView(loadingProgressBar, false, 0);
animateView(errorPanelRoot, false, 150);
}
@Override
public void showError(String message, boolean showRetryButton) {
if (DEBUG) Log.d(TAG, "showError() called with: message = [" + message + "], showRetryButton = [" + showRetryButton + "]");
isLoading.set(false);
InfoCache.getInstance().clearCache();
hideLoading();
errorTextView.setText(message);
if (showRetryButton) animateView(errorButtonRetry, true, 600);
else animateView(errorButtonRetry, false, 0);
animateView(errorPanelRoot, true, 300);
}
@Override
public void handleResult(I result) {
if (DEBUG) Log.d(TAG, "handleResult() called with: result = [" + result + "]");
hideLoading();
}
/*//////////////////////////////////////////////////////////////////////////
// Error handling
//////////////////////////////////////////////////////////////////////////*/
/**
* Default implementation handles some general exceptions
*
* @return if the exception was handled
*/
protected boolean onError(Throwable exception) {
if (DEBUG) Log.d(TAG, "onError() called with: exception = [" + exception + "]");
isLoading.set(false);
if (isDetached() || isRemoving()) {
if (DEBUG) Log.w(TAG, "onError() is detached or removing = [" + exception + "]");
return true;
}
if (ExtractorHelper.isInterruptedCaused(exception)) {
if (DEBUG) Log.w(TAG, "onError() isInterruptedCaused! = [" + exception + "]");
return true;
}
if (exception instanceof ReCaptchaException) {
onReCaptchaException();
return true;
} else if (exception instanceof IOException) {
showError(getString(R.string.network_error), true);
return true;
}
return false;
}
public void onReCaptchaException() {
if (DEBUG) Log.d(TAG, "onReCaptchaException() called");
Toast.makeText(activity, R.string.recaptcha_request_toast, Toast.LENGTH_LONG).show();
// Starting ReCaptcha Challenge Activity
startActivityForResult(new Intent(activity, ReCaptchaActivity.class), ReCaptchaActivity.RECAPTCHA_REQUEST);
showError(getString(R.string.recaptcha_request_toast), false);
}
public void onUnrecoverableError(Throwable exception, UserAction userAction, String serviceName, String request, @StringRes int errorId) {
onUnrecoverableError(Collections.singletonList(exception), userAction, serviceName, request, errorId);
}
public void onUnrecoverableError(List<Throwable> exception, UserAction userAction, String serviceName, String request, @StringRes int errorId) {
if (DEBUG) Log.d(TAG, "onUnrecoverableError() called with: exception = [" + exception + "]");
if (serviceName == null) serviceName = "none";
if (request == null) request = "none";
ErrorActivity.reportError(getContext(), exception, MainActivity.class, null, ErrorActivity.ErrorInfo.make(userAction, serviceName, request, errorId));
}
public void showSnackBarError(Throwable exception, UserAction userAction, String serviceName, String request, @StringRes int errorId) {
showSnackBarError(Collections.singletonList(exception), userAction, serviceName, request, errorId);
}
/**
* Show a SnackBar and only call ErrorActivity#reportError IF we a find a valid view (otherwise the error screen appears)
*/
public void showSnackBarError(List<Throwable> exception, UserAction userAction, String serviceName, String request, @StringRes int errorId) {
if (DEBUG) {
Log.d(TAG, "showSnackBarError() called with: exception = [" + exception + "], userAction = [" + userAction + "], request = [" + request + "], errorId = [" + errorId + "]");
}
View rootView = activity != null ? activity.findViewById(android.R.id.content) : null;
if (rootView == null && getView() != null) rootView = getView();
if (rootView == null) return;
ErrorActivity.reportError(getContext(), exception, MainActivity.class, rootView, ErrorActivity.ErrorInfo.make(userAction, serviceName, request, errorId));
}
}

View File

@@ -0,0 +1,18 @@
package org.schabi.newpipe.fragments;
import android.os.Bundle;
import android.support.annotation.Nullable;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import org.schabi.newpipe.BaseFragment;
import org.schabi.newpipe.R;
public class BlankFragment extends BaseFragment {
@Nullable
@Override
public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, Bundle savedInstanceState) {
return inflater.inflate(R.layout.fragment_blank, container, false);
}
}

View File

@@ -1,11 +1,13 @@
package org.schabi.newpipe.fragments;
import android.content.Context;
import android.os.Bundle;
import android.support.annotation.Nullable;
import android.support.design.widget.TabLayout;
import android.support.v4.app.Fragment;
import android.support.v4.app.FragmentManager;
import android.support.v4.app.FragmentPagerAdapter;
import android.support.v4.view.ViewPager;
import android.support.v7.app.ActionBar;
import android.support.v7.app.AppCompatActivity;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.Menu;
@@ -14,40 +16,44 @@ import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import org.schabi.newpipe.MainActivity;
import org.schabi.newpipe.BaseFragment;
import org.schabi.newpipe.R;
import org.schabi.newpipe.fragments.subscription.SubscriptionFragment;
import org.schabi.newpipe.util.NavigationHelper;
public class MainFragment extends Fragment {
private final String TAG = "MainFragment@" + Integer.toHexString(hashCode());
private static final boolean DEBUG = MainActivity.DEBUG;
private AppCompatActivity activity;
public class MainFragment extends BaseFragment implements TabLayout.OnTabSelectedListener {
private ViewPager viewPager;
/*//////////////////////////////////////////////////////////////////////////
// Fragment's LifeCycle
//////////////////////////////////////////////////////////////////////////*/
@Override
public void onAttach(Context context) {
super.onAttach(context);
if (DEBUG) Log.d(TAG, "onAttach() called with: context = [" + context + "]");
activity = ((AppCompatActivity) context);
}
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
if (DEBUG) Log.d(TAG, "onCreate() called with: savedInstanceState = [" + savedInstanceState + "]");
setHasOptionsMenu(true);
}
@Override
public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
if (DEBUG) Log.d(TAG, "onCreateView() called with: inflater = [" + inflater + "], container = [" + container + "], savedInstanceState = [" + savedInstanceState + "]");
return inflater.inflate(R.layout.fragment_main, container, false);
}
@Override
protected void initViews(View rootView, Bundle savedInstanceState) {
super.initViews(rootView, savedInstanceState);
TabLayout tabLayout = rootView.findViewById(R.id.main_tab_layout);
viewPager = rootView.findViewById(R.id.pager);
/* Nested fragment, use child fragment here to maintain backstack in view pager. */
PagerAdapter adapter = new PagerAdapter(getChildFragmentManager());
viewPager.setAdapter(adapter);
viewPager.setOffscreenPageLimit(adapter.getCount());
tabLayout.setupWithViewPager(viewPager);
}
/*//////////////////////////////////////////////////////////////////////////
// Menu
//////////////////////////////////////////////////////////////////////////*/
@@ -74,4 +80,53 @@ public class MainFragment extends Fragment {
}
return super.onOptionsItemSelected(item);
}
/*//////////////////////////////////////////////////////////////////////////
// Tabs
//////////////////////////////////////////////////////////////////////////*/
@Override
public void onTabSelected(TabLayout.Tab tab) {
viewPager.setCurrentItem(tab.getPosition());
}
@Override
public void onTabUnselected(TabLayout.Tab tab) {
}
@Override
public void onTabReselected(TabLayout.Tab tab) {
}
private class PagerAdapter extends FragmentPagerAdapter {
private int[] tabTitles = new int[]{
R.string.tab_main,
R.string.tab_subscriptions
};
PagerAdapter(FragmentManager fm) {
super(fm);
}
@Override
public Fragment getItem(int position) {
switch (position) {
case 1:
return new SubscriptionFragment();
default:
return new BlankFragment();
}
}
@Override
public CharSequence getPageTitle(int position) {
return getString(this.tabTitles[position]);
}
@Override
public int getCount() {
return this.tabTitles.length;
}
}
}

View File

@@ -0,0 +1,43 @@
package org.schabi.newpipe.fragments;
import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView;
import android.support.v7.widget.StaggeredGridLayoutManager;
/**
* Recycler view scroll listener which calls the method {@link #onScrolledDown(RecyclerView)}
* if the view is scrolled below the last item.
*/
public abstract class OnScrollBelowItemsListener extends RecyclerView.OnScrollListener {
@Override
public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
super.onScrolled(recyclerView, dx, dy);
if (dy > 0) {
int pastVisibleItems = 0, visibleItemCount, totalItemCount;
RecyclerView.LayoutManager layoutManager = recyclerView.getLayoutManager();
visibleItemCount = layoutManager.getChildCount();
totalItemCount = layoutManager.getItemCount();
// Already covers the GridLayoutManager case
if (layoutManager instanceof LinearLayoutManager) {
pastVisibleItems = ((LinearLayoutManager) layoutManager).findFirstVisibleItemPosition();
} else if (layoutManager instanceof StaggeredGridLayoutManager) {
int[] positions = ((StaggeredGridLayoutManager) layoutManager).findFirstVisibleItemPositions(null);
if (positions != null && positions.length > 0) pastVisibleItems = positions[0];
}
if ((visibleItemCount + pastVisibleItems) >= totalItemCount) {
onScrolledDown(recyclerView);
}
}
}
/**
* Called when the recycler view is scrolled below the last item.
*
* @param recyclerView the recycler view
*/
public abstract void onScrolledDown(RecyclerView recyclerView);
}

View File

@@ -0,0 +1,10 @@
package org.schabi.newpipe.fragments;
public interface ViewContract<I> {
void showLoading();
void hideLoading();
void showEmptyState();
void showError(String message, boolean showRetryButton);
void handleResult(I result);
}

View File

@@ -1,403 +0,0 @@
package org.schabi.newpipe.fragments.channel;
import android.content.Intent;
import android.net.Uri;
import android.os.Bundle;
import android.support.annotation.Nullable;
import android.support.v4.app.Fragment;
import android.support.v4.content.ContextCompat;
import android.support.v7.app.ActionBar;
import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView;
import android.text.TextUtils;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Button;
import android.widget.ImageView;
import android.widget.TextView;
import org.schabi.newpipe.ImageErrorLoadingListener;
import org.schabi.newpipe.R;
import org.schabi.newpipe.extractor.InfoItem;
import org.schabi.newpipe.extractor.channel.ChannelInfo;
import org.schabi.newpipe.fragments.BaseFragment;
import org.schabi.newpipe.fragments.search.OnScrollBelowItemsListener;
import org.schabi.newpipe.info_list.InfoItemBuilder;
import org.schabi.newpipe.info_list.InfoListAdapter;
import org.schabi.newpipe.util.Constants;
import org.schabi.newpipe.util.NavigationHelper;
import org.schabi.newpipe.workers.ChannelExtractorWorker;
import java.io.Serializable;
import java.text.NumberFormat;
import java.util.ArrayList;
import static org.schabi.newpipe.util.AnimationUtils.animateView;
public class ChannelFragment extends BaseFragment implements ChannelExtractorWorker.OnChannelInfoReceive {
private final String TAG = "ChannelFragment@" + Integer.toHexString(hashCode());
private static final String INFO_LIST_KEY = "info_list_key";
private static final String CHANNEL_INFO_KEY = "channel_info_key";
private static final String PAGE_NUMBER_KEY = "page_number_key";
private InfoListAdapter infoListAdapter;
private ChannelExtractorWorker currentChannelWorker;
private ChannelInfo currentChannelInfo;
private int serviceId = -1;
private String channelName = "";
private String channelUrl = "";
private int pageNumber = 0;
private boolean hasNextPage = true;
/*//////////////////////////////////////////////////////////////////////////
// Views
//////////////////////////////////////////////////////////////////////////*/
private RecyclerView channelVideosList;
private View headerRootLayout;
private ImageView headerChannelBanner;
private ImageView headerAvatarView;
private TextView headerTitleView;
private TextView headerSubscribersTextView;
private Button headerRssButton;
/*////////////////////////////////////////////////////////////////////////*/
public ChannelFragment() {
}
public static Fragment getInstance(int serviceId, String channelUrl, String name) {
ChannelFragment instance = new ChannelFragment();
instance.setChannel(serviceId, channelUrl, name);
return instance;
}
/*//////////////////////////////////////////////////////////////////////////
// Fragment's LifeCycle
//////////////////////////////////////////////////////////////////////////*/
@Override
public void onCreate(Bundle savedInstanceState) {
if (DEBUG) Log.d(TAG, "onCreate() called with: savedInstanceState = [" + savedInstanceState + "]");
super.onCreate(savedInstanceState);
setHasOptionsMenu(true);
if (savedInstanceState != null) {
channelUrl = savedInstanceState.getString(Constants.KEY_URL);
channelName = savedInstanceState.getString(Constants.KEY_TITLE);
serviceId = savedInstanceState.getInt(Constants.KEY_SERVICE_ID, -1);
pageNumber = savedInstanceState.getInt(PAGE_NUMBER_KEY, 0);
Serializable serializable = savedInstanceState.getSerializable(CHANNEL_INFO_KEY);
if (serializable instanceof ChannelInfo) currentChannelInfo = (ChannelInfo) serializable;
}
}
@Override
public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
if (DEBUG) Log.d(TAG, "onCreateView() called with: inflater = [" + inflater + "], container = [" + container + "], savedInstanceState = [" + savedInstanceState + "]");
return inflater.inflate(R.layout.fragment_channel, container, false);
}
@Override
public void onViewCreated(View view, @Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
if (currentChannelInfo == null) loadPage(0);
else handleChannelInfo(currentChannelInfo, false, false);
}
@Override
public void onDestroyView() {
if (DEBUG) Log.d(TAG, "onDestroyView() called");
headerAvatarView.setImageBitmap(null);
headerChannelBanner.setImageBitmap(null);
channelVideosList.removeAllViews();
channelVideosList = null;
headerRootLayout = null;
headerChannelBanner = null;
headerAvatarView = null;
headerTitleView = null;
headerSubscribersTextView = null;
headerRssButton = null;
super.onDestroyView();
}
@Override
public void onResume() {
if (DEBUG) Log.d(TAG, "onResume() called");
super.onResume();
if (wasLoading.getAndSet(false) && (currentChannelWorker == null || !currentChannelWorker.isRunning())) {
loadPage(pageNumber);
}
}
@Override
public void onStop() {
if (DEBUG) Log.d(TAG, "onStop() called");
super.onStop();
wasLoading.set(currentChannelWorker != null && currentChannelWorker.isRunning());
if (currentChannelWorker != null && currentChannelWorker.isRunning()) currentChannelWorker.cancel();
}
@Override
public void onSaveInstanceState(Bundle outState) {
if (DEBUG) Log.d(TAG, "onSaveInstanceState() called with: outState = [" + outState + "]");
super.onSaveInstanceState(outState);
outState.putString(Constants.KEY_URL, channelUrl);
outState.putString(Constants.KEY_TITLE, channelName);
outState.putInt(Constants.KEY_SERVICE_ID, serviceId);
outState.putSerializable(INFO_LIST_KEY, ((ArrayList<InfoItem>) infoListAdapter.getItemsList()));
outState.putSerializable(CHANNEL_INFO_KEY, currentChannelInfo);
outState.putInt(PAGE_NUMBER_KEY, pageNumber);
}
/*//////////////////////////////////////////////////////////////////////////
// Menu
//////////////////////////////////////////////////////////////////////////*/
@Override
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
if (DEBUG) Log.d(TAG, "onCreateOptionsMenu() called with: menu = [" + menu + "], inflater = [" + inflater + "]");
super.onCreateOptionsMenu(menu, inflater);
inflater.inflate(R.menu.menu_channel, menu);
ActionBar supportActionBar = activity.getSupportActionBar();
if (supportActionBar != null) {
supportActionBar.setDisplayShowTitleEnabled(true);
supportActionBar.setDisplayHomeAsUpEnabled(true);
}
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
if (DEBUG) Log.d(TAG, "onOptionsItemSelected() called with: item = [" + item + "]");
super.onOptionsItemSelected(item);
switch (item.getItemId()) {
case R.id.menu_item_openInBrowser: {
Intent intent = new Intent();
intent.setAction(Intent.ACTION_VIEW);
intent.setData(Uri.parse(channelUrl));
startActivity(Intent.createChooser(intent, getString(R.string.choose_browser)));
return true;
}
case R.id.menu_item_share:
Intent intent = new Intent();
intent.setAction(Intent.ACTION_SEND);
intent.putExtra(Intent.EXTRA_TEXT, channelUrl);
intent.setType("text/plain");
startActivity(Intent.createChooser(intent, getString(R.string.share_dialog_title)));
return true;
default:
return super.onOptionsItemSelected(item);
}
}
/*//////////////////////////////////////////////////////////////////////////
// Init's
//////////////////////////////////////////////////////////////////////////*/
@Override
protected void initViews(View rootView, Bundle savedInstanceState) {
super.initViews(rootView, savedInstanceState);
channelVideosList = (RecyclerView) rootView.findViewById(R.id.channel_streams_view);
channelVideosList.setLayoutManager(new LinearLayoutManager(activity));
if (infoListAdapter == null) {
infoListAdapter = new InfoListAdapter(activity);
if (savedInstanceState != null) {
//noinspection unchecked
ArrayList<InfoItem> serializable = (ArrayList<InfoItem>) savedInstanceState.getSerializable(INFO_LIST_KEY);
infoListAdapter.addInfoItemList(serializable);
}
}
channelVideosList.setAdapter(infoListAdapter);
headerRootLayout = activity.getLayoutInflater().inflate(R.layout.channel_header, channelVideosList, false);
infoListAdapter.setHeader(headerRootLayout);
infoListAdapter.setFooter(activity.getLayoutInflater().inflate(R.layout.pignate_footer, channelVideosList, false));
headerChannelBanner = (ImageView) headerRootLayout.findViewById(R.id.channel_banner_image);
headerAvatarView = (ImageView) headerRootLayout.findViewById(R.id.channel_avatar_view);
headerTitleView = (TextView) headerRootLayout.findViewById(R.id.channel_title_view);
headerSubscribersTextView = (TextView) headerRootLayout.findViewById(R.id.channel_subscriber_view);
headerRssButton = (Button) headerRootLayout.findViewById(R.id.channel_rss_button);
}
protected void initListeners() {
super.initListeners();
infoListAdapter.setOnStreamInfoItemSelectedListener(new InfoItemBuilder.OnInfoItemSelectedListener() {
@Override
public void selected(int serviceId, String url, String title) {
if (DEBUG) Log.d(TAG, "selected() called with: serviceId = [" + serviceId + "], url = [" + url + "], title = [" + title + "]");
NavigationHelper.openVideoDetailFragment(getFragmentManager(), serviceId, url, title);
}
});
channelVideosList.clearOnScrollListeners();
channelVideosList.addOnScrollListener(new OnScrollBelowItemsListener() {
@Override
public void onScrolledDown(RecyclerView recyclerView) {
if ((currentChannelWorker == null || !currentChannelWorker.isRunning()) && hasNextPage && !isLoading.get()) {
pageNumber++;
loadMoreVideos();
}
}
});
headerRssButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
if (DEBUG) Log.d(TAG, "onClick() called with: view = [" + view + "] feed url > " + currentChannelInfo.feed_url);
Intent i = new Intent(Intent.ACTION_VIEW, Uri.parse(currentChannelInfo.feed_url));
startActivity(i);
}
});
}
@Override
protected void reloadContent() {
if (DEBUG) Log.d(TAG, "reloadContent() called");
currentChannelInfo = null;
infoListAdapter.clearStreamItemList();
loadPage(0);
}
/*//////////////////////////////////////////////////////////////////////////
// Utils
//////////////////////////////////////////////////////////////////////////*/
private String buildSubscriberString(long count) {
String out = NumberFormat.getNumberInstance().format(count);
out += " " + getString(count > 1 ? R.string.subscriber_plural : R.string.subscriber);
return out;
}
private void loadPage(int page) {
if (DEBUG) Log.d(TAG, "loadPage() called with: page = [" + page + "]");
if (currentChannelWorker != null && currentChannelWorker.isRunning()) currentChannelWorker.cancel();
isLoading.set(true);
pageNumber = page;
infoListAdapter.showFooter(false);
animateView(loadingProgressBar, true, 200);
animateView(errorPanel, false, 200);
imageLoader.cancelDisplayTask(headerChannelBanner);
imageLoader.cancelDisplayTask(headerAvatarView);
headerRssButton.setVisibility(View.GONE);
headerSubscribersTextView.setVisibility(View.GONE);
headerTitleView.setText(channelName != null ? channelName : "");
headerChannelBanner.setImageDrawable(ContextCompat.getDrawable(activity, R.drawable.channel_banner));
headerAvatarView.setImageDrawable(ContextCompat.getDrawable(activity, R.drawable.buddy));
if (activity.getSupportActionBar() != null) activity.getSupportActionBar().setTitle(channelName != null ? channelName : "");
currentChannelWorker = new ChannelExtractorWorker(activity, serviceId, channelUrl, page, false, this);
currentChannelWorker.start();
}
private void loadMoreVideos() {
if (DEBUG) Log.d(TAG, "loadMoreVideos() called");
if (currentChannelWorker != null && currentChannelWorker.isRunning()) currentChannelWorker.cancel();
isLoading.set(true);
currentChannelWorker = new ChannelExtractorWorker(activity, serviceId, channelUrl, pageNumber, true, this);
currentChannelWorker.start();
}
private void setChannel(int serviceId, String channelUrl, String name) {
this.serviceId = serviceId;
this.channelUrl = channelUrl;
this.channelName = name;
}
private void handleChannelInfo(ChannelInfo info, boolean onlyVideos, boolean addVideos) {
currentChannelInfo = info;
animateView(errorPanel, false, 300);
animateView(channelVideosList, true, 200);
animateView(loadingProgressBar, false, 200);
if (!onlyVideos) {
headerRootLayout.setVisibility(View.VISIBLE);
//animateView(loadingProgressBar, false, 200, null);
if (!TextUtils.isEmpty(info.channel_name)) {
if (activity.getSupportActionBar() != null) activity.getSupportActionBar().setTitle(info.channel_name);
headerTitleView.setText(info.channel_name);
channelName = info.channel_name;
} else channelName = "";
if (!TextUtils.isEmpty(info.banner_url)) {
imageLoader.displayImage(info.banner_url, headerChannelBanner, displayImageOptions, new ImageErrorLoadingListener(activity, getView(), info.service_id));
}
if (!TextUtils.isEmpty(info.avatar_url)) {
headerAvatarView.setVisibility(View.VISIBLE);
imageLoader.displayImage(info.avatar_url, headerAvatarView, displayImageOptions, new ImageErrorLoadingListener(activity, getView(), info.service_id));
}
if (info.subscriberCount != -1) {
headerSubscribersTextView.setText(buildSubscriberString(info.subscriberCount));
headerSubscribersTextView.setVisibility(View.VISIBLE);
} else headerSubscribersTextView.setVisibility(View.GONE);
if (!TextUtils.isEmpty(info.feed_url)) headerRssButton.setVisibility(View.VISIBLE);
else headerRssButton.setVisibility(View.INVISIBLE);
infoListAdapter.showFooter(true);
}
hasNextPage = info.hasNextPage;
if (!hasNextPage) infoListAdapter.showFooter(false);
//if (!listRestored) {
if (addVideos) infoListAdapter.addInfoItemList(info.related_streams);
//}
}
@Override
protected void setErrorMessage(String message, boolean showRetryButton) {
super.setErrorMessage(message, showRetryButton);
animateView(channelVideosList, false, 200);
currentChannelInfo = null;
}
/*//////////////////////////////////////////////////////////////////////////
// OnChannelInfoReceiveListener
//////////////////////////////////////////////////////////////////////////*/
@Override
public void onReceive(ChannelInfo info, boolean onlyVideos) {
if (DEBUG) Log.d(TAG, "onReceive() called with: info = [" + info + "]");
if (info == null || isRemoving() || !isVisible()) return;
handleChannelInfo(info, onlyVideos, true);
isLoading.set(false);
}
@Override
public void onError(int messageId) {
if (DEBUG) Log.d(TAG, "onError() called with: messageId = [" + messageId + "]");
setErrorMessage(getString(messageId), true);
}
@Override
public void onUnrecoverableError(Exception exception) {
if (DEBUG) Log.d(TAG, "onUnrecoverableError() called with: exception = [" + exception + "]");
activity.finish();
}
}

View File

@@ -12,12 +12,12 @@ import android.widget.AdapterView;
import android.widget.Spinner;
import org.schabi.newpipe.R;
import org.schabi.newpipe.extractor.stream_info.VideoStream;
import org.schabi.newpipe.util.Utils;
import org.schabi.newpipe.extractor.stream.VideoStream;
import org.schabi.newpipe.util.ListHelper;
import java.util.List;
/**
/*
* Created by Christian Schabesberger on 18.08.15.
* <p>
* Copyright (C) Christian Schabesberger 2015 <chris.schabesberger@mailbox.org>
@@ -68,7 +68,7 @@ class ActionBarHandler {
public void setupStreamList(final List<VideoStream> videoStreams, Spinner toolbarSpinner) {
if (activity == null) return;
selectedVideoStream = Utils.getDefaultResolution(activity, videoStreams);
selectedVideoStream = ListHelper.getDefaultResolutionIndex(activity, videoStreams);
boolean isExternalPlayerEnabled = PreferenceManager.getDefaultSharedPreferences(activity).getBoolean(activity.getString(R.string.use_external_video_player_key), false);
toolbarSpinner.setAdapter(new SpinnerToolbarAdapter(activity, videoStreams, isExternalPlayerEnabled));

View File

@@ -11,7 +11,7 @@ import android.widget.TextView;
import org.schabi.newpipe.R;
import org.schabi.newpipe.extractor.MediaFormat;
import org.schabi.newpipe.extractor.stream_info.VideoStream;
import org.schabi.newpipe.extractor.stream.VideoStream;
import java.util.List;
@@ -57,8 +57,8 @@ public class SpinnerToolbarAdapter extends BaseAdapter {
convertView = LayoutInflater.from(context).inflate(R.layout.resolutions_spinner_item, parent, false);
}
ImageView woSoundIcon = (ImageView) convertView.findViewById(R.id.wo_sound_icon);
TextView text = (TextView) convertView.findViewById(android.R.id.text1);
ImageView woSoundIcon = convertView.findViewById(R.id.wo_sound_icon);
TextView text = convertView.findViewById(android.R.id.text1);
VideoStream item = (VideoStream) getItem(position);
text.setText(MediaFormat.getNameById(item.format) + " " + item.resolution);

View File

@@ -1,24 +1,25 @@
package org.schabi.newpipe.fragments.detail;
import org.schabi.newpipe.extractor.stream_info.StreamInfo;
import java.io.Serializable;
@SuppressWarnings("WeakerAccess")
public class StackItem implements Serializable {
class StackItem implements Serializable {
private int serviceId;
private String title, url;
private StreamInfo info;
public StackItem(String url, String title) {
this.title = title;
StackItem(int serviceId, String url, String title) {
this.serviceId = serviceId;
this.url = url;
this.title = title;
}
public void setTitle(String title) {
this.title = title;
}
public int getServiceId() {
return serviceId;
}
public String getTitle() {
return title;
}
@@ -27,16 +28,8 @@ public class StackItem implements Serializable {
return url;
}
public void setInfo(StreamInfo info) {
this.info = info;
}
public StreamInfo getInfo() {
return info;
}
@Override
public String toString() {
return getUrl() + " > " + getTitle();
return getServiceId() + ":" + getUrl() + " > " + getTitle();
}
}

View File

@@ -1,85 +0,0 @@
package org.schabi.newpipe.fragments.detail;
import android.support.annotation.NonNull;
import android.text.TextUtils;
import android.util.Log;
import org.schabi.newpipe.MainActivity;
import org.schabi.newpipe.extractor.stream_info.StreamInfo;
import java.util.Iterator;
import java.util.LinkedHashMap;
@SuppressWarnings("WeakerAccess")
public class StreamInfoCache {
private static String TAG = "StreamInfoCache@";
private static final boolean DEBUG = MainActivity.DEBUG;
private static final StreamInfoCache instance = new StreamInfoCache();
private static final int MAX_ITEMS_ON_CACHE = 20;
private final LinkedHashMap<String, StreamInfo> myCache = new LinkedHashMap<>();
private StreamInfoCache() {
TAG += "" + Integer.toHexString(hashCode());
}
public static StreamInfoCache getInstance() {
if (DEBUG) Log.d(TAG, "getInstance() called");
return instance;
}
public boolean hasKey(@NonNull String url) {
if (DEBUG) Log.d(TAG, "hasKey() called with: url = [" + url + "]");
return !TextUtils.isEmpty(url) && myCache.containsKey(url) && myCache.get(url) != null;
}
public StreamInfo getFromKey(@NonNull String url) {
if (DEBUG) Log.d(TAG, "getFromKey() called with: url = [" + url + "]");
return myCache.get(url);
}
public void putInfo(@NonNull StreamInfo info) {
if (DEBUG) Log.d(TAG, "putInfo() called with: info = [" + info + "]");
putInfo(info.webpage_url, info);
}
public void putInfo(@NonNull String url, @NonNull StreamInfo info) {
if (DEBUG) Log.d(TAG, "putInfo() called with: url = [" + url + "], info = [" + info + "]");
myCache.put(url, info);
}
public void removeInfo(@NonNull StreamInfo info) {
if (DEBUG) Log.d(TAG, "removeInfo() called with: info = [" + info + "]");
myCache.remove(info.webpage_url);
}
public void removeInfo(@NonNull String url) {
if (DEBUG) Log.d(TAG, "removeInfo() called with: url = [" + url + "]");
myCache.remove(url);
}
@SuppressWarnings("unused")
public void clearCache() {
if (DEBUG) Log.d(TAG, "clearCache() called");
myCache.clear();
}
public void removeOldEntries() {
if (DEBUG) Log.d(TAG, "removeOldEntries() called , size = " + getSize());
if (getSize() > MAX_ITEMS_ON_CACHE) {
Iterator<String> iterator = myCache.keySet().iterator();
while (iterator.hasNext()) {
iterator.next();
iterator.remove();
if (DEBUG) Log.d(TAG, "getSize() = " + getSize());
if (getSize() <= MAX_ITEMS_ON_CACHE) break;
}
}
}
public int getSize() {
return myCache.size();
}
}

View File

@@ -0,0 +1,239 @@
package org.schabi.newpipe.fragments.list;
import android.content.Context;
import android.os.Bundle;
import android.support.annotation.NonNull;
import android.support.v7.app.ActionBar;
import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView;
import android.util.Log;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.View;
import org.schabi.newpipe.R;
import org.schabi.newpipe.extractor.InfoItem;
import org.schabi.newpipe.extractor.channel.ChannelInfoItem;
import org.schabi.newpipe.extractor.playlist.PlaylistInfoItem;
import org.schabi.newpipe.extractor.stream.StreamInfoItem;
import org.schabi.newpipe.fragments.BaseStateFragment;
import org.schabi.newpipe.fragments.OnScrollBelowItemsListener;
import org.schabi.newpipe.info_list.InfoItemBuilder;
import org.schabi.newpipe.info_list.InfoListAdapter;
import org.schabi.newpipe.util.NavigationHelper;
import org.schabi.newpipe.util.StateSaver;
import java.util.List;
import java.util.Queue;
import static org.schabi.newpipe.util.AnimationUtils.animateView;
public abstract class BaseListFragment<I, N> extends BaseStateFragment<I> implements ListViewContract<I, N>, StateSaver.WriteRead {
/*//////////////////////////////////////////////////////////////////////////
// Views
//////////////////////////////////////////////////////////////////////////*/
protected InfoListAdapter infoListAdapter;
protected RecyclerView itemsList;
/*//////////////////////////////////////////////////////////////////////////
// LifeCycle
//////////////////////////////////////////////////////////////////////////*/
@Override
public void onAttach(Context context) {
super.onAttach(context);
infoListAdapter = new InfoListAdapter(activity);
}
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setHasOptionsMenu(true);
}
@Override
public void onDestroy() {
super.onDestroy();
StateSaver.onDestroy(savedState);
}
/*//////////////////////////////////////////////////////////////////////////
// State Saving
//////////////////////////////////////////////////////////////////////////*/
protected StateSaver.SavedState savedState;
@Override
public String generateSuffix() {
// Naive solution, but it's good for now (the items don't change)
return "." + infoListAdapter.getItemsList().size() + ".list";
}
@Override
public void writeTo(Queue<Object> objectsToSave) {
objectsToSave.add(infoListAdapter.getItemsList());
}
@Override
@SuppressWarnings("unchecked")
public void readFrom(@NonNull Queue<Object> savedObjects) throws Exception {
infoListAdapter.getItemsList().clear();
infoListAdapter.getItemsList().addAll((List<InfoItem>) savedObjects.poll());
}
@Override
public void onSaveInstanceState(Bundle bundle) {
super.onSaveInstanceState(bundle);
savedState = StateSaver.tryToSave(activity.isChangingConfigurations(), savedState, bundle, this);
}
@Override
protected void onRestoreInstanceState(@NonNull Bundle bundle) {
super.onRestoreInstanceState(bundle);
savedState = StateSaver.tryToRestore(bundle, this);
}
/*//////////////////////////////////////////////////////////////////////////
// Init
//////////////////////////////////////////////////////////////////////////*/
protected View getListHeader() {
return null;
}
protected View getListFooter() {
return activity.getLayoutInflater().inflate(R.layout.pignate_footer, itemsList, false);
}
protected RecyclerView.LayoutManager getListLayoutManager() {
return new LinearLayoutManager(activity);
}
@Override
protected void initViews(View rootView, Bundle savedInstanceState) {
super.initViews(rootView, savedInstanceState);
itemsList = rootView.findViewById(R.id.items_list);
itemsList.setLayoutManager(getListLayoutManager());
infoListAdapter.setFooter(getListFooter());
infoListAdapter.setHeader(getListHeader());
itemsList.setAdapter(infoListAdapter);
}
protected void onItemSelected(InfoItem selectedItem) {
if (DEBUG) Log.d(TAG, "onItemSelected() called with: selectedItem = [" + selectedItem + "]");
}
@Override
protected void initListeners() {
super.initListeners();
infoListAdapter.setOnStreamSelectedListener(new InfoItemBuilder.OnInfoItemSelectedListener<StreamInfoItem>() {
@Override
public void selected(StreamInfoItem selectedItem) {
onItemSelected(selectedItem);
NavigationHelper.openVideoDetailFragment(getFragmentManager(), selectedItem.service_id, selectedItem.url, selectedItem.name);
}
});
infoListAdapter.setOnChannelSelectedListener(new InfoItemBuilder.OnInfoItemSelectedListener<ChannelInfoItem>() {
@Override
public void selected(ChannelInfoItem selectedItem) {
onItemSelected(selectedItem);
NavigationHelper.openChannelFragment(getFragmentManager(), selectedItem.service_id, selectedItem.url, selectedItem.name);
}
});
infoListAdapter.setOnPlaylistSelectedListener(new InfoItemBuilder.OnInfoItemSelectedListener<PlaylistInfoItem>() {
@Override
public void selected(PlaylistInfoItem selectedItem) {
onItemSelected(selectedItem);
NavigationHelper.openPlaylistFragment(getFragmentManager(), selectedItem.service_id, selectedItem.url, selectedItem.name);
}
});
itemsList.clearOnScrollListeners();
itemsList.addOnScrollListener(new OnScrollBelowItemsListener() {
@Override
public void onScrolledDown(RecyclerView recyclerView) {
onScrollToBottom();
}
});
}
protected void onScrollToBottom() {
if (hasMoreItems() && !isLoading.get()) {
loadMoreItems();
}
}
/*//////////////////////////////////////////////////////////////////////////
// Menu
//////////////////////////////////////////////////////////////////////////*/
@Override
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
if (DEBUG) Log.d(TAG, "onCreateOptionsMenu() called with: menu = [" + menu + "], inflater = [" + inflater + "]");
super.onCreateOptionsMenu(menu, inflater);
ActionBar supportActionBar = activity.getSupportActionBar();
if (supportActionBar != null) {
supportActionBar.setDisplayShowTitleEnabled(true);
supportActionBar.setDisplayHomeAsUpEnabled(true);
}
}
/*//////////////////////////////////////////////////////////////////////////
// Load and handle
//////////////////////////////////////////////////////////////////////////*/
protected abstract void loadMoreItems();
protected abstract boolean hasMoreItems();
/*//////////////////////////////////////////////////////////////////////////
// Contract
//////////////////////////////////////////////////////////////////////////*/
@Override
public void showLoading() {
super.showLoading();
// animateView(itemsList, false, 400);
}
@Override
public void hideLoading() {
super.hideLoading();
animateView(itemsList, true, 300);
}
@Override
public void showError(String message, boolean showRetryButton) {
super.showError(message, showRetryButton);
showListFooter(false);
animateView(itemsList, false, 200);
}
@Override
public void showEmptyState() {
super.showEmptyState();
showListFooter(false);
}
@Override
public void showListFooter(final boolean show) {
itemsList.post(new Runnable() {
@Override
public void run() {
infoListAdapter.showFooter(show);
}
});
}
@Override
public void handleNextItems(N result) {
isLoading.set(false);
}
}

View File

@@ -0,0 +1,216 @@
package org.schabi.newpipe.fragments.list;
import android.os.Bundle;
import android.support.annotation.NonNull;
import android.text.TextUtils;
import android.util.Log;
import android.view.View;
import org.schabi.newpipe.extractor.ListExtractor;
import org.schabi.newpipe.extractor.ListInfo;
import java.util.Queue;
import icepick.State;
import io.reactivex.Single;
import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.disposables.Disposable;
import io.reactivex.functions.Consumer;
import io.reactivex.schedulers.Schedulers;
public abstract class BaseListInfoFragment<I extends ListInfo> extends BaseListFragment<I, ListExtractor.NextItemsResult> {
@State
protected int serviceId = -1;
@State
protected String name;
@State
protected String url;
protected I currentInfo;
protected String currentNextItemsUrl;
protected Disposable currentWorker;
@Override
protected void initViews(View rootView, Bundle savedInstanceState) {
super.initViews(rootView, savedInstanceState);
setTitle(name);
showListFooter(hasMoreItems());
}
@Override
public void onPause() {
super.onPause();
if (currentWorker != null) currentWorker.dispose();
}
@Override
public void onResume() {
super.onResume();
// Check if it was loading when the fragment was stopped/paused,
if (wasLoading.getAndSet(false)) {
if (hasMoreItems() && infoListAdapter.getItemsList().size() > 0) {
loadMoreItems();
} else {
doInitialLoadLogic();
}
}
}
@Override
public void onDestroy() {
super.onDestroy();
if (currentWorker != null) currentWorker.dispose();
currentWorker = null;
}
/*//////////////////////////////////////////////////////////////////////////
// State Saving
//////////////////////////////////////////////////////////////////////////*/
@Override
public void writeTo(Queue<Object> objectsToSave) {
super.writeTo(objectsToSave);
objectsToSave.add(currentInfo);
objectsToSave.add(currentNextItemsUrl);
}
@Override
@SuppressWarnings("unchecked")
public void readFrom(@NonNull Queue<Object> savedObjects) throws Exception {
super.readFrom(savedObjects);
currentInfo = (I) savedObjects.poll();
currentNextItemsUrl = (String) savedObjects.poll();
}
/*//////////////////////////////////////////////////////////////////////////
// Utils
//////////////////////////////////////////////////////////////////////////*/
public void setTitle(String title) {
Log.d(TAG, "setTitle() called with: title = [" + title + "]");
if (activity.getSupportActionBar() != null) {
activity.getSupportActionBar().setTitle(title);
}
}
/*//////////////////////////////////////////////////////////////////////////
// Load and handle
//////////////////////////////////////////////////////////////////////////*/
protected void doInitialLoadLogic() {
if (DEBUG) Log.d(TAG, "doInitialLoadLogic() called");
if (currentInfo == null) {
startLoading(false);
} else handleResult(currentInfo);
}
/**
* Implement the logic to load the info from the network.<br/>
* You can use the default implementations from {@link org.schabi.newpipe.util.ExtractorHelper}.
*
* @param forceLoad allow or disallow the result to come from the cache
*/
protected abstract Single<I> loadResult(boolean forceLoad);
@Override
public void startLoading(boolean forceLoad) {
super.startLoading(forceLoad);
showListFooter(false);
currentInfo = null;
if (currentWorker != null) currentWorker.dispose();
currentWorker = loadResult(forceLoad)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(new Consumer<I>() {
@Override
public void accept(@NonNull I result) throws Exception {
isLoading.set(false);
currentInfo = result;
currentNextItemsUrl = result.next_streams_url;
handleResult(result);
}
}, new Consumer<Throwable>() {
@Override
public void accept(@NonNull Throwable throwable) throws Exception {
onError(throwable);
}
});
}
/**
* 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 void loadMoreItems() {
isLoading.set(true);
if (currentWorker != null) currentWorker.dispose();
currentWorker = loadMoreItemsLogic()
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(new Consumer<ListExtractor.NextItemsResult>() {
@Override
public void accept(@io.reactivex.annotations.NonNull ListExtractor.NextItemsResult nextItemsResult) throws Exception {
isLoading.set(false);
handleNextItems(nextItemsResult);
}
}, new Consumer<Throwable>() {
@Override
public void accept(@io.reactivex.annotations.NonNull Throwable throwable) throws Exception {
isLoading.set(false);
onError(throwable);
}
});
}
@Override
public void handleNextItems(ListExtractor.NextItemsResult result) {
super.handleNextItems(result);
currentNextItemsUrl = result.nextItemsUrl;
infoListAdapter.addInfoItemList(result.nextItemsList);
showListFooter(hasMoreItems());
}
@Override
protected boolean hasMoreItems() {
return !TextUtils.isEmpty(currentNextItemsUrl);
}
/*//////////////////////////////////////////////////////////////////////////
// Contract
//////////////////////////////////////////////////////////////////////////*/
@Override
public void handleResult(@NonNull I result) {
super.handleResult(result);
url = result.url;
name = result.name;
setTitle(name);
if (infoListAdapter.getItemsList().size() == 0) {
if (result.related_streams.size() > 0) {
infoListAdapter.addInfoItemList(result.related_streams);
showListFooter(hasMoreItems());
} else {
infoListAdapter.clearStreamItemList();
showEmptyState();
}
}
}
/*//////////////////////////////////////////////////////////////////////////
// Utils
//////////////////////////////////////////////////////////////////////////*/
protected void setInitialData(int serviceId, String url, String name) {
this.serviceId = serviceId;
this.url = url;
this.name = !TextUtils.isEmpty(name) ? name : "";
}
}

View File

@@ -0,0 +1,9 @@
package org.schabi.newpipe.fragments.list;
import org.schabi.newpipe.fragments.ViewContract;
public interface ListViewContract<I, N> extends ViewContract<I> {
void showListFooter(boolean show);
void handleNextItems(N result);
}

View File

@@ -0,0 +1,383 @@
package org.schabi.newpipe.fragments.list.channel;
import android.content.Context;
import android.content.Intent;
import android.net.Uri;
import android.os.Bundle;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.v4.content.ContextCompat;
import android.text.TextUtils;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Button;
import android.widget.ImageView;
import android.widget.TextView;
import com.jakewharton.rxbinding2.view.RxView;
import org.schabi.newpipe.R;
import org.schabi.newpipe.database.subscription.SubscriptionEntity;
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.fragments.list.BaseListInfoFragment;
import org.schabi.newpipe.fragments.subscription.SubscriptionService;
import org.schabi.newpipe.report.UserAction;
import org.schabi.newpipe.util.AnimationUtils;
import org.schabi.newpipe.util.ExtractorHelper;
import org.schabi.newpipe.util.Localization;
import java.util.List;
import java.util.concurrent.TimeUnit;
import io.reactivex.Observable;
import io.reactivex.Single;
import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.disposables.CompositeDisposable;
import io.reactivex.disposables.Disposable;
import io.reactivex.functions.Action;
import io.reactivex.functions.Consumer;
import io.reactivex.functions.Function;
import io.reactivex.schedulers.Schedulers;
import static org.schabi.newpipe.util.AnimationUtils.animateBackgroundColor;
import static org.schabi.newpipe.util.AnimationUtils.animateTextColor;
import static org.schabi.newpipe.util.AnimationUtils.animateView;
public class ChannelFragment extends BaseListInfoFragment<ChannelInfo> {
private CompositeDisposable disposables = new CompositeDisposable();
private Disposable subscribeButtonMonitor;
private SubscriptionService subscriptionService;
/*//////////////////////////////////////////////////////////////////////////
// Views
//////////////////////////////////////////////////////////////////////////*/
private View headerRootLayout;
private ImageView headerChannelBanner;
private ImageView headerAvatarView;
private TextView headerTitleView;
private TextView headerSubscribersTextView;
private Button headerSubscribeButton;
private MenuItem menuRssButton;
public static ChannelFragment getInstance(int serviceId, String url, String name) {
ChannelFragment instance = new ChannelFragment();
instance.setInitialData(serviceId, url, name);
return instance;
}
/*//////////////////////////////////////////////////////////////////////////
// LifeCycle
//////////////////////////////////////////////////////////////////////////*/
@Override
public void onAttach(Context context) {
super.onAttach(context);
subscriptionService = SubscriptionService.getInstance();
}
@Override
public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
return inflater.inflate(R.layout.fragment_channel, container, false);
}
@Override
public void onDestroy() {
super.onDestroy();
if (disposables != null) disposables.clear();
if (subscribeButtonMonitor != null) subscribeButtonMonitor.dispose();
}
/*//////////////////////////////////////////////////////////////////////////
// Init
//////////////////////////////////////////////////////////////////////////*/
protected View getListHeader() {
headerRootLayout = activity.getLayoutInflater().inflate(R.layout.channel_header, itemsList, false);
headerChannelBanner = headerRootLayout.findViewById(R.id.channel_banner_image);
headerAvatarView = headerRootLayout.findViewById(R.id.channel_avatar_view);
headerTitleView = headerRootLayout.findViewById(R.id.channel_title_view);
headerSubscribersTextView = headerRootLayout.findViewById(R.id.channel_subscriber_view);
headerSubscribeButton = headerRootLayout.findViewById(R.id.channel_subscribe_button);
return headerRootLayout;
}
/*//////////////////////////////////////////////////////////////////////////
// Menu
//////////////////////////////////////////////////////////////////////////*/
@Override
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
if (DEBUG) Log.d(TAG, "onCreateOptionsMenu() called with: menu = [" + menu + "], inflater = [" + inflater + "]");
super.onCreateOptionsMenu(menu, inflater);
inflater.inflate(R.menu.menu_channel, menu);
menuRssButton = menu.findItem(R.id.menu_item_rss);
if (currentInfo != null) {
menuRssButton.setVisible(!TextUtils.isEmpty(currentInfo.feed_url));
}
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
case R.id.menu_item_rss: {
Intent intent = new Intent();
intent.setAction(Intent.ACTION_VIEW);
intent.setData(Uri.parse(currentInfo.feed_url));
startActivity(intent);
return true;
}
default:
return super.onOptionsItemSelected(item);
}
}
/*//////////////////////////////////////////////////////////////////////////
// Channel Subscription
//////////////////////////////////////////////////////////////////////////*/
private static final int BUTTON_DEBOUNCE_INTERVAL = 100;
private void monitorSubscription(final ChannelInfo info) {
final Consumer<Throwable> onError = new Consumer<Throwable>() {
@Override
public void accept(Throwable throwable) throws Exception {
animateView(headerSubscribeButton, false, 100);
showSnackBarError(throwable, UserAction.SUBSCRIPTION, NewPipe.getNameOfService(currentInfo.service_id), "Get subscription status", 0);
}
};
final Observable<List<SubscriptionEntity>> observable = subscriptionService.subscriptionTable()
.getSubscription(info.service_id, info.url)
.toObservable();
disposables.add(observable
.observeOn(AndroidSchedulers.mainThread())
.subscribe(getSubscribeUpdateMonitor(info), onError));
disposables.add(observable
// Some updates are very rapid (when calling the updateSubscription(info), for example)
// so only update the UI for the latest emission ("sync" the subscribe button's state)
.debounce(100, TimeUnit.MILLISECONDS)
.observeOn(AndroidSchedulers.mainThread())
.subscribe(new Consumer<List<SubscriptionEntity>>() {
@Override
public void accept(List<SubscriptionEntity> subscriptionEntities) throws Exception {
updateSubscribeButton(!subscriptionEntities.isEmpty());
}
}, onError));
}
private Function<Object, Object> mapOnSubscribe(final SubscriptionEntity subscription) {
return new Function<Object, Object>() {
@Override
public Object apply(@NonNull Object o) throws Exception {
subscriptionService.subscriptionTable().insert(subscription);
return o;
}
};
}
private Function<Object, Object> mapOnUnsubscribe(final SubscriptionEntity subscription) {
return new Function<Object, Object>() {
@Override
public Object apply(@NonNull Object o) throws Exception {
subscriptionService.subscriptionTable().delete(subscription);
return o;
}
};
}
private void updateSubscription(final ChannelInfo info) {
if (DEBUG) Log.d(TAG, "updateSubscription() called with: info = [" + info + "]");
final Action onComplete = new Action() {
@Override
public void run() throws Exception {
if (DEBUG) Log.d(TAG, "Updated subscription: " + info.url);
}
};
final Consumer<Throwable> onError = new Consumer<Throwable>() {
@Override
public void accept(@NonNull Throwable throwable) throws Exception {
onUnrecoverableError(throwable, UserAction.SUBSCRIPTION, NewPipe.getNameOfService(info.service_id), "Updating Subscription for " + info.url, R.string.subscription_update_failed);
}
};
disposables.add(subscriptionService.updateChannelInfo(info)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(onComplete, onError));
}
private Disposable monitorSubscribeButton(final Button subscribeButton, final Function<Object, Object> action) {
final Consumer<Object> onNext = new Consumer<Object>() {
@Override
public void accept(@NonNull Object o) throws Exception {
if (DEBUG) Log.d(TAG, "Changed subscription status to this channel!");
}
};
final Consumer<Throwable> onError = new Consumer<Throwable>() {
@Override
public void accept(@NonNull Throwable throwable) throws Exception {
onUnrecoverableError(throwable, UserAction.SUBSCRIPTION, NewPipe.getNameOfService(currentInfo.service_id), "Subscription Change", R.string.subscription_change_failed);
}
};
/* Emit clicks from main thread unto io thread */
return RxView.clicks(subscribeButton)
.subscribeOn(AndroidSchedulers.mainThread())
.observeOn(Schedulers.io())
.debounce(BUTTON_DEBOUNCE_INTERVAL, TimeUnit.MILLISECONDS) // Ignore rapid clicks
.map(action)
.subscribe(onNext, onError);
}
private Consumer<List<SubscriptionEntity>> getSubscribeUpdateMonitor(final ChannelInfo info) {
return new Consumer<List<SubscriptionEntity>>() {
@Override
public void accept(List<SubscriptionEntity> subscriptionEntities) throws Exception {
if (DEBUG)
Log.d(TAG, "subscriptionService.subscriptionTable.doOnNext() called with: subscriptionEntities = [" + subscriptionEntities + "]");
if (subscribeButtonMonitor != null) subscribeButtonMonitor.dispose();
if (subscriptionEntities.isEmpty()) {
if (DEBUG) Log.d(TAG, "No subscription to this channel!");
SubscriptionEntity channel = new SubscriptionEntity();
channel.setServiceId(info.service_id);
channel.setUrl(info.url);
channel.setData(info.name, info.avatar_url, info.description, info.subscriber_count);
subscribeButtonMonitor = monitorSubscribeButton(headerSubscribeButton, mapOnSubscribe(channel));
} else {
if (DEBUG) Log.d(TAG, "Found subscription to this channel!");
final SubscriptionEntity subscription = subscriptionEntities.get(0);
subscribeButtonMonitor = monitorSubscribeButton(headerSubscribeButton, mapOnUnsubscribe(subscription));
}
}
};
}
private void updateSubscribeButton(boolean isSubscribed) {
if (DEBUG) Log.d(TAG, "updateSubscribeButton() called with: isSubscribed = [" + isSubscribed + "]");
boolean isButtonVisible = headerSubscribeButton.getVisibility() == View.VISIBLE;
int backgroundDuration = isButtonVisible ? 300 : 0;
int textDuration = isButtonVisible ? 200 : 0;
int subscribeBackground = ContextCompat.getColor(activity, R.color.subscribe_background_color);
int subscribeText = ContextCompat.getColor(activity, R.color.subscribe_text_color);
int subscribedBackground = ContextCompat.getColor(activity, R.color.subscribed_background_color);
int subscribedText = ContextCompat.getColor(activity, R.color.subscribed_text_color);
if (!isSubscribed) {
headerSubscribeButton.setText(R.string.subscribe_button_title);
animateBackgroundColor(headerSubscribeButton, backgroundDuration, subscribedBackground, subscribeBackground);
animateTextColor(headerSubscribeButton, textDuration, subscribedText, subscribeText);
} else {
headerSubscribeButton.setText(R.string.subscribed_button_title);
animateBackgroundColor(headerSubscribeButton, backgroundDuration, subscribeBackground, subscribedBackground);
animateTextColor(headerSubscribeButton, textDuration, subscribeText, subscribedText);
}
animateView(headerSubscribeButton, AnimationUtils.Type.LIGHT_SCALE_AND_ALPHA, true, 100);
}
/*//////////////////////////////////////////////////////////////////////////
// Load and handle
//////////////////////////////////////////////////////////////////////////*/
@Override
protected Single<ListExtractor.NextItemsResult> loadMoreItemsLogic() {
return ExtractorHelper.getMoreChannelItems(serviceId, url, currentNextItemsUrl);
}
@Override
protected Single<ChannelInfo> loadResult(boolean forceLoad) {
return ExtractorHelper.getChannelInfo(serviceId, url, forceLoad);
}
/*//////////////////////////////////////////////////////////////////////////
// Contract
//////////////////////////////////////////////////////////////////////////*/
@Override
public void showLoading() {
super.showLoading();
imageLoader.cancelDisplayTask(headerChannelBanner);
imageLoader.cancelDisplayTask(headerAvatarView);
animateView(headerSubscribeButton, false, 100);
}
@Override
public void handleResult(@NonNull ChannelInfo result) {
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);
if (result.subscriber_count != -1) {
headerSubscribersTextView.setText(Localization.localizeSubscribersCount(activity, result.subscriber_count));
headerSubscribersTextView.setVisibility(View.VISIBLE);
} else headerSubscribersTextView.setVisibility(View.GONE);
if (menuRssButton != null) menuRssButton.setVisible(!TextUtils.isEmpty(result.feed_url));
if (!result.errors.isEmpty()) {
showSnackBarError(result.errors, UserAction.REQUESTED_CHANNEL, NewPipe.getNameOfService(result.service_id), result.url, 0);
}
if (disposables != null) disposables.clear();
if (subscribeButtonMonitor != null) subscribeButtonMonitor.dispose();
updateSubscription(result);
monitorSubscription(result);
}
@Override
public void handleNextItems(ListExtractor.NextItemsResult result) {
super.handleNextItems(result);
if (!result.errors.isEmpty()) {
showSnackBarError(result.errors, UserAction.REQUESTED_CHANNEL, NewPipe.getNameOfService(serviceId),
"Get next page of: " + url, R.string.general_error);
}
}
/*//////////////////////////////////////////////////////////////////////////
// OnError
//////////////////////////////////////////////////////////////////////////*/
@Override
protected boolean onError(Throwable exception) {
if (super.onError(exception)) return true;
int errorId = exception instanceof ExtractionException ? R.string.parsing_error : R.string.general_error;
onUnrecoverableError(exception, UserAction.REQUESTED_CHANNEL, NewPipe.getNameOfService(serviceId), url, errorId);
return true;
}
/*//////////////////////////////////////////////////////////////////////////
// Utils
//////////////////////////////////////////////////////////////////////////*/
@Override
public void setTitle(String title) {
super.setTitle(title);
headerTitleView.setText(title);
}
}

View File

@@ -0,0 +1,445 @@
package org.schabi.newpipe.fragments.list.feed;
import android.os.Bundle;
import android.os.Handler;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.v7.app.ActionBar;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.View;
import android.view.ViewGroup;
import org.reactivestreams.Subscriber;
import org.reactivestreams.Subscription;
import org.schabi.newpipe.R;
import org.schabi.newpipe.database.subscription.SubscriptionEntity;
import org.schabi.newpipe.extractor.InfoItem;
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 java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Queue;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import io.reactivex.Flowable;
import io.reactivex.MaybeObserver;
import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.disposables.CompositeDisposable;
import io.reactivex.disposables.Disposable;
import io.reactivex.functions.Action;
import io.reactivex.functions.Consumer;
import io.reactivex.functions.Predicate;
import io.reactivex.schedulers.Schedulers;
public class FeedFragment extends BaseListFragment<List<SubscriptionEntity>, Void> {
private static final int OFF_SCREEN_ITEMS_COUNT = 3;
private static final int MIN_ITEMS_INITIAL_LOAD = 8;
private int FEED_LOAD_COUNT = MIN_ITEMS_INITIAL_LOAD;
private int subscriptionPoolSize;
private SubscriptionService subscriptionService;
private AtomicBoolean allItemsLoaded = new AtomicBoolean(false);
private HashSet<String> itemsLoaded = new HashSet<>();
private final AtomicInteger requestLoadedAtomic = new AtomicInteger();
private CompositeDisposable compositeDisposable = new CompositeDisposable();
private Disposable subscriptionObserver;
private Subscription feedSubscriber;
/*//////////////////////////////////////////////////////////////////////////
// Fragment LifeCycle
//////////////////////////////////////////////////////////////////////////*/
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
subscriptionService = SubscriptionService.getInstance();
FEED_LOAD_COUNT = howManyItemsToLoad();
}
@Override
public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, Bundle savedInstanceState) {
return inflater.inflate(R.layout.fragment_feed, container, false);
}
@Override
public void onPause() {
super.onPause();
disposeEverything();
}
@Override
public void onResume() {
super.onResume();
if (wasLoading.get()) doInitialLoadLogic();
}
@Override
public void onDestroy() {
super.onDestroy();
disposeEverything();
subscriptionService = null;
compositeDisposable = null;
subscriptionObserver = null;
feedSubscriber = null;
}
@Override
public void onDestroyView() {
// Do not monitor for updates when user is not viewing the feed fragment.
// This is a waste of bandwidth.
disposeEverything();
super.onDestroyView();
}
/*@Override
protected RecyclerView.LayoutManager getListLayoutManager() {
boolean isPortrait = getResources().getDisplayMetrics().heightPixels > getResources().getDisplayMetrics().widthPixels;
return new GridLayoutManager(activity, isPortrait ? 1 : 2);
}*/
@Override
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
super.onCreateOptionsMenu(menu, inflater);
ActionBar supportActionBar = activity.getSupportActionBar();
if (supportActionBar != null) {
supportActionBar.setTitle(R.string.fragment_whats_new);
}
}
@Override
public void reloadContent() {
resetFragment();
super.reloadContent();
}
/*//////////////////////////////////////////////////////////////////////////
// StateSaving
//////////////////////////////////////////////////////////////////////////*/
@Override
public void writeTo(Queue<Object> objectsToSave) {
super.writeTo(objectsToSave);
objectsToSave.add(allItemsLoaded);
objectsToSave.add(itemsLoaded);
}
@Override
@SuppressWarnings("unchecked")
public void readFrom(@NonNull Queue<Object> savedObjects) throws Exception {
super.readFrom(savedObjects);
allItemsLoaded = (AtomicBoolean) savedObjects.poll();
itemsLoaded = (HashSet<String>) savedObjects.poll();
}
/*//////////////////////////////////////////////////////////////////////////
// Feed Loader
//////////////////////////////////////////////////////////////////////////*/
@Override
public void startLoading(boolean forceLoad) {
if (DEBUG) Log.d(TAG, "startLoading() called with: forceLoad = [" + forceLoad + "]");
if (subscriptionObserver != null) subscriptionObserver.dispose();
if (allItemsLoaded.get()) {
if (infoListAdapter.getItemsList().size() == 0) {
showEmptyState();
} else {
showListFooter(false);
hideLoading();
}
isLoading.set(false);
return;
}
isLoading.set(true);
showLoading();
showListFooter(true);
subscriptionObserver = subscriptionService.getSubscription()
.onErrorReturnItem(Collections.<SubscriptionEntity>emptyList())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(new Consumer<List<SubscriptionEntity>>() {
@Override
public void accept(List<SubscriptionEntity> subscriptionEntities) throws Exception {
handleResult(subscriptionEntities);
}
}, new Consumer<Throwable>() {
@Override
public void accept(Throwable throwable) throws Exception {
onError(throwable);
}
});
}
@Override
public void handleResult(@android.support.annotation.NonNull List<SubscriptionEntity> result) {
super.handleResult(result);
if (result.isEmpty()) {
infoListAdapter.clearStreamItemList();
showEmptyState();
return;
}
subscriptionPoolSize = result.size();
Flowable.fromIterable(result)
.observeOn(AndroidSchedulers.mainThread())
.subscribe(getSubscriptionObserver());
}
/**
* Responsible for reacting to user pulling request and starting a request for new feed stream.
* <p>
* On initialization, it automatically requests the amount of feed needed to display
* a minimum amount required (FEED_LOAD_SIZE).
* <p>
* Upon receiving a user pull, it creates a Single Observer to fetch the ChannelInfo
* containing the feed streams.
**/
private Subscriber<SubscriptionEntity> getSubscriptionObserver() {
return new Subscriber<SubscriptionEntity>() {
@Override
public void onSubscribe(Subscription s) {
if (feedSubscriber != null) feedSubscriber.cancel();
feedSubscriber = s;
int requestSize = FEED_LOAD_COUNT - infoListAdapter.getItemsList().size();
if (wasLoading.getAndSet(false)) requestSize = FEED_LOAD_COUNT;
boolean hasToLoad = requestSize > 0;
if (hasToLoad) {
requestLoadedAtomic.set(infoListAdapter.getItemsList().size());
requestFeed(requestSize);
}
isLoading.set(hasToLoad);
}
@Override
public void onNext(SubscriptionEntity subscriptionEntity) {
if (!itemsLoaded.contains(subscriptionEntity.getServiceId() + subscriptionEntity.getUrl())) {
subscriptionService.getChannelInfo(subscriptionEntity)
.observeOn(AndroidSchedulers.mainThread())
.onErrorComplete(new Predicate<Throwable>() {
@Override
public boolean test(@io.reactivex.annotations.NonNull Throwable throwable) throws Exception {
return FeedFragment.super.onError(throwable);
}
})
.subscribe(getChannelInfoObserver(subscriptionEntity.getServiceId(), subscriptionEntity.getUrl()));
} else {
requestFeed(1);
}
}
@Override
public void onError(Throwable exception) {
FeedFragment.this.onError(exception);
}
@Override
public void onComplete() {
if (DEBUG) Log.d(TAG, "getSubscriptionObserver > onComplete() called");
}
};
}
/**
* On each request, a subscription item from the updated table is transformed
* into a ChannelInfo, containing the latest streams from the channel.
* <p>
* Currently, the feed uses the first into from the list of streams.
* <p>
* If chosen feed already displayed, then we request another feed from another
* subscription, until the subscription table runs out of new items.
* <p>
* This Observer is self-contained and will dispose itself when complete. However, this
* does not obey the fragment lifecycle and may continue running in the background
* until it is complete. This is done due to RxJava2 no longer propagate errors once
* an observer is unsubscribed while the thread process is still running.
* <p>
* To solve the above issue, we can either set a global RxJava Error Handler, or
* manage exceptions case by case. This should be done if the current implementation is
* too costly when dealing with larger subscription sets.
*
* @param url + serviceId to put in {@link #allItemsLoaded} to signal that this specific entity has been loaded.
*/
private MaybeObserver<ChannelInfo> getChannelInfoObserver(final int serviceId, final String url) {
return new MaybeObserver<ChannelInfo>() {
private Disposable observer;
@Override
public void onSubscribe(Disposable d) {
observer = d;
compositeDisposable.add(d);
isLoading.set(true);
}
// Called only when response is non-empty
@Override
public void onSuccess(final ChannelInfo channelInfo) {
if (infoListAdapter == null || channelInfo.related_streams.isEmpty()) {
onDone();
return;
}
final InfoItem item = channelInfo.related_streams.get(0);
// Keep requesting new items if the current one already exists
boolean itemExists = doesItemExist(infoListAdapter.getItemsList(), item);
if (!itemExists) {
infoListAdapter.addInfoItem(item);
//updateSubscription(channelInfo);
} else {
requestFeed(1);
}
onDone();
}
@Override
public void onError(Throwable exception) {
showSnackBarError(exception, UserAction.SUBSCRIPTION, NewPipe.getNameOfService(serviceId), url, 0);
requestFeed(1);
onDone();
}
// Called only when response is empty
@Override
public void onComplete() {
onDone();
}
private void onDone() {
if (observer.isDisposed()) {
return;
}
itemsLoaded.add(serviceId + url);
compositeDisposable.remove(observer);
int loaded = requestLoadedAtomic.incrementAndGet();
if (loaded >= Math.min(FEED_LOAD_COUNT, subscriptionPoolSize)) {
requestLoadedAtomic.set(0);
isLoading.set(false);
}
if (itemsLoaded.size() == subscriptionPoolSize) {
if (DEBUG) Log.d(TAG, "getChannelInfoObserver > All Items Loaded");
allItemsLoaded.set(true);
showListFooter(false);
isLoading.set(false);
hideLoading();
if (infoListAdapter.getItemsList().size() == 0) {
showEmptyState();
}
}
}
};
}
@Override
protected void loadMoreItems() {
isLoading.set(true);
delayHandler.removeCallbacksAndMessages(null);
// Add a little of a delay when requesting more items because the cache is so fast,
// that the view seems stuck to the user when he scroll to the bottom
delayHandler.postDelayed(new Runnable() {
@Override
public void run() {
requestFeed(FEED_LOAD_COUNT);
}
}, 300);
}
@Override
protected boolean hasMoreItems() {
return !allItemsLoaded.get();
}
private final Handler delayHandler = new Handler();
private void requestFeed(final int count) {
if (DEBUG) Log.d(TAG, "requestFeed() called with: count = [" + count + "], feedSubscriber = [" + feedSubscriber + "]");
if (feedSubscriber == null) return;
isLoading.set(true);
delayHandler.removeCallbacksAndMessages(null);
feedSubscriber.request(count);
}
/*//////////////////////////////////////////////////////////////////////////
// Utils
//////////////////////////////////////////////////////////////////////////*/
private void resetFragment() {
if (DEBUG) Log.d(TAG, "resetFragment() called");
if (subscriptionObserver != null) subscriptionObserver.dispose();
if (compositeDisposable != null) compositeDisposable.clear();
if (infoListAdapter != null) infoListAdapter.clearStreamItemList();
delayHandler.removeCallbacksAndMessages(null);
requestLoadedAtomic.set(0);
allItemsLoaded.set(false);
showListFooter(false);
itemsLoaded.clear();
}
private void disposeEverything() {
if (subscriptionObserver != null) subscriptionObserver.dispose();
if (compositeDisposable != null) compositeDisposable.clear();
if (feedSubscriber != null) feedSubscriber.cancel();
delayHandler.removeCallbacksAndMessages(null);
}
private boolean doesItemExist(final List<InfoItem> items, final InfoItem item) {
for (final InfoItem existingItem : items) {
if (existingItem.info_type == item.info_type &&
existingItem.service_id == item.service_id &&
existingItem.name.equals(item.name) &&
existingItem.url.equals(item.url)) return true;
}
return false;
}
private int howManyItemsToLoad() {
int heightPixels = getResources().getDisplayMetrics().heightPixels;
int itemHeightPixels = activity.getResources().getDimensionPixelSize(R.dimen.video_item_search_height);
int items = itemHeightPixels > 0 ? heightPixels / itemHeightPixels + OFF_SCREEN_ITEMS_COUNT : MIN_ITEMS_INITIAL_LOAD;
return Math.max(MIN_ITEMS_INITIAL_LOAD, items);
}
/*//////////////////////////////////////////////////////////////////////////
// Fragment Error Handling
//////////////////////////////////////////////////////////////////////////*/
@Override
public void showError(String message, boolean showRetryButton) {
resetFragment();
super.showError(message, showRetryButton);
}
@Override
protected boolean onError(Throwable exception) {
if (super.onError(exception)) return true;
int errorId = exception instanceof ExtractionException ? R.string.parsing_error : R.string.general_error;
onUnrecoverableError(exception, UserAction.SOMETHING_ELSE, "none", "Requesting feed", errorId);
return true;
}
}

View File

@@ -0,0 +1,174 @@
package org.schabi.newpipe.fragments.list.playlist;
import android.os.Bundle;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.text.TextUtils;
import android.util.Log;
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 org.schabi.newpipe.R;
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.fragments.list.BaseListInfoFragment;
import org.schabi.newpipe.report.UserAction;
import org.schabi.newpipe.util.ExtractorHelper;
import org.schabi.newpipe.util.NavigationHelper;
import io.reactivex.Single;
import static org.schabi.newpipe.util.AnimationUtils.animateView;
public class PlaylistFragment extends BaseListInfoFragment<PlaylistInfo> {
/*//////////////////////////////////////////////////////////////////////////
// Views
//////////////////////////////////////////////////////////////////////////*/
private View headerRootLayout;
private TextView headerTitleView;
private View headerUploaderLayout;
private TextView headerUploaderName;
private ImageView headerUploaderAvatar;
private TextView headerStreamCount;
public static PlaylistFragment getInstance(int serviceId, String url, String name) {
PlaylistFragment instance = new PlaylistFragment();
instance.setInitialData(serviceId, url, name);
return instance;
}
/*//////////////////////////////////////////////////////////////////////////
// LifeCycle
//////////////////////////////////////////////////////////////////////////*/
@Override
public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
return inflater.inflate(R.layout.fragment_playlist, container, false);
}
/*//////////////////////////////////////////////////////////////////////////
// Init
//////////////////////////////////////////////////////////////////////////*/
protected View getListHeader() {
headerRootLayout = activity.getLayoutInflater().inflate(R.layout.playlist_header, itemsList, false);
headerTitleView = headerRootLayout.findViewById(R.id.playlist_title_view);
headerUploaderLayout = headerRootLayout.findViewById(R.id.uploader_layout);
headerUploaderName = headerRootLayout.findViewById(R.id.uploader_name);
headerUploaderAvatar = headerRootLayout.findViewById(R.id.uploader_avatar_view);
headerStreamCount = headerRootLayout.findViewById(R.id.playlist_stream_count);
return headerRootLayout;
}
@Override
protected void initViews(View rootView, Bundle savedInstanceState) {
super.initViews(rootView, savedInstanceState);
infoListAdapter.useMiniItemVariants(true);
}
@Override
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
if (DEBUG) Log.d(TAG, "onCreateOptionsMenu() called with: menu = [" + menu + "], inflater = [" + inflater + "]");
super.onCreateOptionsMenu(menu, inflater);
inflater.inflate(R.menu.menu_playlist, menu);
}
/*//////////////////////////////////////////////////////////////////////////
// Load and handle
//////////////////////////////////////////////////////////////////////////*/
@Override
protected Single<ListExtractor.NextItemsResult> loadMoreItemsLogic() {
return ExtractorHelper.getMorePlaylistItems(serviceId, url, currentNextItemsUrl);
}
@Override
protected Single<PlaylistInfo> loadResult(boolean forceLoad) {
return ExtractorHelper.getPlaylistInfo(serviceId, url, forceLoad);
}
/*//////////////////////////////////////////////////////////////////////////
// Contract
//////////////////////////////////////////////////////////////////////////*/
@Override
public void showLoading() {
super.showLoading();
animateView(headerRootLayout, false, 200);
animateView(itemsList, false, 100);
imageLoader.cancelDisplayTask(headerUploaderAvatar);
animateView(headerUploaderLayout, false, 200);
}
@Override
public void handleResult(@NonNull final PlaylistInfo result) {
super.handleResult(result);
animateView(headerRootLayout, true, 100);
animateView(headerUploaderLayout, true, 300);
headerUploaderLayout.setOnClickListener(null);
if (!TextUtils.isEmpty(result.uploader_name)) {
headerUploaderName.setText(result.uploader_name);
if (!TextUtils.isEmpty(result.uploader_url)) {
headerUploaderLayout.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
NavigationHelper.openChannelFragment(getFragmentManager(), result.service_id, result.uploader_url, result.uploader_name);
}
});
}
}
imageLoader.displayImage(result.uploader_avatar_url, headerUploaderAvatar, DISPLAY_AVATAR_OPTIONS);
headerStreamCount.setText(result.stream_count + " videos");
if (!result.errors.isEmpty()) {
showSnackBarError(result.errors, UserAction.REQUESTED_PLAYLIST, NewPipe.getNameOfService(result.service_id), result.url, 0);
}
}
@Override
public void handleNextItems(ListExtractor.NextItemsResult result) {
super.handleNextItems(result);
if (!result.errors.isEmpty()) {
showSnackBarError(result.errors, UserAction.REQUESTED_PLAYLIST, NewPipe.getNameOfService(serviceId)
, "Get next page of: " + url, 0);
}
}
/*//////////////////////////////////////////////////////////////////////////
// OnError
//////////////////////////////////////////////////////////////////////////*/
@Override
protected boolean onError(Throwable exception) {
if (super.onError(exception)) return true;
int errorId = exception instanceof ExtractionException ? R.string.parsing_error : R.string.general_error;
onUnrecoverableError(exception, UserAction.REQUESTED_PLAYLIST, NewPipe.getNameOfService(serviceId), url, errorId);
return true;
}
/*//////////////////////////////////////////////////////////////////////////
// Utils
//////////////////////////////////////////////////////////////////////////*/
@Override
public void setTitle(String title) {
super.setTitle(title);
headerTitleView.setText(title);
}
}

View File

@@ -0,0 +1,695 @@
package org.schabi.newpipe.fragments.list.search;
import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.os.Build;
import android.os.Bundle;
import android.preference.PreferenceManager;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.v7.app.ActionBar;
import android.support.v7.widget.TooltipCompat;
import android.text.Editable;
import android.text.TextUtils;
import android.text.TextWatcher;
import android.util.Log;
import android.view.KeyEvent;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.view.animation.DecelerateInterpolator;
import android.view.inputmethod.EditorInfo;
import android.view.inputmethod.InputMethodManager;
import android.widget.AdapterView;
import android.widget.AutoCompleteTextView;
import android.widget.TextView;
import org.schabi.newpipe.R;
import org.schabi.newpipe.ReCaptchaActivity;
import org.schabi.newpipe.extractor.InfoItem;
import org.schabi.newpipe.extractor.ListExtractor;
import org.schabi.newpipe.extractor.NewPipe;
import org.schabi.newpipe.extractor.exceptions.ParsingException;
import org.schabi.newpipe.extractor.search.SearchEngine;
import org.schabi.newpipe.extractor.search.SearchResult;
import org.schabi.newpipe.fragments.list.BaseListFragment;
import org.schabi.newpipe.history.HistoryListener;
import org.schabi.newpipe.report.UserAction;
import org.schabi.newpipe.util.ExtractorHelper;
import org.schabi.newpipe.util.NavigationHelper;
import org.schabi.newpipe.util.StateSaver;
import java.util.ArrayList;
import java.util.List;
import java.util.Queue;
import java.util.concurrent.TimeUnit;
import icepick.State;
import io.reactivex.Notification;
import io.reactivex.Observable;
import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.disposables.Disposable;
import io.reactivex.functions.Consumer;
import io.reactivex.functions.Function;
import io.reactivex.functions.Predicate;
import io.reactivex.schedulers.Schedulers;
import io.reactivex.subjects.PublishSubject;
import static org.schabi.newpipe.util.AnimationUtils.animateView;
public class SearchFragment extends BaseListFragment<SearchResult, ListExtractor.NextItemsResult> {
/*//////////////////////////////////////////////////////////////////////////
// Search
//////////////////////////////////////////////////////////////////////////*/
/**
* The suggestions will appear only if the query meet this threshold (>=).
*/
private static final int THRESHOLD_SUGGESTION = 3;
/**
* How much time have to pass without emitting a item (i.e. the user stop typing) to fetch/show the suggestions, in milliseconds.
*/
private static final int SUGGESTIONS_DEBOUNCE = 150; //ms
@State
protected int filterItemCheckedId = -1;
private SearchEngine.Filter filter = SearchEngine.Filter.ANY;
@State
protected int serviceId = -1;
@State
protected String searchQuery = "";
@State
protected boolean wasSearchFocused = false;
private int currentPage = 0;
private int currentNextPage = 0;
private String searchLanguage;
private boolean showSuggestions = true;
private PublishSubject<String> suggestionPublisher = PublishSubject.create();
private Disposable searchDisposable;
private Disposable suggestionWorkerDisposable;
private SuggestionListAdapter suggestionListAdapter;
/*//////////////////////////////////////////////////////////////////////////
// Views
//////////////////////////////////////////////////////////////////////////*/
private View searchToolbarContainer;
private AutoCompleteTextView searchEditText;
private View searchClear;
/*////////////////////////////////////////////////////////////////////////*/
public static SearchFragment getInstance(int serviceId, String query) {
SearchFragment searchFragment = new SearchFragment();
searchFragment.setQuery(serviceId, query);
searchFragment.searchOnResume();
return searchFragment;
}
/**
* Set wasLoading to true so when the fragment onResume is called, the initial search is done.
* (it will only start searching if the query is not null or empty)
*/
private void searchOnResume() {
if (!TextUtils.isEmpty(searchQuery)) {
wasLoading.set(true);
}
}
/*//////////////////////////////////////////////////////////////////////////
// Fragment's LifeCycle
//////////////////////////////////////////////////////////////////////////*/
@Override
public void onAttach(Context context) {
super.onAttach(context);
suggestionListAdapter = new SuggestionListAdapter(activity);
}
@Override
public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
return inflater.inflate(R.layout.fragment_search, container, false);
}
@Override
public void onPause() {
super.onPause();
wasSearchFocused = searchEditText.hasFocus();
if (searchDisposable != null) searchDisposable.dispose();
if (suggestionWorkerDisposable != null) suggestionWorkerDisposable.dispose();
hideSoftKeyboard(searchEditText);
}
@Override
public void onResume() {
if (DEBUG) Log.d(TAG, "onResume() called");
super.onResume();
SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(activity);
showSuggestions = preferences.getBoolean(getString(R.string.show_search_suggestions_key), true);
searchLanguage = preferences.getString(getString(R.string.search_language_key), getString(R.string.default_language_value));
if (!TextUtils.isEmpty(searchQuery)) {
if (wasLoading.getAndSet(false)) {
if (currentNextPage > currentPage) loadMoreItems();
else search(searchQuery);
} else if (infoListAdapter.getItemsList().size() == 0) {
if (savedState == null) {
search(searchQuery);
} else if (!isLoading.get() && !wasSearchFocused) {
infoListAdapter.clearStreamItemList();
showEmptyState();
}
}
}
if (suggestionWorkerDisposable == null || suggestionWorkerDisposable.isDisposed()) initSuggestionObserver();
}
@Override
public void onDestroyView() {
if (DEBUG) Log.d(TAG, "onDestroyView() called");
unsetSearchListeners();
super.onDestroyView();
}
@Override
public void onDestroy() {
super.onDestroy();
if (!activity.isChangingConfigurations()) StateSaver.onDestroy(savedState);
if (searchDisposable != null) searchDisposable.dispose();
if (suggestionWorkerDisposable != null) suggestionWorkerDisposable.dispose();
}
@Override
public void onActivityResult(int requestCode, int resultCode, Intent data) {
switch (requestCode) {
case ReCaptchaActivity.RECAPTCHA_REQUEST:
if (resultCode == Activity.RESULT_OK && searchQuery.length() != 0) {
search(searchQuery);
} else Log.e(TAG, "ReCaptcha failed");
break;
default:
Log.e(TAG, "Request code from activity not supported [" + requestCode + "]");
break;
}
}
/*//////////////////////////////////////////////////////////////////////////
// State Saving
//////////////////////////////////////////////////////////////////////////*/
@Override
public void writeTo(Queue<Object> objectsToSave) {
super.writeTo(objectsToSave);
objectsToSave.add(currentPage);
objectsToSave.add(currentNextPage);
}
@Override
public void readFrom(@NonNull Queue<Object> savedObjects) throws Exception {
super.readFrom(savedObjects);
currentPage = (int) savedObjects.poll();
currentNextPage = (int) savedObjects.poll();
}
@Override
public void onSaveInstanceState(Bundle bundle) {
searchQuery = searchEditText != null && !TextUtils.isEmpty(searchEditText.getText().toString())
? searchEditText.getText().toString() : searchQuery;
super.onSaveInstanceState(bundle);
}
/*//////////////////////////////////////////////////////////////////////////
// Init's
//////////////////////////////////////////////////////////////////////////*/
@Override
public void reloadContent() {
if (!TextUtils.isEmpty(searchQuery) || (searchEditText != null && !TextUtils.isEmpty(searchEditText.getText()))) {
search(!TextUtils.isEmpty(searchQuery) ? searchQuery : searchEditText.getText().toString());
} else {
if (searchEditText != null) {
searchEditText.setText("");
showSoftKeyboard(searchEditText);
}
animateView(errorPanelRoot, false, 200);
}
}
/*//////////////////////////////////////////////////////////////////////////
// Menu
//////////////////////////////////////////////////////////////////////////*/
@Override
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
super.onCreateOptionsMenu(menu, inflater);
ActionBar supportActionBar = activity.getSupportActionBar();
if (supportActionBar != null) {
supportActionBar.setDisplayShowTitleEnabled(false);
supportActionBar.setDisplayHomeAsUpEnabled(true);
}
inflater.inflate(R.menu.menu_search, menu);
searchToolbarContainer = activity.findViewById(R.id.toolbar_search_container);
searchEditText = searchToolbarContainer.findViewById(R.id.toolbar_search_edit_text);
searchClear = searchToolbarContainer.findViewById(R.id.toolbar_search_clear);
setupSearchView();
restoreFilterChecked(menu, filterItemCheckedId);
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
case R.id.menu_filter_all:
case R.id.menu_filter_video:
case R.id.menu_filter_channel:
case R.id.menu_filter_playlist:
changeFilter(item, getFilterFromMenuId(item.getItemId()));
return true;
default:
return super.onOptionsItemSelected(item);
}
}
private void restoreFilterChecked(Menu menu, int itemId) {
if (itemId != -1) {
MenuItem item = menu.findItem(itemId);
if (item == null) return;
item.setChecked(true);
filter = getFilterFromMenuId(itemId);
}
}
private SearchEngine.Filter getFilterFromMenuId(int itemId) {
switch (itemId) {
case R.id.menu_filter_all:
return SearchEngine.Filter.ANY;
case R.id.menu_filter_video:
return SearchEngine.Filter.STREAM;
case R.id.menu_filter_channel:
return SearchEngine.Filter.CHANNEL;
case R.id.menu_filter_playlist:
return SearchEngine.Filter.PLAYLIST;
default:
return SearchEngine.Filter.ANY;
}
}
/*//////////////////////////////////////////////////////////////////////////
// Search
//////////////////////////////////////////////////////////////////////////*/
private TextWatcher textWatcher;
private void setupSearchView() {
searchEditText.setText(searchQuery != null ? searchQuery : "");
searchEditText.setAdapter(suggestionListAdapter);
if (TextUtils.isEmpty(searchQuery) || TextUtils.isEmpty(searchEditText.getText())) {
searchToolbarContainer.setTranslationX(100);
searchToolbarContainer.setAlpha(0f);
searchToolbarContainer.setVisibility(View.VISIBLE);
searchToolbarContainer.animate().translationX(0).alpha(1f).setDuration(200).setInterpolator(new DecelerateInterpolator()).start();
} else {
searchToolbarContainer.setTranslationX(0);
searchToolbarContainer.setAlpha(1f);
searchToolbarContainer.setVisibility(View.VISIBLE);
}
initSearchListeners();
if (TextUtils.isEmpty(searchQuery) || wasSearchFocused) showSoftKeyboard(searchEditText);
else hideSoftKeyboard(searchEditText);
wasSearchFocused = false;
}
private void initSearchListeners() {
searchClear.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
if (DEBUG) Log.d(TAG, "onClick() called with: v = [" + v + "]");
if (TextUtils.isEmpty(searchEditText.getText())) {
NavigationHelper.gotoMainFragment(getFragmentManager());
return;
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
searchEditText.setText("", false);
} else searchEditText.setText("");
suggestionListAdapter.updateAdapter(new ArrayList<String>());
showSoftKeyboard(searchEditText);
}
});
TooltipCompat.setTooltipText(searchClear, getString(R.string.clear));
searchEditText.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
if (DEBUG) Log.d(TAG, "onClick() called with: v = [" + v + "]");
searchEditText.showDropDown();
}
});
searchEditText.setOnFocusChangeListener(new View.OnFocusChangeListener() {
@Override
public void onFocusChange(View v, boolean hasFocus) {
if (DEBUG) Log.d(TAG, "onFocusChange() called with: v = [" + v + "], hasFocus = [" + hasFocus + "]");
if (hasFocus) searchEditText.showDropDown();
}
});
searchEditText.setOnItemClickListener(new AdapterView.OnItemClickListener() {
@Override
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
if (DEBUG) {
Log.d(TAG, "onItemClick() called with: parent = [" + parent + "], view = [" + view + "], position = [" + position + "], id = [" + id + "]");
}
String s = suggestionListAdapter.getSuggestion(position);
if (DEBUG) Log.d(TAG, "onItemClick text = " + s);
submitQuery(s);
}
});
searchEditText.setThreshold(THRESHOLD_SUGGESTION);
if (textWatcher != null) searchEditText.removeTextChangedListener(textWatcher);
textWatcher = new TextWatcher() {
@Override
public void beforeTextChanged(CharSequence s, int start, int count, int after) {
}
@Override
public void onTextChanged(CharSequence s, int start, int before, int count) {
}
@Override
public void afterTextChanged(Editable s) {
String newText = searchEditText.getText().toString();
if (!TextUtils.isEmpty(newText)) suggestionPublisher.onNext(newText);
}
};
searchEditText.addTextChangedListener(textWatcher);
searchEditText.setOnEditorActionListener(new TextView.OnEditorActionListener() {
@Override
public boolean onEditorAction(TextView v, int actionId, KeyEvent event) {
if (DEBUG)
Log.d(TAG, "onEditorAction() called with: v = [" + v + "], actionId = [" + actionId + "], event = [" + event + "]");
if (event != null && (event.getKeyCode() == KeyEvent.KEYCODE_ENTER || event.getAction() == EditorInfo.IME_ACTION_SEARCH)) {
submitQuery(searchEditText.getText().toString());
return true;
}
return false;
}
});
if (suggestionWorkerDisposable == null || suggestionWorkerDisposable.isDisposed()) initSuggestionObserver();
}
private void unsetSearchListeners() {
searchClear.setOnClickListener(null);
searchClear.setOnLongClickListener(null);
searchEditText.setOnClickListener(null);
searchEditText.setOnItemClickListener(null);
searchEditText.setOnFocusChangeListener(null);
searchEditText.setOnEditorActionListener(null);
if (textWatcher != null) searchEditText.removeTextChangedListener(textWatcher);
textWatcher = null;
}
private void showSoftKeyboard(View view) {
if (DEBUG) Log.d(TAG, "showSoftKeyboard() called with: view = [" + view + "]");
if (view == null) return;
if (view.requestFocus()) {
InputMethodManager imm = (InputMethodManager) activity.getSystemService(Context.INPUT_METHOD_SERVICE);
imm.showSoftInput(view, InputMethodManager.SHOW_IMPLICIT);
}
}
private void hideSoftKeyboard(View view) {
if (DEBUG) Log.d(TAG, "hideSoftKeyboard() called with: view = [" + view + "]");
if (view == null) return;
InputMethodManager imm = (InputMethodManager) activity.getSystemService(Context.INPUT_METHOD_SERVICE);
imm.hideSoftInputFromWindow(view.getWindowToken(), InputMethodManager.HIDE_NOT_ALWAYS);
view.clearFocus();
}
public void giveSearchEditTextFocus() {
showSoftKeyboard(searchEditText);
}
private void initSuggestionObserver() {
if (suggestionWorkerDisposable != null) suggestionWorkerDisposable.dispose();
final Predicate<String> checkEnabledAndLength = new Predicate<String>() {
@Override
public boolean test(@io.reactivex.annotations.NonNull String s) throws Exception {
boolean lengthCheck = s.length() >= THRESHOLD_SUGGESTION;
// Clear the suggestions adapter if the length check fails
if (!lengthCheck && !suggestionListAdapter.isEmpty()) {
suggestionListAdapter.updateAdapter(new ArrayList<String>());
}
// Only pass through if suggestions is enabled and the query length is equal or greater than THRESHOLD_SUGGESTION
return showSuggestions && lengthCheck;
}
};
suggestionWorkerDisposable = suggestionPublisher
.debounce(SUGGESTIONS_DEBOUNCE, TimeUnit.MILLISECONDS)
.startWith(!TextUtils.isEmpty(searchQuery) ? searchQuery : "")
.filter(checkEnabledAndLength)
.switchMap(new Function<String, Observable<Notification<List<String>>>>() {
@Override
public Observable<Notification<List<String>>> apply(@io.reactivex.annotations.NonNull String query) throws Exception {
return ExtractorHelper.suggestionsFor(serviceId, query, searchLanguage).toObservable().materialize();
}
})
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(new Consumer<Notification<List<String>>>() {
@Override
public void accept(@io.reactivex.annotations.NonNull Notification<List<String>> listNotification) throws Exception {
if (listNotification.isOnNext()) {
handleSuggestions(listNotification.getValue());
if (errorPanelRoot.getVisibility() == View.VISIBLE) {
hideLoading();
}
} else if (listNotification.isOnError()) {
Throwable error = listNotification.getError();
if (!ExtractorHelper.isInterruptedCaused(error)) {
onSuggestionError(error);
}
}
}
});
}
@Override
protected void doInitialLoadLogic() {
// no-op
}
private void search(final String query) {
if (DEBUG) Log.d(TAG, "search() called with: query = [" + query + "]");
hideSoftKeyboard(searchEditText);
this.searchQuery = query;
this.currentPage = 0;
infoListAdapter.clearStreamItemList();
if (activity instanceof HistoryListener) {
((HistoryListener) activity).onSearch(serviceId, query);
}
final SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(getContext());
final String searchLanguageKey = getContext().getString(R.string.search_language_key);
searchLanguage = sharedPreferences.getString(searchLanguageKey, getContext().getString(R.string.default_language_value));
startLoading(false);
}
@Override
public void startLoading(boolean forceLoad) {
super.startLoading(forceLoad);
if (searchDisposable != null) searchDisposable.dispose();
searchDisposable = ExtractorHelper.searchFor(serviceId, searchQuery, currentPage, searchLanguage, 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);
}
});
}
@Override
protected void loadMoreItems() {
isLoading.set(true);
showListFooter(true);
if (searchDisposable != null) searchDisposable.dispose();
currentNextPage = currentPage + 1;
searchDisposable = ExtractorHelper.getMoreSearchItems(serviceId, searchQuery, currentNextPage, searchLanguage, 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);
}
});
}
@Override
protected boolean hasMoreItems() {
// TODO: No way to tell if search has more items in the moment
return true;
}
@Override
protected void onItemSelected(InfoItem selectedItem) {
super.onItemSelected(selectedItem);
hideSoftKeyboard(searchEditText);
}
/*//////////////////////////////////////////////////////////////////////////
// Utils
//////////////////////////////////////////////////////////////////////////*/
private void changeFilter(MenuItem item, SearchEngine.Filter filter) {
this.filter = filter;
this.filterItemCheckedId = item.getItemId();
item.setChecked(true);
if (searchQuery != null && !searchQuery.isEmpty()) search(searchQuery);
}
private void submitQuery(String query) {
if (DEBUG) Log.d(TAG, "submitQuery() called with: query = [" + query + "]");
if (query.isEmpty()) return;
search(query);
}
private void setQuery(int serviceId, String searchQuery) {
this.serviceId = serviceId;
this.searchQuery = searchQuery;
}
@Override
public void showError(String message, boolean showRetryButton) {
super.showError(message, showRetryButton);
hideSoftKeyboard(searchEditText);
}
/*//////////////////////////////////////////////////////////////////////////
// Suggestion Results
//////////////////////////////////////////////////////////////////////////*/
public void handleSuggestions(@NonNull List<String> suggestions) {
if (DEBUG) Log.d(TAG, "handleSuggestions() called with: suggestions = [" + suggestions + "]");
suggestionListAdapter.updateAdapter(suggestions);
}
public void onSuggestionError(Throwable exception) {
if (DEBUG) Log.d(TAG, "onSuggestionError() called with: exception = [" + exception + "]");
if (super.onError(exception)) return;
int errorId = exception instanceof ParsingException ? R.string.parsing_error : R.string.general_error;
onUnrecoverableError(exception, UserAction.GET_SUGGESTIONS, NewPipe.getNameOfService(serviceId), searchQuery, errorId);
}
/*//////////////////////////////////////////////////////////////////////////
// Contract
//////////////////////////////////////////////////////////////////////////*/
@Override
public void hideLoading() {
super.hideLoading();
showListFooter(false);
}
/*//////////////////////////////////////////////////////////////////////////
// Search Results
//////////////////////////////////////////////////////////////////////////*/
@Override
public void handleResult(@NonNull SearchResult result) {
if (!result.errors.isEmpty()) {
showSnackBarError(result.errors, UserAction.SEARCHED, NewPipe.getNameOfService(serviceId), searchQuery, 0);
}
if (infoListAdapter.getItemsList().size() == 0) {
if (result.resultList.size() > 0) {
infoListAdapter.addInfoItemList(result.resultList);
} else {
infoListAdapter.clearStreamItemList();
showEmptyState();
return;
}
}
super.handleResult(result);
}
@Override
public void handleNextItems(ListExtractor.NextItemsResult result) {
showListFooter(false);
currentPage = Integer.parseInt(result.nextItemsUrl);
infoListAdapter.addInfoItemList(result.nextItemsList);
if (!result.errors.isEmpty()) {
showSnackBarError(result.errors, UserAction.SEARCHED, NewPipe.getNameOfService(serviceId)
, "\"" + searchQuery + "\" → page " + currentPage, 0);
}
super.handleNextItems(result);
}
@Override
protected boolean onError(Throwable exception) {
if (super.onError(exception)) return true;
if (exception instanceof SearchEngine.NothingFoundException) {
infoListAdapter.clearStreamItemList();
showEmptyState();
} else {
int errorId = exception instanceof ParsingException ? R.string.parsing_error : R.string.general_error;
onUnrecoverableError(exception, UserAction.SEARCHED, NewPipe.getNameOfService(serviceId), searchQuery, errorId);
}
return true;
}
}

View File

@@ -1,4 +1,4 @@
package org.schabi.newpipe.fragments.search;
package org.schabi.newpipe.fragments.list.search;
import android.content.Context;
import android.database.Cursor;
@@ -9,7 +9,7 @@ import android.widget.TextView;
import java.util.List;
/**
/*
* Created by Christian Schabesberger on 02.08.16.
*
* Copyright (C) Christian Schabesberger 2016 <chris.schabesberger@mailbox.org>
@@ -83,7 +83,7 @@ public class SuggestionListAdapter extends ResourceCursorAdapter {
private class ViewHolder {
private final TextView suggestionTitle;
private ViewHolder(View view) {
this.suggestionTitle = (TextView) view.findViewById(android.R.id.text1);
this.suggestionTitle = view.findViewById(android.R.id.text1);
}
}
}

View File

@@ -1,33 +0,0 @@
package org.schabi.newpipe.fragments.search;
import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView;
/**
* Recycler view scroll listener which calls the method {@link #onScrolledDown(RecyclerView)}
* if the view is scrolled below the last item.
*/
public abstract class OnScrollBelowItemsListener extends RecyclerView.OnScrollListener {
@Override
public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
super.onScrolled(recyclerView, dx, dy);
//check for scroll down
if (dy > 0) {
int pastVisibleItems, visibleItemCount, totalItemCount;
LinearLayoutManager layoutManager = (LinearLayoutManager) recyclerView.getLayoutManager();
visibleItemCount = recyclerView.getLayoutManager().getChildCount();
totalItemCount = recyclerView.getLayoutManager().getItemCount();
pastVisibleItems = layoutManager.findFirstVisibleItemPosition();
if ((visibleItemCount + pastVisibleItems) >= totalItemCount) {
onScrolledDown(recyclerView);
}
}
}
/**
* Called when the recycler view is scrolled below the last item.
* @param recyclerView the recycler view
*/
public abstract void onScrolledDown(RecyclerView recyclerView);
}

View File

@@ -1,615 +0,0 @@
package org.schabi.newpipe.fragments.search;
import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.os.Build;
import android.os.Bundle;
import android.preference.PreferenceManager;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.v7.app.ActionBar;
import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView;
import android.text.Editable;
import android.text.TextUtils;
import android.text.TextWatcher;
import android.util.Log;
import android.view.KeyEvent;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.view.animation.DecelerateInterpolator;
import android.view.inputmethod.EditorInfo;
import android.view.inputmethod.InputMethodManager;
import android.widget.AdapterView;
import android.widget.AutoCompleteTextView;
import android.widget.TextView;
import android.widget.Toast;
import org.schabi.newpipe.R;
import org.schabi.newpipe.ReCaptchaActivity;
import org.schabi.newpipe.extractor.InfoItem;
import org.schabi.newpipe.extractor.search.SearchEngine;
import org.schabi.newpipe.extractor.search.SearchResult;
import org.schabi.newpipe.fragments.BaseFragment;
import org.schabi.newpipe.info_list.InfoItemBuilder;
import org.schabi.newpipe.info_list.InfoListAdapter;
import org.schabi.newpipe.util.Constants;
import org.schabi.newpipe.util.NavigationHelper;
import org.schabi.newpipe.workers.SearchWorker;
import org.schabi.newpipe.workers.SuggestionWorker;
import java.util.ArrayList;
import java.util.EnumSet;
import java.util.List;
import static org.schabi.newpipe.util.AnimationUtils.animateView;
public class SearchFragment extends BaseFragment implements SuggestionWorker.OnSuggestionResult, SearchWorker.OnSearchResult {
private final String TAG = "SearchFragment@" + Integer.toHexString(hashCode());
// savedInstanceBundle arguments
private static final String QUERY_KEY = "query_key";
private static final String PAGE_NUMBER_KEY = "page_number_key";
private static final String INFO_LIST_KEY = "info_list_key";
private static final String WAS_LOADING_KEY = "was_loading_key";
private static final String ERROR_KEY = "error_key";
private static final String FILTER_CHECKED_ID_KEY = "filter_checked_id_key";
/*//////////////////////////////////////////////////////////////////////////
// Search
//////////////////////////////////////////////////////////////////////////*/
private int filterItemCheckedId = -1;
private EnumSet<SearchEngine.Filter> filter = EnumSet.of(SearchEngine.Filter.CHANNEL, SearchEngine.Filter.STREAM);
private int serviceId = -1;
private String searchQuery = "";
private int pageNumber = 0;
private boolean showSuggestions = true;
private SearchWorker curSearchWorker;
private SuggestionWorker curSuggestionWorker;
private SuggestionListAdapter suggestionListAdapter;
private InfoListAdapter infoListAdapter;
/*//////////////////////////////////////////////////////////////////////////
// Views
//////////////////////////////////////////////////////////////////////////*/
private View searchToolbarContainer;
private AutoCompleteTextView searchEditText;
private View searchClear;
private RecyclerView resultRecyclerView;
/*////////////////////////////////////////////////////////////////////////*/
public static SearchFragment getInstance(int serviceId, String query) {
SearchFragment searchFragment = new SearchFragment();
searchFragment.setQuery(serviceId, query);
return searchFragment;
}
/*//////////////////////////////////////////////////////////////////////////
// Fragment's LifeCycle
//////////////////////////////////////////////////////////////////////////*/
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
if (DEBUG) Log.d(TAG, "onCreate() called with: savedInstanceState = [" + savedInstanceState + "]");
setHasOptionsMenu(true);
if (savedInstanceState != null) {
searchQuery = savedInstanceState.getString(QUERY_KEY);
serviceId = savedInstanceState.getInt(Constants.KEY_SERVICE_ID, 0);
pageNumber = savedInstanceState.getInt(PAGE_NUMBER_KEY, 0);
wasLoading.set(savedInstanceState.getBoolean(WAS_LOADING_KEY, false));
filterItemCheckedId = savedInstanceState.getInt(FILTER_CHECKED_ID_KEY, 0);
}
}
@Override
public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
if (DEBUG) Log.d(TAG, "onCreateView() called with: inflater = [" + inflater + "], container = [" + container + "], savedInstanceState = [" + savedInstanceState + "]");
return inflater.inflate(R.layout.fragment_search, container, false);
}
@Override
public void onViewCreated(View rootView, @Nullable Bundle savedInstanceState) {
super.onViewCreated(rootView, savedInstanceState);
if (DEBUG) Log.d(TAG, "onViewCreated() called with: rootView = [" + rootView + "], savedInstanceState = [" + savedInstanceState + "]");
if (savedInstanceState != null && savedInstanceState.getBoolean(ERROR_KEY, false)) {
search(searchQuery, 0, true);
}
}
@Override
public void onResume() {
super.onResume();
if (DEBUG) Log.d(TAG, "onResume() called");
if (wasLoading.getAndSet(false) && !TextUtils.isEmpty(searchQuery)) {
if (pageNumber > 0) search(searchQuery, pageNumber);
else search(searchQuery, 0, true);
}
showSuggestions = PreferenceManager.getDefaultSharedPreferences(activity).getBoolean(getString(R.string.show_search_suggestions_key), true);
}
@Override
public void onStop() {
super.onStop();
if (DEBUG) Log.d(TAG, "onStop() called");
hideSoftKeyboard(searchEditText);
wasLoading.set(curSearchWorker != null && curSearchWorker.isRunning());
if (curSearchWorker != null && curSearchWorker.isRunning()) curSearchWorker.cancel();
if (curSuggestionWorker != null && curSuggestionWorker.isRunning()) curSuggestionWorker.cancel();
}
@Override
public void onDestroyView() {
if (DEBUG) Log.d(TAG, "onDestroyView() called");
unsetSearchListeners();
resultRecyclerView.removeAllViews();
searchToolbarContainer = null;
searchEditText = null;
searchClear = null;
resultRecyclerView = null;
super.onDestroyView();
}
@Override
public void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
if (DEBUG) Log.d(TAG, "onSaveInstanceState() called with: outState = [" + outState + "]");
String query = searchEditText != null && !TextUtils.isEmpty(searchEditText.getText().toString())
? searchEditText.getText().toString() : searchQuery;
outState.putString(QUERY_KEY, query);
outState.putInt(Constants.KEY_SERVICE_ID, serviceId);
outState.putInt(PAGE_NUMBER_KEY, pageNumber);
outState.putSerializable(INFO_LIST_KEY, ((ArrayList<InfoItem>) infoListAdapter.getItemsList()));
outState.putBoolean(WAS_LOADING_KEY, curSearchWorker != null && curSearchWorker.isRunning());
if (errorPanel != null && errorPanel.getVisibility() == View.VISIBLE) outState.putBoolean(ERROR_KEY, true);
if (filterItemCheckedId != -1) outState.putInt(FILTER_CHECKED_ID_KEY, filterItemCheckedId);
}
@Override
public void onActivityResult(int requestCode, int resultCode, Intent data) {
switch (requestCode) {
case ReCaptchaActivity.RECAPTCHA_REQUEST:
if (resultCode == Activity.RESULT_OK && searchQuery.length() != 0) {
search(searchQuery, pageNumber, true);
} else Log.e(TAG, "ReCaptcha failed");
break;
default:
Log.e(TAG, "Request code from activity not supported [" + requestCode + "]");
break;
}
}
/*//////////////////////////////////////////////////////////////////////////
// Init's
//////////////////////////////////////////////////////////////////////////*/
@Override
protected void initViews(View rootView, Bundle savedInstanceState) {
super.initViews(rootView, savedInstanceState);
resultRecyclerView = ((RecyclerView) rootView.findViewById(R.id.result_list_view));
resultRecyclerView.setLayoutManager(new LinearLayoutManager(activity));
if (infoListAdapter == null) {
infoListAdapter = new InfoListAdapter(getActivity());
if (savedInstanceState != null) {
//noinspection unchecked
ArrayList<InfoItem> serializable = (ArrayList<InfoItem>) savedInstanceState.getSerializable(INFO_LIST_KEY);
infoListAdapter.addInfoItemList(serializable);
}
infoListAdapter.setFooter(activity.getLayoutInflater().inflate(R.layout.pignate_footer, resultRecyclerView, false));
infoListAdapter.showFooter(false);
infoListAdapter.setOnStreamInfoItemSelectedListener(new InfoItemBuilder.OnInfoItemSelectedListener() {
@Override
public void selected(int serviceId, String url, String title) {
NavigationHelper.openVideoDetailFragment(getFragmentManager(), serviceId, url, title);
}
});
infoListAdapter.setOnChannelInfoItemSelectedListener(new InfoItemBuilder.OnInfoItemSelectedListener() {
@Override
public void selected(int serviceId, String url, String title) {
NavigationHelper.openChannelFragment(getFragmentManager(), serviceId, url, title);
}
});
}
resultRecyclerView.setAdapter(infoListAdapter);
}
@Override
protected void initListeners() {
super.initListeners();
resultRecyclerView.clearOnScrollListeners();
resultRecyclerView.addOnScrollListener(new OnScrollBelowItemsListener() {
@Override
public void onScrolledDown(RecyclerView recyclerView) {
if(!isLoading.get()) {
pageNumber++;
recyclerView.post(new Runnable() {
@Override
public void run() {
infoListAdapter.showFooter(true);
}
});
search(searchQuery, pageNumber);
}
}
});
}
@Override
protected void reloadContent() {
if (DEBUG) Log.d(TAG, "reloadContent() called");
if (!TextUtils.isEmpty(searchQuery) || (searchEditText != null && !TextUtils.isEmpty(searchEditText.getText()))) {
search(!TextUtils.isEmpty(searchQuery) ? searchQuery : searchEditText.getText().toString(), 0, true);
} else {
if (searchEditText != null) {
searchEditText.setText("");
showSoftKeyboard(searchEditText);
}
animateView(errorPanel, false, 200);
}
}
/*//////////////////////////////////////////////////////////////////////////
// Menu
//////////////////////////////////////////////////////////////////////////*/
@Override
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
super.onCreateOptionsMenu(menu, inflater);
if (DEBUG) Log.d(TAG, "onCreateOptionsMenu() called with: menu = [" + menu + "], inflater = [" + inflater + "]");
inflater.inflate(R.menu.search_menu, menu);
ActionBar supportActionBar = activity.getSupportActionBar();
if (supportActionBar != null) {
supportActionBar.setDisplayShowTitleEnabled(false);
supportActionBar.setDisplayHomeAsUpEnabled(true);
}
searchToolbarContainer = activity.findViewById(R.id.toolbar_search_container);
searchEditText = (AutoCompleteTextView) searchToolbarContainer.findViewById(R.id.toolbar_search_edit_text);
searchClear = searchToolbarContainer.findViewById(R.id.toolbar_search_clear);
setupSearchView();
restoreFilterChecked(menu, filterItemCheckedId);
}
private void restoreFilterChecked(Menu menu, int itemId) {
if (itemId != -1) {
MenuItem item = menu.findItem(itemId);
if (item == null) return;
item.setChecked(true);
switch (itemId) {
case R.id.menu_filter_all:
filter = EnumSet.of(SearchEngine.Filter.STREAM, SearchEngine.Filter.CHANNEL);
break;
case R.id.menu_filter_video:
filter = EnumSet.of(SearchEngine.Filter.STREAM);
break;
case R.id.menu_filter_channel:
filter = EnumSet.of(SearchEngine.Filter.CHANNEL);
break;
}
}
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
if (DEBUG) Log.d(TAG, "onOptionsItemSelected() called with: item = [" + item + "]");
switch (item.getItemId()) {
case R.id.menu_filter_all:
changeFilter(item, EnumSet.of(SearchEngine.Filter.STREAM, SearchEngine.Filter.CHANNEL));
return true;
case R.id.menu_filter_video:
changeFilter(item, EnumSet.of(SearchEngine.Filter.STREAM));
return true;
case R.id.menu_filter_channel:
changeFilter(item, EnumSet.of(SearchEngine.Filter.CHANNEL));
return true;
default:
return false;
}
}
/*//////////////////////////////////////////////////////////////////////////
// Search
//////////////////////////////////////////////////////////////////////////*/
private TextWatcher textWatcher;
private void setupSearchView() {
searchEditText.setText(searchQuery != null ? searchQuery : "");
searchEditText.setHint(getString(R.string.search) + "...");
////searchEditText.setCursorVisible(true);
suggestionListAdapter = new SuggestionListAdapter(activity);
searchEditText.setAdapter(suggestionListAdapter);
if (TextUtils.isEmpty(searchQuery) || TextUtils.isEmpty(searchEditText.getText())) {
searchToolbarContainer.setTranslationX(100);
searchToolbarContainer.setAlpha(0f);
searchToolbarContainer.setVisibility(View.VISIBLE);
searchToolbarContainer.animate().translationX(0).alpha(1f).setDuration(400).setInterpolator(new DecelerateInterpolator()).start();
} else {
searchToolbarContainer.setTranslationX(0);
searchToolbarContainer.setAlpha(1f);
searchToolbarContainer.setVisibility(View.VISIBLE);
}
//
initSearchListeners();
if (TextUtils.isEmpty(searchQuery)) showSoftKeyboard(searchEditText);
else hideSoftKeyboard(searchEditText);
if (!TextUtils.isEmpty(searchQuery) && searchQuery.length() > 2 && suggestionListAdapter != null && suggestionListAdapter.isEmpty()) {
searchSuggestions(searchQuery);
}
}
private void initSearchListeners() {
searchClear.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
if (DEBUG) Log.d(TAG, "onClick() called with: v = [" + v + "]");
if (TextUtils.isEmpty(searchEditText.getText())) {
NavigationHelper.gotoMainFragment(getFragmentManager());
return;
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
searchEditText.setText("", false);
} else searchEditText.setText("");
suggestionListAdapter.updateAdapter(new ArrayList<String>());
showSoftKeyboard(searchEditText);
}
});
searchClear.setOnLongClickListener(new View.OnLongClickListener() {
@Override
public boolean onLongClick(View v) {
if (DEBUG) Log.d(TAG, "onLongClick() called with: v = [" + v + "]");
showMenuTooltip(v, getString(R.string.clear));
return true;
}
});
searchEditText.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
searchEditText.showDropDown();
}
});
searchEditText.setOnFocusChangeListener(new View.OnFocusChangeListener() {
@Override
public void onFocusChange(View v, boolean hasFocus) {
if (hasFocus) searchEditText.showDropDown();
}
});
searchEditText.setOnItemClickListener(new AdapterView.OnItemClickListener() {
@Override
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
if (DEBUG) Log.d(TAG, "onItemClick() called with: parent = [" + parent + "], view = [" + view + "], position = [" + position + "], id = [" + id + "]");
String s = suggestionListAdapter.getSuggestion(position);
if (DEBUG) Log.d(TAG, "onItemClick text = " + s);
submitQuery(s);
}
});
if (textWatcher != null) searchEditText.removeTextChangedListener(textWatcher);
textWatcher = new TextWatcher() {
@Override
public void beforeTextChanged(CharSequence s, int start, int count, int after) {
}
@Override
public void onTextChanged(CharSequence s, int start, int before, int count) {
}
@Override
public void afterTextChanged(Editable s) {
String newText = searchEditText.getText().toString();
if (!TextUtils.isEmpty(newText) && newText.length() > 1) onQueryTextChange(newText);
}
};
searchEditText.addTextChangedListener(textWatcher);
searchEditText.setOnEditorActionListener(new TextView.OnEditorActionListener() {
@Override
public boolean onEditorAction(TextView v, int actionId, KeyEvent event) {
if (DEBUG) Log.d(TAG, "onEditorAction() called with: v = [" + v + "], actionId = [" + actionId + "], event = [" + event + "]");
if (event != null && (event.getKeyCode() == KeyEvent.KEYCODE_ENTER || event.getAction() == EditorInfo.IME_ACTION_SEARCH)) {
submitQuery(searchEditText.getText().toString());
return true;
}
return false;
}
});
}
private void unsetSearchListeners() {
searchClear.setOnClickListener(null);
searchClear.setOnLongClickListener(null);
if (textWatcher != null) searchEditText.removeTextChangedListener(textWatcher);
searchEditText.setOnClickListener(null);
searchEditText.setOnItemClickListener(null);
searchEditText.setOnFocusChangeListener(null);
searchEditText.setOnEditorActionListener(null);
textWatcher = null;
}
public void showSoftKeyboard(View view) {
if (DEBUG) Log.d(TAG, "showSoftKeyboard() called with: view = [" + view + "]");
if (view == null) return;
if (view.requestFocus()) {
InputMethodManager imm = (InputMethodManager) activity.getSystemService(Context.INPUT_METHOD_SERVICE);
imm.showSoftInput(view, InputMethodManager.SHOW_IMPLICIT);
}
}
public void hideSoftKeyboard(View view) {
if (DEBUG) Log.d(TAG, "hideSoftKeyboard() called with: view = [" + view + "]");
if (view == null) return;
InputMethodManager imm = (InputMethodManager) activity.getSystemService(Context.INPUT_METHOD_SERVICE);
imm.hideSoftInputFromWindow(view.getWindowToken(), InputMethodManager.HIDE_NOT_ALWAYS);
view.clearFocus();
}
/*//////////////////////////////////////////////////////////////////////////
// Utils
//////////////////////////////////////////////////////////////////////////*/
private void changeFilter(MenuItem item, EnumSet<SearchEngine.Filter> filter) {
this.filter = filter;
this.filterItemCheckedId = item.getItemId();
item.setChecked(true);
if (searchQuery != null && !searchQuery.isEmpty()) search(searchQuery, 0, true);
}
public void submitQuery(String query) {
if (DEBUG) Log.d(TAG, "submitQuery() called with: query = [" + query + "]");
if (query.isEmpty()) return;
search(query, 0, true);
searchQuery = query;
}
public void onQueryTextChange(String newText) {
if (DEBUG) Log.d(TAG, "onQueryTextChange() called with: newText = [" + newText + "]");
if (!newText.isEmpty()) searchSuggestions(newText);
}
private void setQuery(int serviceId, String searchQuery) {
this.serviceId = serviceId;
this.searchQuery = searchQuery;
}
private void searchSuggestions(String query) {
if (!showSuggestions) {
if (DEBUG) Log.d(TAG, "searchSuggestions() showSuggestions is disabled");
return;
}
if (DEBUG) Log.d(TAG, "searchSuggestions() called with: query = [" + query + "]");
if (curSuggestionWorker != null && curSuggestionWorker.isRunning()) curSuggestionWorker.cancel();
curSuggestionWorker = SuggestionWorker.startForQuery(activity, serviceId, query, this);
}
private void search(String query, int pageNumber) {
if (DEBUG) Log.d(TAG, "search() called with: query = [" + query + "], pageNumber = [" + pageNumber + "]");
search(query, pageNumber, false);
}
private void search(String query, int pageNumber, boolean clearList) {
if (DEBUG) Log.d(TAG, "search() called with: query = [" + query + "], pageNumber = [" + pageNumber + "], clearList = [" + clearList + "]");
isLoading.set(true);
hideSoftKeyboard(searchEditText);
searchQuery = query;
this.pageNumber = pageNumber;
if (clearList) {
animateView(resultRecyclerView, false, 50);
infoListAdapter.clearStreamItemList();
infoListAdapter.showFooter(false);
animateView(loadingProgressBar, true, 200);
}
animateView(errorPanel, false, 200);
if (curSearchWorker != null && curSearchWorker.isRunning()) curSearchWorker.cancel();
curSearchWorker = SearchWorker.startForQuery(activity, serviceId, query, pageNumber, filter, this);
}
protected void setErrorMessage(String message, boolean showRetryButton) {
super.setErrorMessage(message, showRetryButton);
animateView(resultRecyclerView, false, 400);
hideSoftKeyboard(searchEditText);
}
/*//////////////////////////////////////////////////////////////////////////
// OnSuggestionResult
//////////////////////////////////////////////////////////////////////////*/
@Override
public void onSuggestionResult(@NonNull List<String> suggestions) {
if (DEBUG) Log.d(TAG, "onSuggestionResult() called with: suggestions = [" + suggestions + "]");
suggestionListAdapter.updateAdapter(suggestions);
}
@Override
public void onSuggestionError(int messageId) {
if (DEBUG) Log.d(TAG, "onSuggestionError() called with: messageId = [" + messageId + "]");
setErrorMessage(getString(messageId), true);
}
/*//////////////////////////////////////////////////////////////////////////
// SearchWorkerResultListener
//////////////////////////////////////////////////////////////////////////*/
@Override
public void onSearchResult(SearchResult result) {
if (DEBUG) Log.d(TAG, "onSearchResult() called with: result = [" + result + "]");
infoListAdapter.addInfoItemList(result.resultList);
animateView(resultRecyclerView, true, 400);
animateView(loadingProgressBar, false, 200);
isLoading.set(false);
}
@Override
public void onNothingFound(String message) {
if (DEBUG) Log.d(TAG, "onNothingFound() called with: messageId = [" + message + "]");
setErrorMessage(message, false);
}
@Override
public void onSearchError(int messageId) {
if (DEBUG) Log.d(TAG, "onSearchError() called with: messageId = [" + messageId + "]");
//Toast.makeText(getActivity(), messageId, Toast.LENGTH_LONG).show();
setErrorMessage(getString(messageId), true);
}
@Override
public void onReCaptchaChallenge() {
if (DEBUG) Log.d(TAG, "onReCaptchaChallenge() called");
Toast.makeText(getActivity(), R.string.recaptcha_request_toast, Toast.LENGTH_LONG).show();
setErrorMessage(getString(R.string.recaptcha_request_toast), false);
// Starting ReCaptcha Challenge Activity
startActivityForResult(new Intent(getActivity(), ReCaptchaActivity.class), ReCaptchaActivity.RECAPTCHA_REQUEST);
}
}

View File

@@ -0,0 +1,240 @@
package org.schabi.newpipe.fragments.subscription;
import android.content.Context;
import android.os.Bundle;
import android.os.Parcelable;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import org.schabi.newpipe.R;
import org.schabi.newpipe.database.subscription.SubscriptionEntity;
import org.schabi.newpipe.extractor.InfoItem;
import org.schabi.newpipe.extractor.channel.ChannelInfoItem;
import org.schabi.newpipe.fragments.BaseStateFragment;
import org.schabi.newpipe.info_list.InfoItemBuilder;
import org.schabi.newpipe.info_list.InfoListAdapter;
import org.schabi.newpipe.report.UserAction;
import org.schabi.newpipe.util.NavigationHelper;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import icepick.State;
import io.reactivex.Observer;
import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.disposables.CompositeDisposable;
import io.reactivex.disposables.Disposable;
import io.reactivex.schedulers.Schedulers;
import static org.schabi.newpipe.util.AnimationUtils.animateView;
public class SubscriptionFragment extends BaseStateFragment<List<SubscriptionEntity>> {
private View headerRootLayout;
private InfoListAdapter infoListAdapter;
private RecyclerView itemsList;
@State
protected Parcelable itemsListState;
/* Used for independent events */
private CompositeDisposable disposables = new CompositeDisposable();
private SubscriptionService subscriptionService;
///////////////////////////////////////////////////////////////////////////
// Fragment LifeCycle
///////////////////////////////////////////////////////////////////////////
@Override
public void onAttach(Context context) {
super.onAttach(context);
infoListAdapter = new InfoListAdapter(activity);
subscriptionService = SubscriptionService.getInstance();
}
@Nullable
@Override
public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, Bundle savedInstanceState) {
return inflater.inflate(R.layout.fragment_subscription, container, false);
}
@Override
public void onPause() {
super.onPause();
itemsListState = itemsList.getLayoutManager().onSaveInstanceState();
}
@Override
public void onDestroyView() {
if (disposables != null) disposables.clear();
super.onDestroyView();
}
@Override
public void onDestroy() {
if (disposables != null) disposables.dispose();
disposables = null;
subscriptionService = null;
super.onDestroy();
}
///////////////////////////////////////////////////////////////////////////
// Fragment Views
///////////////////////////////////////////////////////////////////////////
@Override
protected void initViews(View rootView, Bundle savedInstanceState) {
super.initViews(rootView, savedInstanceState);
infoListAdapter = new InfoListAdapter(getActivity());
itemsList = rootView.findViewById(R.id.items_list);
itemsList.setLayoutManager(new LinearLayoutManager(activity));
infoListAdapter.setHeader(headerRootLayout = activity.getLayoutInflater().inflate(R.layout.subscription_header, itemsList, false));
infoListAdapter.useMiniItemVariants(true);
itemsList.setAdapter(infoListAdapter);
}
@Override
protected void initListeners() {
super.initListeners();
infoListAdapter.setOnChannelSelectedListener(new InfoItemBuilder.OnInfoItemSelectedListener<ChannelInfoItem>() {
@Override
public void selected(ChannelInfoItem selectedItem) {
// Requires the parent fragment to find holder for fragment replacement
NavigationHelper.openChannelFragment(getParentFragment().getFragmentManager(), selectedItem.service_id, selectedItem.url, selectedItem.name);
}
});
headerRootLayout.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
NavigationHelper.openWhatsNewFragment(getParentFragment().getFragmentManager());
}
});
}
private void resetFragment() {
if (disposables != null) disposables.clear();
if (infoListAdapter != null) infoListAdapter.clearStreamItemList();
}
///////////////////////////////////////////////////////////////////////////
// Subscriptions Loader
///////////////////////////////////////////////////////////////////////////
@Override
public void startLoading(boolean forceLoad) {
super.startLoading(forceLoad);
resetFragment();
subscriptionService.getSubscription().toObservable()
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(getSubscriptionObserver());
}
private Observer<List<SubscriptionEntity>> getSubscriptionObserver() {
return new Observer<List<SubscriptionEntity>>() {
@Override
public void onSubscribe(Disposable d) {
showLoading();
disposables.add(d);
}
@Override
public void onNext(List<SubscriptionEntity> subscriptions) {
handleResult(subscriptions);
}
@Override
public void onError(Throwable exception) {
SubscriptionFragment.this.onError(exception);
}
@Override
public void onComplete() {
}
};
}
@Override
public void handleResult(@NonNull List<SubscriptionEntity> result) {
super.handleResult(result);
infoListAdapter.clearStreamItemList();
if (result.isEmpty()) {
showEmptyState();
} else {
infoListAdapter.addInfoItemList(getSubscriptionItems(result));
if (itemsListState != null) {
itemsList.getLayoutManager().onRestoreInstanceState(itemsListState);
itemsListState = null;
}
hideLoading();
}
}
private List<InfoItem> getSubscriptionItems(List<SubscriptionEntity> subscriptions) {
List<InfoItem> items = new ArrayList<>();
for (final SubscriptionEntity subscription : subscriptions) items.add(subscription.toChannelInfoItem());
Collections.sort(items, new Comparator<InfoItem>() {
@Override
public int compare(InfoItem o1, InfoItem o2) {
return o1.name.compareToIgnoreCase(o2.name);
}
});
return items;
}
/*//////////////////////////////////////////////////////////////////////////
// Contract
//////////////////////////////////////////////////////////////////////////*/
@Override
public void showLoading() {
super.showLoading();
animateView(itemsList, false, 100);
}
@Override
public void hideLoading() {
super.hideLoading();
animateView(itemsList, true, 200);
}
@Override
public void showEmptyState() {
super.showEmptyState();
animateView(itemsList, false, 200);
}
///////////////////////////////////////////////////////////////////////////
// Fragment Error Handling
///////////////////////////////////////////////////////////////////////////
@Override
protected boolean onError(Throwable exception) {
resetFragment();
if (super.onError(exception)) return true;
onUnrecoverableError(exception, UserAction.SOMETHING_ELSE, "none", "Subscriptions", R.string.general_error);
return true;
}
}

View File

@@ -0,0 +1,147 @@
package org.schabi.newpipe.fragments.subscription;
import android.util.Log;
import org.schabi.newpipe.MainActivity;
import org.schabi.newpipe.NewPipeDatabase;
import org.schabi.newpipe.database.AppDatabase;
import org.schabi.newpipe.database.subscription.SubscriptionDAO;
import org.schabi.newpipe.database.subscription.SubscriptionEntity;
import org.schabi.newpipe.extractor.channel.ChannelInfo;
import org.schabi.newpipe.util.ExtractorHelper;
import java.util.List;
import java.util.concurrent.Executor;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import io.reactivex.Completable;
import io.reactivex.CompletableSource;
import io.reactivex.Flowable;
import io.reactivex.Maybe;
import io.reactivex.Scheduler;
import io.reactivex.annotations.NonNull;
import io.reactivex.functions.Function;
import io.reactivex.schedulers.Schedulers;
/**
* Subscription Service singleton:
* Provides a basis for channel Subscriptions.
* Provides access to subscription table in database as well as
* up-to-date observations on the subscribed channels
*/
public class SubscriptionService {
private static final SubscriptionService sInstance = new SubscriptionService();
public static SubscriptionService getInstance() {
return sInstance;
}
protected final String TAG = "SubscriptionService@" + Integer.toHexString(hashCode());
protected static final boolean DEBUG = MainActivity.DEBUG;
private static final int SUBSCRIPTION_DEBOUNCE_INTERVAL = 500;
private static final int SUBSCRIPTION_THREAD_POOL_SIZE = 4;
private AppDatabase db;
private Flowable<List<SubscriptionEntity>> subscription;
private Scheduler subscriptionScheduler;
private SubscriptionService() {
db = NewPipeDatabase.getInstance();
subscription = getSubscriptionInfos();
final Executor subscriptionExecutor = Executors.newFixedThreadPool(SUBSCRIPTION_THREAD_POOL_SIZE);
subscriptionScheduler = Schedulers.from(subscriptionExecutor);
}
/**
* Part of subscription observation pipeline
*
* @see SubscriptionService#getSubscription()
*/
private Flowable<List<SubscriptionEntity>> getSubscriptionInfos() {
return subscriptionTable().getAll()
// Wait for a period of infrequent updates and return the latest update
.debounce(SUBSCRIPTION_DEBOUNCE_INTERVAL, TimeUnit.MILLISECONDS)
.share() // Share allows multiple subscribers on the same observable
.replay(1) // Replay synchronizes subscribers to the last emitted result
.autoConnect();
}
/**
* Provides an observer to the latest update to the subscription table.
* <p>
* This observer may be subscribed multiple times, where each subscriber obtains
* the latest synchronized changes available, effectively share the same data
* across all subscribers.
* <p>
* This observer has a debounce cooldown, meaning if multiple updates are observed
* in the cooldown interval, only the latest changes are emitted to the subscribers.
* This reduces the amount of observations caused by frequent updates to the database.
*/
@android.support.annotation.NonNull
public Flowable<List<SubscriptionEntity>> getSubscription() {
return subscription;
}
public Maybe<ChannelInfo> getChannelInfo(final SubscriptionEntity subscriptionEntity) {
if (DEBUG) Log.d(TAG, "getChannelInfo() called with: subscriptionEntity = [" + subscriptionEntity + "]");
return Maybe.fromSingle(ExtractorHelper
.getChannelInfo(subscriptionEntity.getServiceId(), subscriptionEntity.getUrl(), false))
.subscribeOn(subscriptionScheduler);
}
/**
* Returns the database access interface for subscription table.
*/
public SubscriptionDAO subscriptionTable() {
return db.subscriptionDAO();
}
public Completable updateChannelInfo(final ChannelInfo info) {
final Function<List<SubscriptionEntity>, CompletableSource> update = new Function<List<SubscriptionEntity>, CompletableSource>() {
@Override
public CompletableSource apply(@NonNull List<SubscriptionEntity> subscriptionEntities) throws Exception {
if (DEBUG) Log.d(TAG, "updateChannelInfo() called with: subscriptionEntities = [" + subscriptionEntities + "]");
if (subscriptionEntities.size() == 1) {
SubscriptionEntity subscription = subscriptionEntities.get(0);
// Subscriber count changes very often, making this check almost unnecessary.
// Consider removing it later.
if (!isSubscriptionUpToDate(info, subscription)) {
subscription.setData(info.name, info.avatar_url, info.description, info.subscriber_count);
return update(subscription);
}
}
return Completable.complete();
}
};
return subscriptionTable().getSubscription(info.service_id, info.url)
.firstOrError()
.flatMapCompletable(update);
}
private Completable update(final SubscriptionEntity updatedSubscription) {
return Completable.fromRunnable(new Runnable() {
@Override
public void run() {
subscriptionTable().update(updatedSubscription);
}
});
}
private boolean isSubscriptionUpToDate(final ChannelInfo info, final SubscriptionEntity entity) {
return info.url.equals(entity.getUrl()) &&
info.service_id == entity.getServiceId() &&
info.name.equals(entity.getName()) &&
info.avatar_url.equals(entity.getAvatarUrl()) &&
info.description.equals(entity.getDescription()) &&
info.subscriber_count == entity.getSubscriberCount();
}
}

View File

@@ -0,0 +1,147 @@
package org.schabi.newpipe.history;
import android.content.Intent;
import android.os.Bundle;
import android.support.design.widget.FloatingActionButton;
import android.support.design.widget.TabLayout;
import android.support.v4.app.Fragment;
import android.support.v4.app.FragmentManager;
import android.support.v4.app.FragmentPagerAdapter;
import android.support.v4.app.FragmentStatePagerAdapter;
import android.support.v4.view.ViewPager;
import android.support.v7.app.AppCompatActivity;
import android.support.v7.widget.Toolbar;
import android.util.Log;
import android.view.Menu;
import android.view.MenuItem;
import com.jakewharton.rxbinding2.view.RxView;
import org.schabi.newpipe.R;
import org.schabi.newpipe.settings.SettingsActivity;
import org.schabi.newpipe.util.ThemeHelper;
import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.functions.Consumer;
public class HistoryActivity extends AppCompatActivity {
private static final String TAG = "HistoryActivity";
/**
* The {@link android.support.v4.view.PagerAdapter} that will provide
* fragments for each of the sections. We use a
* {@link FragmentPagerAdapter} derivative, which will keep every
* loaded fragment in memory. If this becomes too memory intensive, it
* may be best to switch to a
* {@link android.support.v4.app.FragmentStatePagerAdapter}.
*/
private SectionsPagerAdapter mSectionsPagerAdapter;
/**
* The {@link ViewPager} that will host the section contents.
*/
private ViewPager mViewPager;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
ThemeHelper.setTheme(this);
setContentView(R.layout.activity_history);
Toolbar toolbar = findViewById(R.id.toolbar);
setSupportActionBar(toolbar);
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
getSupportActionBar().setTitle(R.string.title_activity_history);
// Create the adapter that will return a fragment for each of the three
// primary sections of the activity.
mSectionsPagerAdapter = new SectionsPagerAdapter(getSupportFragmentManager());
// Set up the ViewPager with the sections adapter.
mViewPager = findViewById(R.id.container);
mViewPager.setAdapter(mSectionsPagerAdapter);
TabLayout tabLayout = findViewById(R.id.tabs);
tabLayout.setupWithViewPager(mViewPager);
final FloatingActionButton fab = findViewById(R.id.fab);
RxView.clicks(fab)
.observeOn(AndroidSchedulers.mainThread())
.subscribe(new Consumer<Object>() {
@Override
public void accept(Object o) {
int currentItem = mViewPager.getCurrentItem();
HistoryFragment fragment = (HistoryFragment) mSectionsPagerAdapter.instantiateItem(mViewPager, currentItem);
if(fragment != null) {
fragment.onHistoryCleared();
} else {
Log.w(TAG, "Couldn't find current fragment");
}
}
});
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
// Inflate the menu; this adds items to the action bar if it is present.
getMenuInflater().inflate(R.menu.menu_history, menu);
return true;
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
case android.R.id.home:
finish();
return true;
case R.id.action_settings:
Intent intent = new Intent(this, SettingsActivity.class);
startActivity(intent);
return true;
}
return super.onOptionsItemSelected(item);
}
/**
* A {@link FragmentPagerAdapter} that returns a fragment corresponding to
* one of the sections/tabs/pages.
*/
public class SectionsPagerAdapter extends FragmentStatePagerAdapter {
public SectionsPagerAdapter(FragmentManager fm) {
super(fm);
}
@Override
public Fragment getItem(int position) {
Fragment fragment;
switch (position) {
case 0:
fragment = SearchHistoryFragment.newInstance();
break;
case 1:
fragment = WatchedHistoryFragment.newInstance();
break;
default:
throw new IllegalArgumentException("position: " + position);
}
return fragment;
}
@Override
public CharSequence getPageTitle(int position) {
switch (position) {
case 0:
return getString(R.string.title_history_search);
case 1:
return getString(R.string.title_history_view);
}
throw new IllegalArgumentException("position: " + position);
}
@Override
public int getCount() {
// Show 3 total pages.
return 2;
}
}
}

View File

@@ -0,0 +1,106 @@
package org.schabi.newpipe.history;
import android.content.Context;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.v7.widget.RecyclerView;
import android.view.View;
import org.schabi.newpipe.database.history.model.HistoryEntry;
import java.text.DateFormat;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Date;
/**
* Adapter for history entries
* @param <E> the type of the entries
* @param <VH> the type of the view holder
*/
public abstract class HistoryEntryAdapter<E extends HistoryEntry, VH extends RecyclerView.ViewHolder> extends RecyclerView.Adapter<VH> {
private final ArrayList<E> mEntries;
private final DateFormat mDateFormat;
private OnHistoryItemClickListener<E> onHistoryItemClickListener = null;
public HistoryEntryAdapter(Context context) {
super();
mEntries = new ArrayList<>();
mDateFormat = android.text.format.DateFormat.getDateFormat(context.getApplicationContext());
setHasStableIds(true);
}
public void setEntries(@NonNull Collection<E> historyEntries) {
mEntries.clear();
mEntries.addAll(historyEntries);
notifyDataSetChanged();
}
public Collection<E> getItems() {
return mEntries;
}
public void clear() {
mEntries.clear();
notifyDataSetChanged();
}
protected String getFormattedDate(Date date) {
return mDateFormat.format(date);
}
@Override
public long getItemId(int position) {
return mEntries.get(position).getId();
}
@Override
public int getItemCount() {
return mEntries.size();
}
@Override
public void onBindViewHolder(VH holder, int position) {
final E entry = mEntries.get(position);
holder.itemView.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
final OnHistoryItemClickListener<E> historyItemClickListener = onHistoryItemClickListener;
if(historyItemClickListener != null) {
historyItemClickListener.onHistoryItemClick(entry);
}
}
});
onBindViewHolder(holder, entry, position);
}
@Override
public void onViewRecycled(VH holder) {
super.onViewRecycled(holder);
holder.itemView.setOnClickListener(null);
}
abstract void onBindViewHolder(VH holder, E entry, int position);
public void setOnHistoryItemClickListener(@Nullable OnHistoryItemClickListener<E> onHistoryItemClickListener) {
this.onHistoryItemClickListener = onHistoryItemClickListener;
}
public boolean isEmpty() {
return mEntries.isEmpty();
}
public E removeItemAt(int position) {
E entry = mEntries.remove(position);
notifyItemRemoved(position);
return entry;
}
public interface OnHistoryItemClickListener<E extends HistoryEntry> {
void onHistoryItemClick(E historyItem);
}
}

View File

@@ -0,0 +1,311 @@
package org.schabi.newpipe.history;
import android.content.SharedPreferences;
import android.graphics.Color;
import android.os.Bundle;
import android.os.Parcelable;
import android.preference.PreferenceManager;
import android.support.annotation.CallSuper;
import android.support.annotation.MainThread;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.annotation.StringRes;
import android.support.design.widget.Snackbar;
import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView;
import android.support.v7.widget.helper.ItemTouchHelper;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import org.schabi.newpipe.BaseFragment;
import org.schabi.newpipe.R;
import org.schabi.newpipe.database.history.dao.HistoryDAO;
import org.schabi.newpipe.database.history.model.HistoryEntry;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import icepick.State;
import io.reactivex.Observer;
import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.disposables.Disposable;
import io.reactivex.functions.Consumer;
import io.reactivex.schedulers.Schedulers;
import io.reactivex.subjects.PublishSubject;
import static org.schabi.newpipe.util.AnimationUtils.animateView;
public abstract class HistoryFragment<E extends HistoryEntry> extends BaseFragment
implements HistoryEntryAdapter.OnHistoryItemClickListener<E> {
private SharedPreferences mSharedPreferences;
private String mHistoryIsEnabledKey;
private boolean mHistoryIsEnabled;
private HistoryIsEnabledChangeListener mHistoryIsEnabledChangeListener;
private View mDisabledView;
private View mEmptyHistoryView;
@State
Parcelable mRecyclerViewState;
private RecyclerView mRecyclerView;
private HistoryEntryAdapter<E, ? extends RecyclerView.ViewHolder> mHistoryAdapter;
private ItemTouchHelper.SimpleCallback mHistoryItemSwipeCallback;
private int allowedSwipeToDeleteDirections = ItemTouchHelper.LEFT | ItemTouchHelper.RIGHT;
private HistoryDAO<E> mHistoryDataSource;
private PublishSubject<Collection<E>> mHistoryEntryDeleteSubject;
private PublishSubject<Collection<E>> mHistoryEntryInsertSubject;
@StringRes
abstract int getEnabledConfigKey();
@CallSuper
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
mHistoryIsEnabledKey = getString(getEnabledConfigKey());
mSharedPreferences = PreferenceManager.getDefaultSharedPreferences(getContext());
// Read history enabled from preferences
mHistoryIsEnabled = isHistoryEnabled();
// Register history enabled listener
mSharedPreferences.registerOnSharedPreferenceChangeListener(mHistoryIsEnabledChangeListener);
mHistoryDataSource = createHistoryDAO();
mHistoryEntryDeleteSubject = PublishSubject.create();
mHistoryEntryDeleteSubject
.observeOn(Schedulers.io())
.subscribe(new Consumer<Collection<E>>() {
@Override
public void accept(Collection<E> historyEntries) throws Exception {
mHistoryDataSource.delete(historyEntries);
}
});
mHistoryEntryInsertSubject = PublishSubject.create();
mHistoryEntryInsertSubject
.observeOn(Schedulers.io())
.subscribe(new Consumer<Collection<E>>() {
@Override
public void accept(Collection<E> historyEntries) throws Exception {
mHistoryDataSource.insertAll(historyEntries);
}
});
mHistoryItemSwipeCallback = new ItemTouchHelper.SimpleCallback(0, allowedSwipeToDeleteDirections) {
@Override
public boolean onMove(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder, RecyclerView.ViewHolder target) {
return false;
}
@Override
public void onSwiped(RecyclerView.ViewHolder viewHolder, int swipeDir) {
if (mHistoryAdapter != null) {
final E historyEntry = mHistoryAdapter.removeItemAt(viewHolder.getAdapterPosition());
mHistoryEntryDeleteSubject.onNext(Collections.singletonList(historyEntry));
View view = getActivity().findViewById(R.id.main_content);
if (view == null) view = mRecyclerView.getRootView();
Snackbar.make(view, R.string.item_deleted, 5 * 1000)
.setActionTextColor(Color.WHITE)
.setAction(R.string.undo, new View.OnClickListener() {
@Override
public void onClick(View v) {
mHistoryEntryInsertSubject.onNext(Collections.singletonList(historyEntry));
}
}).show();
}
}
};
}
@NonNull
protected abstract HistoryEntryAdapter<E, ? extends RecyclerView.ViewHolder> createAdapter();
@Override
public void onResume() {
super.onResume();
mHistoryDataSource.getAll()
.toObservable()
.observeOn(AndroidSchedulers.mainThread())
.subscribe(getHistoryListConsumer());
boolean newEnabled = isHistoryEnabled();
if (newEnabled != mHistoryIsEnabled) {
onHistoryIsEnabledChanged(newEnabled);
}
}
@NonNull
private Observer<List<E>> getHistoryListConsumer() {
return new Observer<List<E>>() {
@Override
public void onSubscribe(@NonNull Disposable d) {
}
@Override
public void onNext(@NonNull List<E> historyEntries) {
if (!historyEntries.isEmpty()) {
mHistoryAdapter.setEntries(historyEntries);
animateView(mEmptyHistoryView, false, 200);
if (mRecyclerViewState != null) {
mRecyclerView.getLayoutManager().onRestoreInstanceState(mRecyclerViewState);
mRecyclerViewState = null;
}
} else {
mHistoryAdapter.clear();
showEmptyHistory();
}
}
@Override
public void onError(@NonNull Throwable e) {
// TODO: error handling like in (see e.g. subscription fragment)
}
@Override
public void onComplete() {
}
};
}
private boolean isHistoryEnabled() {
return mSharedPreferences.getBoolean(mHistoryIsEnabledKey, false);
}
/**
* Called when the history is cleared to update the views
*/
@MainThread
public void onHistoryCleared() {
final Parcelable stateBeforeClear = mRecyclerView.getLayoutManager().onSaveInstanceState();
final Collection<E> itemsToDelete = new ArrayList<>(mHistoryAdapter.getItems());
mHistoryEntryDeleteSubject.onNext(itemsToDelete);
View view = getActivity().findViewById(R.id.main_content);
if (view == null) view = mRecyclerView.getRootView();
if (!itemsToDelete.isEmpty()) {
Snackbar.make(view, R.string.history_cleared, 5 * 1000)
.setActionTextColor(Color.WHITE)
.setAction(R.string.undo, new View.OnClickListener() {
@Override
public void onClick(View v) {
mRecyclerViewState = stateBeforeClear;
mHistoryEntryInsertSubject.onNext(itemsToDelete);
}
}).show();
} else {
Snackbar.make(view, R.string.history_cleared, Snackbar.LENGTH_LONG).show();
}
mHistoryAdapter.clear();
showEmptyHistory();
}
private void showEmptyHistory() {
if (mHistoryIsEnabled) {
animateView(mEmptyHistoryView, true, 200);
}
}
@Nullable
@CallSuper
@Override
public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
View rootView = inflater.inflate(R.layout.fragment_history, container, false);
mRecyclerView = rootView.findViewById(R.id.history_view);
RecyclerView.LayoutManager layoutManager = new LinearLayoutManager(getContext(), LinearLayoutManager.VERTICAL, false);
mRecyclerView.setLayoutManager(layoutManager);
mHistoryAdapter = createAdapter();
mHistoryAdapter.setOnHistoryItemClickListener(this);
mRecyclerView.setAdapter(mHistoryAdapter);
ItemTouchHelper itemTouchHelper = new ItemTouchHelper(mHistoryItemSwipeCallback);
itemTouchHelper.attachToRecyclerView(mRecyclerView);
mDisabledView = rootView.findViewById(R.id.history_disabled_view);
mEmptyHistoryView = rootView.findViewById(R.id.history_empty);
if (mHistoryIsEnabled) {
mRecyclerView.setVisibility(View.VISIBLE);
} else {
mDisabledView.setVisibility(View.VISIBLE);
}
return rootView;
}
@CallSuper
@Override
public void onDestroy() {
super.onDestroy();
mSharedPreferences.unregisterOnSharedPreferenceChangeListener(mHistoryIsEnabledChangeListener);
mSharedPreferences = null;
mHistoryIsEnabledChangeListener = null;
mHistoryIsEnabledKey = null;
mHistoryDataSource = null;
}
@Override
public void onPause() {
super.onPause();
mRecyclerViewState = mRecyclerView.getLayoutManager().onSaveInstanceState();
}
public void setAllowedSwipeToDeleteDirections(int allowedSwipeToDeleteDirections) {
this.allowedSwipeToDeleteDirections = allowedSwipeToDeleteDirections;
}
/**
* Called when history enabled flag is changed.
*
* @param historyIsEnabled the new value
*/
@CallSuper
public void onHistoryIsEnabledChanged(boolean historyIsEnabled) {
mHistoryIsEnabled = historyIsEnabled;
if (historyIsEnabled) {
animateView(mRecyclerView, true, 300);
animateView(mDisabledView, false, 300);
if (mHistoryAdapter.isEmpty()) {
animateView(mEmptyHistoryView, true, 300);
}
} else {
animateView(mRecyclerView, false, 300);
animateView(mDisabledView, true, 300);
animateView(mEmptyHistoryView, false, 300);
}
}
/**
* Creates a new history DAO
*
* @return the history DAO
*/
@NonNull
protected abstract HistoryDAO<E> createHistoryDAO();
private class HistoryIsEnabledChangeListener implements SharedPreferences.OnSharedPreferenceChangeListener {
@Override
public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) {
if (key.equals(mHistoryIsEnabledKey)) {
boolean enabled = sharedPreferences.getBoolean(key, false);
if (mHistoryIsEnabled != enabled) {
onHistoryIsEnabledChanged(enabled);
}
}
}
}
}

View File

@@ -0,0 +1,31 @@
package org.schabi.newpipe.history;
import org.schabi.newpipe.extractor.stream.AudioStream;
import org.schabi.newpipe.extractor.stream.StreamInfo;
import org.schabi.newpipe.extractor.stream.VideoStream;
public interface HistoryListener {
/**
* Called when a video is played
*
* @param streamInfo the stream info
* @param videoStream the video stream that is played
*/
void onVideoPlayed(StreamInfo streamInfo, VideoStream videoStream);
/**
* Called when the audio is played in the background
*
* @param streamInfo the stream info
* @param audioStream the audio stream that is played
*/
void onAudioPlayed(StreamInfo streamInfo, AudioStream audioStream);
/**
* Called when the user searched for something
*
* @param serviceId which service the search was done
* @param query what the user searched for
*/
void onSearch(int serviceId, String query);
}

View File

@@ -0,0 +1,79 @@
package org.schabi.newpipe.history;
import android.content.Context;
import android.support.annotation.NonNull;
import android.support.annotation.StringRes;
import android.support.v7.widget.RecyclerView;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
import org.schabi.newpipe.NewPipeDatabase;
import org.schabi.newpipe.R;
import org.schabi.newpipe.database.history.dao.HistoryDAO;
import org.schabi.newpipe.database.history.model.SearchHistoryEntry;
import org.schabi.newpipe.util.NavigationHelper;
public class SearchHistoryFragment extends HistoryFragment<SearchHistoryEntry> {
@NonNull
public static SearchHistoryFragment newInstance() {
return new SearchHistoryFragment();
}
@NonNull
@Override
protected SearchHistoryAdapter createAdapter() {
return new SearchHistoryAdapter(getContext());
}
@StringRes
@Override
int getEnabledConfigKey() {
return R.string.enable_search_history_key;
}
@NonNull
@Override
protected HistoryDAO<SearchHistoryEntry> createHistoryDAO() {
return NewPipeDatabase.getInstance().searchHistoryDAO();
}
@Override
public void onHistoryItemClick(SearchHistoryEntry historyItem) {
NavigationHelper.openSearch(getContext(), historyItem.getServiceId(), historyItem.getSearch());
}
private static class ViewHolder extends RecyclerView.ViewHolder {
private final TextView search;
private final TextView time;
public ViewHolder(View itemView) {
super(itemView);
search = itemView.findViewById(R.id.search);
time = itemView.findViewById(R.id.time);
}
}
protected class SearchHistoryAdapter extends HistoryEntryAdapter<SearchHistoryEntry, ViewHolder> {
public SearchHistoryAdapter(Context context) {
super(context);
}
@Override
public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
LayoutInflater inflater = LayoutInflater.from(parent.getContext());
View rootView = inflater.inflate(R.layout.item_search_history, parent, false);
return new ViewHolder(rootView);
}
@Override
void onBindViewHolder(ViewHolder holder, SearchHistoryEntry entry, int position) {
holder.search.setText(entry.getSearch());
holder.time.setText(getFormattedDate(entry.getCreationDate()));
}
}
}

View File

@@ -0,0 +1,113 @@
package org.schabi.newpipe.history;
import android.content.Context;
import android.os.Bundle;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.annotation.StringRes;
import android.support.v7.widget.RecyclerView;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageView;
import android.widget.TextView;
import com.nostra13.universalimageloader.core.ImageLoader;
import org.schabi.newpipe.NewPipeDatabase;
import org.schabi.newpipe.R;
import org.schabi.newpipe.database.history.dao.HistoryDAO;
import org.schabi.newpipe.database.history.model.WatchHistoryEntry;
import org.schabi.newpipe.info_list.holder.StreamInfoItemHolder;
import org.schabi.newpipe.util.Localization;
import org.schabi.newpipe.util.NavigationHelper;
public class WatchedHistoryFragment extends HistoryFragment<WatchHistoryEntry> {
@NonNull
public static WatchedHistoryFragment newInstance() {
return new WatchedHistoryFragment();
}
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
}
@StringRes
@Override
int getEnabledConfigKey() {
return R.string.enable_watch_history_key;
}
@NonNull
@Override
protected WatchedHistoryAdapter createAdapter() {
return new WatchedHistoryAdapter(getContext());
}
@NonNull
@Override
protected HistoryDAO<WatchHistoryEntry> createHistoryDAO() {
return NewPipeDatabase.getInstance().watchHistoryDAO();
}
@Override
public void onHistoryItemClick(WatchHistoryEntry historyItem) {
NavigationHelper.openVideoDetail(getContext(),
historyItem.getServiceId(),
historyItem.getUrl(),
historyItem.getTitle());
}
private static class WatchedHistoryAdapter extends HistoryEntryAdapter<WatchHistoryEntry, ViewHolder> {
public WatchedHistoryAdapter(Context context) {
super(context);
}
@Override
public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
LayoutInflater inflater = LayoutInflater.from(parent.getContext());
View itemView = inflater.inflate(R.layout.list_stream_item, parent, false);
return new ViewHolder(itemView);
}
@Override
public void onViewRecycled(ViewHolder holder) {
holder.itemView.setOnClickListener(null);
ImageLoader.getInstance()
.cancelDisplayTask(holder.thumbnailView);
}
@Override
void onBindViewHolder(ViewHolder holder, WatchHistoryEntry entry, int position) {
holder.date.setText(getFormattedDate(entry.getCreationDate()));
holder.streamTitle.setText(entry.getTitle());
holder.uploader.setText(entry.getUploader());
holder.duration.setText(Localization.getDurationString(entry.getDuration()));
ImageLoader.getInstance()
.displayImage(entry.getThumbnailURL(), holder.thumbnailView, StreamInfoItemHolder.DISPLAY_THUMBNAIL_OPTIONS);
}
}
private static class ViewHolder extends RecyclerView.ViewHolder {
private final TextView date;
private final TextView streamTitle;
private final ImageView thumbnailView;
private final TextView uploader;
private final TextView duration;
public ViewHolder(View itemView) {
super(itemView);
thumbnailView = itemView.findViewById(R.id.itemThumbnailView);
date = itemView.findViewById(R.id.itemAdditionalDetails);
streamTitle = itemView.findViewById(R.id.itemVideoTitleView);
uploader = itemView.findViewById(R.id.itemUploaderView);
duration = itemView.findViewById(R.id.itemDurationView);
}
}
}

View File

@@ -1,52 +0,0 @@
package org.schabi.newpipe.info_list;
import android.view.View;
import android.widget.TextView;
import org.schabi.newpipe.R;
import org.schabi.newpipe.extractor.InfoItem;
import de.hdodenhof.circleimageview.CircleImageView;
/**
* Created by Christian Schabesberger on 12.02.17.
*
* Copyright (C) Christian Schabesberger 2016 <chris.schabesberger@mailbox.org>
* ChannelInfoItemHolder .java is part of NewPipe.
*
* NewPipe is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* NewPipe is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with NewPipe. If not, see <http://www.gnu.org/licenses/>.
*/
public class ChannelInfoItemHolder extends InfoItemHolder {
public final CircleImageView itemThumbnailView;
public final TextView itemChannelTitleView;
public final TextView itemAdditionalDetailView;
public final TextView itemChannelDescriptionView;
public final View itemRoot;
ChannelInfoItemHolder(View v) {
super(v);
itemRoot = v.findViewById(R.id.itemRoot);
itemThumbnailView = (CircleImageView) v.findViewById(R.id.itemThumbnailView);
itemChannelTitleView = (TextView) v.findViewById(R.id.itemChannelTitleView);
itemAdditionalDetailView = (TextView) v.findViewById(R.id.itemAdditionalDetails);
itemChannelDescriptionView = (TextView) v.findViewById(R.id.itemChannelDescriptionView);
}
@Override
public InfoItem.InfoType infoType() {
return InfoItem.InfoType.CHANNEL;
}
}

View File

@@ -1,25 +1,25 @@
package org.schabi.newpipe.info_list;
import android.content.Context;
import android.text.TextUtils;
import android.support.annotation.NonNull;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import com.nostra13.universalimageloader.core.DisplayImageOptions;
import com.nostra13.universalimageloader.core.ImageLoader;
import org.schabi.newpipe.ImageErrorLoadingListener;
import org.schabi.newpipe.R;
import org.schabi.newpipe.extractor.AbstractStreamInfo;
import org.schabi.newpipe.extractor.InfoItem;
import org.schabi.newpipe.extractor.channel.ChannelInfoItem;
import org.schabi.newpipe.extractor.stream_info.StreamInfoItem;
import org.schabi.newpipe.extractor.playlist.PlaylistInfoItem;
import org.schabi.newpipe.extractor.stream.StreamInfoItem;
import org.schabi.newpipe.info_list.holder.ChannelInfoItemHolder;
import org.schabi.newpipe.info_list.holder.ChannelMiniInfoItemHolder;
import org.schabi.newpipe.info_list.holder.InfoItemHolder;
import org.schabi.newpipe.info_list.holder.PlaylistInfoItemHolder;
import org.schabi.newpipe.info_list.holder.StreamInfoItemHolder;
import org.schabi.newpipe.info_list.holder.StreamMiniInfoItemHolder;
import java.util.Locale;
/**
/*
* Created by Christian Schabesberger on 26.09.16.
* <p>
* Copyright (C) Christian Schabesberger 2016 <chris.schabesberger@mailbox.org>
@@ -40,251 +40,77 @@ import java.util.Locale;
*/
public class InfoItemBuilder {
private final String viewsS;
private final String videosS;
private final String subsS;
private final String subsPluralS;
private final String thousand;
private final String million;
private final String billion;
private static final String TAG = InfoItemBuilder.class.toString();
public interface OnInfoItemSelectedListener {
void selected(int serviceId, String url, String title);
public interface OnInfoItemSelectedListener<T extends InfoItem> {
void selected(T selectedItem);
}
private final Context context;
private ImageLoader imageLoader = ImageLoader.getInstance();
/** Base display options */
private static final DisplayImageOptions DISPLAY_IMAGE_OPTIONS =
new DisplayImageOptions.Builder()
.cacheInMemory(true)
.build();
/** Display options for stream thumbnails */
private static final DisplayImageOptions DISPLAY_STREAM_THUMBNAIL_OPTIONS =
new DisplayImageOptions.Builder()
.cloneFrom(DISPLAY_IMAGE_OPTIONS)
.showImageOnFail(R.drawable.dummy_thumbnail)
.showImageForEmptyUri(R.drawable.dummy_thumbnail)
.showImageOnLoading(R.drawable.dummy_thumbnail)
.build();
/** Display options for channel thumbnails */
private static final DisplayImageOptions DISPLAY_CHANNEL_THUMBNAIL_OPTIONS =
new DisplayImageOptions.Builder()
.cloneFrom(DISPLAY_IMAGE_OPTIONS)
.showImageOnLoading(R.drawable.buddy_channel_item)
.showImageForEmptyUri(R.drawable.buddy_channel_item)
.showImageOnFail(R.drawable.buddy_channel_item)
.build();
private OnInfoItemSelectedListener onStreamInfoItemSelectedListener;
private OnInfoItemSelectedListener onChannelInfoItemSelectedListener;
private OnInfoItemSelectedListener<StreamInfoItem> onStreamSelectedListener;
private OnInfoItemSelectedListener<ChannelInfoItem> onChannelSelectedListener;
private OnInfoItemSelectedListener<PlaylistInfoItem> onPlaylistSelectedListener;
public InfoItemBuilder(Context context) {
viewsS = context.getString(R.string.views);
videosS = context.getString(R.string.videos);
subsS = context.getString(R.string.subscriber);
subsPluralS = context.getString(R.string.subscriber_plural);
thousand = context.getString(R.string.short_thousand);
million = context.getString(R.string.short_million);
billion = context.getString(R.string.short_billion);
this.context = context;
}
public void setOnStreamInfoItemSelectedListener(
OnInfoItemSelectedListener listener) {
this.onStreamInfoItemSelectedListener = listener;
public View buildView(@NonNull ViewGroup parent, @NonNull final InfoItem infoItem) {
return buildView(parent, infoItem, false);
}
public void setOnChannelInfoItemSelectedListener(
OnInfoItemSelectedListener listener) {
this.onChannelInfoItemSelectedListener = listener;
public View buildView(@NonNull ViewGroup parent, @NonNull final InfoItem infoItem, boolean useMiniVariant) {
InfoItemHolder holder = holderFromInfoType(parent, infoItem.info_type, useMiniVariant);
holder.updateFromItem(infoItem);
return holder.itemView;
}
public void buildByHolder(InfoItemHolder holder, final InfoItem i) {
if (i.infoType() != holder.infoType())
return;
switch (i.infoType()) {
private InfoItemHolder holderFromInfoType(@NonNull ViewGroup parent, @NonNull InfoItem.InfoType infoType, boolean useMiniVariant) {
switch (infoType) {
case STREAM:
buildStreamInfoItem((StreamInfoItemHolder) holder, (StreamInfoItem) i);
break;
return useMiniVariant ? new StreamMiniInfoItemHolder(this, parent) : new StreamInfoItemHolder(this, parent);
case CHANNEL:
buildChannelInfoItem((ChannelInfoItemHolder) holder, (ChannelInfoItem) i);
break;
return useMiniVariant ? new ChannelMiniInfoItemHolder(this, parent) : new ChannelInfoItemHolder(this, parent);
case PLAYLIST:
Log.e(TAG, "Not yet implemented");
break;
return new PlaylistInfoItemHolder(this, parent);
default:
Log.e(TAG, "Trollolo");
throw new RuntimeException("InfoType not expected = " + infoType.name());
}
}
public View buildView(ViewGroup parent, final InfoItem info) {
View itemView = null;
InfoItemHolder holder = null;
LayoutInflater inflater = LayoutInflater.from(parent.getContext());
switch (info.infoType()) {
case STREAM:
//long start = System.nanoTime();
itemView = inflater.inflate(R.layout.stream_item, parent, false);
//Log.d(TAG, "time to inflate: " + ((System.nanoTime() - start) / 1000000L) + "ms");
holder = new StreamInfoItemHolder(itemView);
break;
case CHANNEL:
itemView = inflater.inflate(R.layout.channel_item, parent, false);
holder = new ChannelInfoItemHolder(itemView);
break;
case PLAYLIST:
Log.e(TAG, "Not yet implemented");
default:
Log.e(TAG, "Trollolo");
}
buildByHolder(holder, info);
return itemView;
public Context getContext() {
return context;
}
private String getStreamInfoDetailLine(final StreamInfoItem info) {
String viewsAndDate = "";
if(info.view_count >= 0) {
viewsAndDate = shortViewCount(info.view_count);
}
if(!TextUtils.isEmpty(info.upload_date)) {
if(viewsAndDate.isEmpty()) {
viewsAndDate = info.upload_date;
} else {
viewsAndDate += "" + info.upload_date;
}
}
return viewsAndDate;
public ImageLoader getImageLoader() {
return imageLoader;
}
private void buildStreamInfoItem(StreamInfoItemHolder holder, final StreamInfoItem info) {
if (info.infoType() != InfoItem.InfoType.STREAM) {
Log.e("InfoItemBuilder", "Info type not yet supported");
}
// fill holder with information
if (!TextUtils.isEmpty(info.title)) holder.itemVideoTitleView.setText(info.title);
if (!TextUtils.isEmpty(info.uploader)) holder.itemUploaderView.setText(info.uploader);
else holder.itemUploaderView.setVisibility(View.INVISIBLE);
if (info.duration > 0) {
holder.itemDurationView.setText(getDurationString(info.duration));
} else {
if (info.stream_type == AbstractStreamInfo.StreamType.LIVE_STREAM) {
holder.itemDurationView.setText(R.string.duration_live);
} else {
holder.itemDurationView.setVisibility(View.GONE);
}
}
holder.itemAdditionalDetails.setText(getStreamInfoDetailLine(info));
// Default thumbnail is shown on error, while loading and if the url is empty
imageLoader.displayImage(info.thumbnail_url,
holder.itemThumbnailView,
DISPLAY_STREAM_THUMBNAIL_OPTIONS,
new ImageErrorLoadingListener(holder.itemRoot.getContext(), holder.itemRoot.getRootView(), info.service_id));
holder.itemRoot.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
if(onStreamInfoItemSelectedListener != null) {
onStreamInfoItemSelectedListener.selected(info.service_id, info.webpage_url, info.getTitle());
}
}
});
public OnInfoItemSelectedListener<StreamInfoItem> getOnStreamSelectedListener() {
return onStreamSelectedListener;
}
private String getChannelInfoDetailLine(final ChannelInfoItem info) {
String details = "";
if(info.subscriberCount >= 0) {
details = shortSubscriber(info.subscriberCount);
}
if(info.videoAmount >= 0) {
String formattedVideoAmount = info.videoAmount + " " + videosS;
if(!details.isEmpty()) {
details += "" + formattedVideoAmount;
} else {
details = formattedVideoAmount;
}
}
return details;
public void setOnStreamSelectedListener(OnInfoItemSelectedListener<StreamInfoItem> listener) {
this.onStreamSelectedListener = listener;
}
private void buildChannelInfoItem(ChannelInfoItemHolder holder, final ChannelInfoItem info) {
if (!TextUtils.isEmpty(info.getTitle())) holder.itemChannelTitleView.setText(info.getTitle());
holder.itemAdditionalDetailView.setText(getChannelInfoDetailLine(info));
if (!TextUtils.isEmpty(info.description)) holder.itemChannelDescriptionView.setText(info.description);
imageLoader.displayImage(info.thumbnailUrl,
holder.itemThumbnailView,
DISPLAY_CHANNEL_THUMBNAIL_OPTIONS,
new ImageErrorLoadingListener(holder.itemRoot.getContext(), holder.itemRoot.getRootView(), info.serviceId));
holder.itemRoot.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
if(onStreamInfoItemSelectedListener != null) {
onChannelInfoItemSelectedListener.selected(info.serviceId, info.getLink(), info.channelName);
}
}
});
public OnInfoItemSelectedListener<ChannelInfoItem> getOnChannelSelectedListener() {
return onChannelSelectedListener;
}
public String shortViewCount(Long viewCount) {
if (viewCount >= 1000000000) {
return Long.toString(viewCount / 1000000000) + billion + " " + viewsS;
} else if (viewCount >= 1000000) {
return Long.toString(viewCount / 1000000) + million + " " + viewsS;
} else if (viewCount >= 1000) {
return Long.toString(viewCount / 1000) + thousand + " " + viewsS;
} else {
return Long.toString(viewCount) + " " + viewsS;
}
public void setOnChannelSelectedListener(OnInfoItemSelectedListener<ChannelInfoItem> listener) {
this.onChannelSelectedListener = listener;
}
public String shortSubscriber(Long count) {
String curSubString = count > 1 ? subsPluralS : subsS;
if (count >= 1000000000) {
return Long.toString(count / 1000000000) + billion + " " + curSubString;
} else if (count >= 1000000) {
return Long.toString(count / 1000000) + million + " " + curSubString;
} else if (count >= 1000) {
return Long.toString(count / 1000) + thousand + " " + curSubString;
} else {
return Long.toString(count) + " " + curSubString;
}
public OnInfoItemSelectedListener<PlaylistInfoItem> getOnPlaylistSelectedListener() {
return onPlaylistSelectedListener;
}
public static String getDurationString(int duration) {
if(duration < 0) {
duration = 0;
}
String output;
int days = duration / (24 * 60 * 60); /* greater than a day */
duration %= (24 * 60 * 60);
int hours = duration / (60 * 60); /* greater than an hour */
duration %= (60 * 60);
int minutes = duration / 60;
int seconds = duration % 60;
//handle days
if (days > 0) {
output = String.format(Locale.US, "%d:%02d:%02d:%02d", days, hours, minutes, seconds);
} else if(hours > 0) {
output = String.format(Locale.US, "%d:%02d:%02d", hours, minutes, seconds);
} else {
output = String.format(Locale.US, "%d:%02d", minutes, seconds);
}
return output;
public void setOnPlaylistSelectedListener(OnInfoItemSelectedListener<PlaylistInfoItem> listener) {
this.onPlaylistSelectedListener = listener;
}
}

View File

@@ -1,33 +0,0 @@
package org.schabi.newpipe.info_list;
import android.support.v7.widget.RecyclerView;
import android.view.View;
import org.schabi.newpipe.extractor.InfoItem;
/**
* Created by Christian Schabesberger on 12.02.17.
*
* Copyright (C) Christian Schabesberger 2016 <chris.schabesberger@mailbox.org>
* InfoItemHolder.java is part of NewPipe.
*
* NewPipe is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* NewPipe is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with NewPipe. If not, see <http://www.gnu.org/licenses/>.
*/
public abstract class InfoItemHolder extends RecyclerView.ViewHolder {
public InfoItemHolder(View v) {
super(v);
}
public abstract InfoItem.InfoType infoType();
}

View File

@@ -3,17 +3,25 @@ package org.schabi.newpipe.info_list;
import android.app.Activity;
import android.support.v7.widget.RecyclerView;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import org.schabi.newpipe.R;
import org.schabi.newpipe.extractor.InfoItem;
import org.schabi.newpipe.extractor.channel.ChannelInfoItem;
import org.schabi.newpipe.extractor.playlist.PlaylistInfoItem;
import org.schabi.newpipe.extractor.stream.StreamInfoItem;
import org.schabi.newpipe.info_list.InfoItemBuilder.OnInfoItemSelectedListener;
import org.schabi.newpipe.info_list.holder.ChannelInfoItemHolder;
import org.schabi.newpipe.info_list.holder.ChannelMiniInfoItemHolder;
import org.schabi.newpipe.info_list.holder.InfoItemHolder;
import org.schabi.newpipe.info_list.holder.PlaylistInfoItemHolder;
import org.schabi.newpipe.info_list.holder.StreamInfoItemHolder;
import org.schabi.newpipe.info_list.holder.StreamMiniInfoItemHolder;
import java.util.ArrayList;
import java.util.List;
/**
/*
* Created by Christian Schabesberger on 01.08.16.
*
* Copyright (C) Christian Schabesberger 2016 <chris.schabesberger@mailbox.org>
@@ -34,25 +42,32 @@ import java.util.List;
*/
public class InfoListAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {
private static final String TAG = InfoListAdapter.class.toString();
private static final String TAG = InfoListAdapter.class.getSimpleName();
private static final boolean DEBUG = false;
private static final int HEADER_TYPE = 0;
private static final int FOOTER_TYPE = 1;
private static final int MINI_STREAM_HOLDER_TYPE = 0x100;
private static final int STREAM_HOLDER_TYPE = 0x101;
private static final int MINI_CHANNEL_HOLDER_TYPE = 0x200;
private static final int CHANNEL_HOLDER_TYPE = 0x201;
private static final int PLAYLIST_HOLDER_TYPE = 0x301;
private final InfoItemBuilder infoItemBuilder;
private final List<InfoItem> infoItemList;
private final ArrayList<InfoItem> infoItemList;
private boolean useMiniVariant = false;
private boolean showFooter = false;
private View header = null;
private View footer = null;
public class HFHolder extends RecyclerView.ViewHolder {
public View view;
public HFHolder(View v) {
super(v);
view = v;
}
public View view;
}
public void showFooter(boolean show) {
showFooter = show;
notifyDataSetChanged();
}
public InfoListAdapter(Activity a) {
@@ -60,25 +75,71 @@ public class InfoListAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolde
infoItemList = new ArrayList<>();
}
public void setOnStreamInfoItemSelectedListener
(InfoItemBuilder.OnInfoItemSelectedListener listener) {
infoItemBuilder.setOnStreamInfoItemSelectedListener(listener);
public void setOnStreamSelectedListener(OnInfoItemSelectedListener<StreamInfoItem> listener) {
infoItemBuilder.setOnStreamSelectedListener(listener);
}
public void setOnChannelInfoItemSelectedListener
(InfoItemBuilder.OnInfoItemSelectedListener listener) {
infoItemBuilder.setOnChannelInfoItemSelectedListener(listener);
public void setOnChannelSelectedListener(OnInfoItemSelectedListener<ChannelInfoItem> listener) {
infoItemBuilder.setOnChannelSelectedListener(listener);
}
public void setOnPlaylistSelectedListener(OnInfoItemSelectedListener<PlaylistInfoItem> listener) {
infoItemBuilder.setOnPlaylistSelectedListener(listener);
}
public void useMiniItemVariants(boolean useMiniVariant) {
this.useMiniVariant = useMiniVariant;
}
public void addInfoItemList(List<InfoItem> data) {
if(data != null) {
if (data != null) {
if (DEBUG) {
Log.d(TAG, "addInfoItemList() before > infoItemList.size() = " + infoItemList.size() + ", data.size() = " + data.size());
}
int offsetStart = sizeConsideringHeaderOffset();
infoItemList.addAll(data);
notifyDataSetChanged();
if (DEBUG) {
Log.d(TAG, "addInfoItemList() after > offsetStart = " + offsetStart + ", infoItemList.size() = " + infoItemList.size() + ", header = " + header + ", footer = " + footer + ", showFooter = " + showFooter);
}
notifyItemRangeInserted(offsetStart, data.size());
if (footer != null && showFooter) {
int footerNow = sizeConsideringHeaderOffset();
notifyItemMoved(offsetStart, footerNow);
if (DEBUG) Log.d(TAG, "addInfoItemList() footer from " + offsetStart + " to " + footerNow);
}
}
}
public void addInfoItem(InfoItem data) {
if (data != null) {
if (DEBUG) {
Log.d(TAG, "addInfoItem() before > infoItemList.size() = " + infoItemList.size() + ", thread = " + Thread.currentThread());
}
int positionInserted = sizeConsideringHeaderOffset();
infoItemList.add(data);
if (DEBUG) {
Log.d(TAG, "addInfoItem() after > position = " + positionInserted + ", infoItemList.size() = " + infoItemList.size() + ", header = " + header + ", footer = " + footer + ", showFooter = " + showFooter);
}
notifyItemInserted(positionInserted);
if (footer != null && showFooter) {
int footerNow = sizeConsideringHeaderOffset();
notifyItemMoved(positionInserted, footerNow);
if (DEBUG) Log.d(TAG, "addInfoItem() footer from " + positionInserted + " to " + footerNow);
}
}
}
public void clearStreamItemList() {
if(infoItemList.isEmpty()) {
if (infoItemList.isEmpty()) {
return;
}
infoItemList.clear();
@@ -86,45 +147,67 @@ public class InfoListAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolde
}
public void setHeader(View header) {
boolean changed = header != this.header;
this.header = header;
notifyDataSetChanged();
if (changed) notifyDataSetChanged();
}
public void setFooter(View view) {
this.footer = view;
notifyDataSetChanged();
}
public List<InfoItem> getItemsList() {
public void showFooter(boolean show) {
if (DEBUG) Log.d(TAG, "showFooter() called with: show = [" + show + "]");
if (show == showFooter) return;
showFooter = show;
if (show) notifyItemInserted(sizeConsideringHeaderOffset());
else notifyItemRemoved(sizeConsideringHeaderOffset());
}
private int sizeConsideringHeaderOffset() {
int i = infoItemList.size() + (header != null ? 1 : 0);
if (DEBUG) Log.d(TAG, "sizeConsideringHeaderOffset() called → " + i);
return i;
}
public ArrayList<InfoItem> getItemsList() {
return infoItemList;
}
@Override
public int getItemCount() {
int count = infoItemList.size();
if(header != null) count++;
if(footer != null && showFooter) count++;
if (header != null) count++;
if (footer != null && showFooter) count++;
if (DEBUG) {
Log.d(TAG, "getItemCount() called, count = " + count + ", infoItemList.size() = " + infoItemList.size() + ", header = " + header + ", footer = " + footer + ", showFooter = " + showFooter);
}
return count;
}
// don't ask why we have to do that this way... it's android accept it -.-
@Override
public int getItemViewType(int position) {
if(header != null && position == 0) {
return 0;
} else if(header != null) {
if (DEBUG) Log.d(TAG, "getItemViewType() called with: position = [" + position + "]");
if (header != null && position == 0) {
return HEADER_TYPE;
} else if (header != null) {
position--;
}
if(footer != null && position == infoItemList.size() && showFooter) {
return 1;
if (footer != null && position == infoItemList.size() && showFooter) {
return FOOTER_TYPE;
}
switch(infoItemList.get(position).infoType()) {
InfoItem item = infoItemList.get(position);
switch (item.info_type) {
case STREAM:
return 2;
return useMiniVariant ? MINI_STREAM_HOLDER_TYPE : STREAM_HOLDER_TYPE;
case CHANNEL:
return 3;
return useMiniVariant ? MINI_CHANNEL_HOLDER_TYPE : CHANNEL_HOLDER_TYPE;
case PLAYLIST:
return 4;
return PLAYLIST_HOLDER_TYPE;
default:
Log.e(TAG, "Trollolo");
return -1;
@@ -133,20 +216,22 @@ public class InfoListAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolde
@Override
public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int type) {
switch(type) {
case 0:
if (DEBUG) Log.d(TAG, "onCreateViewHolder() called with: parent = [" + parent + "], type = [" + type + "]");
switch (type) {
case HEADER_TYPE:
return new HFHolder(header);
case 1:
case FOOTER_TYPE:
return new HFHolder(footer);
case 2:
return new StreamInfoItemHolder(LayoutInflater.from(parent.getContext())
.inflate(R.layout.stream_item, parent, false));
case 3:
return new ChannelInfoItemHolder(LayoutInflater.from(parent.getContext())
.inflate(R.layout.channel_item, parent, false));
case 4:
Log.e(TAG, "Playlist is not yet implemented");
return null;
case MINI_STREAM_HOLDER_TYPE:
return new StreamMiniInfoItemHolder(infoItemBuilder, parent);
case STREAM_HOLDER_TYPE:
return new StreamInfoItemHolder(infoItemBuilder, parent);
case MINI_CHANNEL_HOLDER_TYPE:
return new ChannelMiniInfoItemHolder(infoItemBuilder, parent);
case CHANNEL_HOLDER_TYPE:
return new ChannelInfoItemHolder(infoItemBuilder, parent);
case PLAYLIST_HOLDER_TYPE:
return new PlaylistInfoItemHolder(infoItemBuilder, parent);
default:
Log.e(TAG, "Trollolo");
return null;
@@ -154,16 +239,16 @@ public class InfoListAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolde
}
@Override
public void onBindViewHolder(RecyclerView.ViewHolder holder, int i) {
//god damn f*** ANDROID SH**
if(holder instanceof InfoItemHolder) {
if(header != null) {
i--;
}
infoItemBuilder.buildByHolder((InfoItemHolder) holder, infoItemList.get(i));
} else if(holder instanceof HFHolder && i == 0 && header != null) {
public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
if (DEBUG) Log.d(TAG, "onBindViewHolder() called with: holder = [" + holder.getClass().getSimpleName() + "], position = [" + position + "]");
if (holder instanceof InfoItemHolder) {
// If header isn't null, offset the items by -1
if (header != null) position--;
((InfoItemHolder) holder).updateFromItem(infoItemList.get(position));
} else if (holder instanceof HFHolder && position == 0 && header != null) {
((HFHolder) holder).view = header;
} else if(holder instanceof HFHolder && i == infoItemList.size() && footer != null && showFooter) {
} else if (holder instanceof HFHolder && position == sizeConsideringHeaderOffset() && footer != null && showFooter) {
((HFHolder) holder).view = footer;
}
}

View File

@@ -1,53 +0,0 @@
package org.schabi.newpipe.info_list;
import android.view.View;
import android.widget.ImageView;
import android.widget.TextView;
import org.schabi.newpipe.R;
import org.schabi.newpipe.extractor.InfoItem;
/**
* Created by Christian Schabesberger on 01.08.16.
* <p>
* Copyright (C) Christian Schabesberger 2016 <chris.schabesberger@mailbox.org>
* StreamInfoItemHolder.java is part of NewPipe.
* <p>
* NewPipe is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
* <p>
* NewPipe is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
* <p>
* You should have received a copy of the GNU General Public License
* along with NewPipe. If not, see <http://www.gnu.org/licenses/>.
*/
public class StreamInfoItemHolder extends InfoItemHolder {
public final ImageView itemThumbnailView;
public final TextView itemVideoTitleView,
itemUploaderView,
itemDurationView,
itemAdditionalDetails;
public final View itemRoot;
public StreamInfoItemHolder(View v) {
super(v);
itemRoot = v.findViewById(R.id.itemRoot);
itemThumbnailView = (ImageView) v.findViewById(R.id.itemThumbnailView);
itemVideoTitleView = (TextView) v.findViewById(R.id.itemVideoTitleView);
itemUploaderView = (TextView) v.findViewById(R.id.itemUploaderView);
itemDurationView = (TextView) v.findViewById(R.id.itemDurationView);
itemAdditionalDetails = (TextView) v.findViewById(R.id.itemAdditionalDetails);
}
@Override
public InfoItem.InfoType infoType() {
return InfoItem.InfoType.STREAM;
}
}

View File

@@ -0,0 +1,65 @@
package org.schabi.newpipe.info_list.holder;
import android.view.ViewGroup;
import android.widget.TextView;
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.Localization;
/*
* Created by Christian Schabesberger on 12.02.17.
*
* Copyright (C) Christian Schabesberger 2016 <chris.schabesberger@mailbox.org>
* ChannelInfoItemHolder .java is part of NewPipe.
*
* NewPipe is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* NewPipe is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with NewPipe. If not, see <http://www.gnu.org/licenses/>.
*/
public class ChannelInfoItemHolder extends ChannelMiniInfoItemHolder {
public final TextView itemChannelDescriptionView;
public ChannelInfoItemHolder(InfoItemBuilder infoItemBuilder, ViewGroup parent) {
super(infoItemBuilder, R.layout.list_channel_item, parent);
itemChannelDescriptionView = itemView.findViewById(R.id.itemChannelDescriptionView);
}
@Override
public void updateFromItem(final InfoItem infoItem) {
super.updateFromItem(infoItem);
if (!(infoItem instanceof ChannelInfoItem)) return;
final ChannelInfoItem item = (ChannelInfoItem) infoItem;
itemChannelDescriptionView.setText(item.description);
}
@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 (!details.isEmpty()) {
details += "" + formattedVideoAmount;
} else {
details = formattedVideoAmount;
}
}
return details;
}
}

View File

@@ -0,0 +1,73 @@
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.Localization;
import de.hdodenhof.circleimageview.CircleImageView;
public class ChannelMiniInfoItemHolder extends InfoItemHolder {
public final CircleImageView itemThumbnailView;
public final TextView itemTitleView;
public final TextView itemAdditionalDetailView;
ChannelMiniInfoItemHolder(InfoItemBuilder infoItemBuilder, int layoutId, ViewGroup parent) {
super(infoItemBuilder, layoutId, parent);
itemThumbnailView = itemView.findViewById(R.id.itemThumbnailView);
itemTitleView = itemView.findViewById(R.id.itemTitleView);
itemAdditionalDetailView = itemView.findViewById(R.id.itemAdditionalDetails);
}
public ChannelMiniInfoItemHolder(InfoItemBuilder infoItemBuilder, ViewGroup parent) {
this(infoItemBuilder, R.layout.list_channel_mini_item, parent);
}
@Override
public void updateFromItem(final InfoItem infoItem) {
if (!(infoItem instanceof ChannelInfoItem)) return;
final ChannelInfoItem item = (ChannelInfoItem) infoItem;
itemTitleView.setText(item.name);
itemAdditionalDetailView.setText(getDetailLine(item));
itemBuilder.getImageLoader()
.displayImage(item.thumbnail_url, itemThumbnailView, ChannelInfoItemHolder.DISPLAY_THUMBNAIL_OPTIONS);
itemView.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View 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);
}
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

@@ -0,0 +1,53 @@
package org.schabi.newpipe.info_list.holder;
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;
/*
* Created by Christian Schabesberger on 12.02.17.
*
* Copyright (C) Christian Schabesberger 2016 <chris.schabesberger@mailbox.org>
* InfoItemHolder.java is part of NewPipe.
*
* NewPipe is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* NewPipe is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with NewPipe. If not, see <http://www.gnu.org/licenses/>.
*/
public abstract class InfoItemHolder extends RecyclerView.ViewHolder {
protected final InfoItemBuilder itemBuilder;
public InfoItemHolder(InfoItemBuilder infoItemBuilder, int layoutId, ViewGroup parent) {
super(LayoutInflater.from(infoItemBuilder.getContext()).inflate(layoutId, parent, false));
this.itemBuilder = infoItemBuilder;
}
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

@@ -0,0 +1,62 @@
package org.schabi.newpipe.info_list.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.extractor.InfoItem;
import org.schabi.newpipe.extractor.playlist.PlaylistInfoItem;
import org.schabi.newpipe.info_list.InfoItemBuilder;
public class PlaylistInfoItemHolder extends InfoItemHolder {
public final ImageView itemThumbnailView;
public final TextView itemStreamCountView;
public final TextView itemTitleView;
public final TextView itemUploaderView;
public PlaylistInfoItemHolder(InfoItemBuilder infoItemBuilder, ViewGroup parent) {
super(infoItemBuilder, R.layout.list_playlist_item, parent);
itemThumbnailView = itemView.findViewById(R.id.itemThumbnailView);
itemTitleView = itemView.findViewById(R.id.itemTitleView);
itemStreamCountView = itemView.findViewById(R.id.itemStreamCountView);
itemUploaderView = itemView.findViewById(R.id.itemUploaderView);
}
@Override
public void updateFromItem(final InfoItem infoItem) {
if (!(infoItem instanceof PlaylistInfoItem)) return;
final PlaylistInfoItem item = (PlaylistInfoItem) infoItem;
itemTitleView.setText(item.name);
itemStreamCountView.setText(item.stream_count + "");
itemUploaderView.setText(item.uploader_name);
itemBuilder.getImageLoader()
.displayImage(item.thumbnail_url, itemThumbnailView, DISPLAY_THUMBNAIL_OPTIONS);
itemView.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
if (itemBuilder.getOnPlaylistSelectedListener() != null) {
itemBuilder.getOnPlaylistSelectedListener().selected(item);
}
}
});
}
/**
* 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

@@ -0,0 +1,66 @@
package org.schabi.newpipe.info_list.holder;
import android.text.TextUtils;
import android.view.ViewGroup;
import android.widget.TextView;
import org.schabi.newpipe.R;
import org.schabi.newpipe.extractor.InfoItem;
import org.schabi.newpipe.extractor.stream.StreamInfoItem;
import org.schabi.newpipe.info_list.InfoItemBuilder;
import org.schabi.newpipe.util.Localization;
/*
* Created by Christian Schabesberger on 01.08.16.
* <p>
* Copyright (C) Christian Schabesberger 2016 <chris.schabesberger@mailbox.org>
* StreamInfoItemHolder.java is part of NewPipe.
* <p>
* NewPipe is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
* <p>
* NewPipe is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
* <p>
* You should have received a copy of the GNU General Public License
* along with NewPipe. If not, see <http://www.gnu.org/licenses/>.
*/
public class StreamInfoItemHolder extends StreamMiniInfoItemHolder {
public final TextView itemAdditionalDetails;
public StreamInfoItemHolder(InfoItemBuilder infoItemBuilder, ViewGroup parent) {
super(infoItemBuilder, R.layout.list_stream_item, parent);
itemAdditionalDetails = itemView.findViewById(R.id.itemAdditionalDetails);
}
@Override
public void updateFromItem(final InfoItem infoItem) {
super.updateFromItem(infoItem);
if (!(infoItem instanceof StreamInfoItem)) return;
final StreamInfoItem item = (StreamInfoItem) infoItem;
itemAdditionalDetails.setText(getStreamInfoDetailLine(item));
}
private String getStreamInfoDetailLine(final StreamInfoItem infoItem) {
String viewsAndDate = "";
if (infoItem.view_count >= 0) {
viewsAndDate = Localization.shortViewCount(itemBuilder.getContext(), infoItem.view_count);
}
if (!TextUtils.isEmpty(infoItem.upload_date)) {
if (viewsAndDate.isEmpty()) {
viewsAndDate = infoItem.upload_date;
} else {
viewsAndDate += "" + infoItem.upload_date;
}
}
return viewsAndDate;
}
}

View File

@@ -0,0 +1,82 @@
package org.schabi.newpipe.info_list.holder;
import android.support.v4.content.ContextCompat;
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.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.Localization;
public class StreamMiniInfoItemHolder extends InfoItemHolder {
public final ImageView itemThumbnailView;
public final TextView itemVideoTitleView;
public final TextView itemUploaderView;
public final TextView itemDurationView;
StreamMiniInfoItemHolder(InfoItemBuilder infoItemBuilder, int layoutId, ViewGroup parent) {
super(infoItemBuilder, layoutId, parent);
itemThumbnailView = itemView.findViewById(R.id.itemThumbnailView);
itemVideoTitleView = itemView.findViewById(R.id.itemVideoTitleView);
itemUploaderView = itemView.findViewById(R.id.itemUploaderView);
itemDurationView = itemView.findViewById(R.id.itemDurationView);
}
public StreamMiniInfoItemHolder(InfoItemBuilder infoItemBuilder, ViewGroup parent) {
this(infoItemBuilder, R.layout.list_stream_mini_item, parent);
}
@Override
public void updateFromItem(final InfoItem infoItem) {
if (!(infoItem instanceof StreamInfoItem)) return;
final StreamInfoItem item = (StreamInfoItem) infoItem;
itemVideoTitleView.setText(item.name);
itemUploaderView.setText(item.uploader_name);
if (item.duration > 0) {
itemDurationView.setText(Localization.getDurationString(item.duration));
itemDurationView.setBackgroundColor(ContextCompat.getColor(itemBuilder.getContext(), R.color.duration_background_color));
itemDurationView.setVisibility(View.VISIBLE);
} else if (item.stream_type == StreamType.LIVE_STREAM) {
itemDurationView.setText(R.string.duration_live);
itemDurationView.setBackgroundColor(ContextCompat.getColor(itemBuilder.getContext(), R.color.live_duration_background_color));
itemDurationView.setVisibility(View.VISIBLE);
} else {
itemDurationView.setVisibility(View.GONE);
}
// 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);
itemView.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
if (itemBuilder.getOnStreamSelectedListener() != null) {
itemBuilder.getOnStreamSelectedListener().selected(item);
}
}
});
}
/**
* 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

@@ -1,6 +1,24 @@
/*
* Copyright 2017 Mauricio Colli <mauriciocolli@outlook.com>
* BackgroundPlayer.java is part of NewPipe
*
* License: GPL-3.0+
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.schabi.newpipe.player;
import android.app.Notification;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.app.Service;
@@ -22,7 +40,7 @@ import org.schabi.newpipe.MainActivity;
import org.schabi.newpipe.R;
import org.schabi.newpipe.extractor.MediaFormat;
import org.schabi.newpipe.extractor.StreamingService;
import org.schabi.newpipe.extractor.stream_info.AudioStream;
import org.schabi.newpipe.extractor.stream.AudioStream;
import org.schabi.newpipe.util.Constants;
import org.schabi.newpipe.util.ThemeHelper;
@@ -66,7 +84,6 @@ public class BackgroundPlayer extends Service {
private RemoteViews bigNotRemoteView;
private final String setAlphaMethodName = (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) ? "setImageAlpha" : "setAlpha";
/*//////////////////////////////////////////////////////////////////////////
// Service's LifeCycle
//////////////////////////////////////////////////////////////////////////*/
@@ -145,13 +162,13 @@ public class BackgroundPlayer extends Service {
setupNotification(notRemoteView);
setupNotification(bigNotRemoteView);
NotificationCompat.Builder builder = new NotificationCompat.Builder(this)
NotificationCompat.Builder builder = new NotificationCompat.Builder(this, getString(R.string.notification_channel_id))
.setOngoing(true)
.setSmallIcon(R.drawable.ic_play_circle_filled_white_24dp)
.setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
.setCustomContentView(notRemoteView)
.setCustomBigContentView(bigNotRemoteView);
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.JELLY_BEAN) builder.setPriority(Notification.PRIORITY_MAX);
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.JELLY_BEAN) builder.setPriority(NotificationCompat.PRIORITY_MAX);
return builder;
}
@@ -159,7 +176,7 @@ public class BackgroundPlayer extends Service {
//if (videoThumbnail != null) remoteViews.setImageViewBitmap(R.id.notificationCover, videoThumbnail);
///else remoteViews.setImageViewResource(R.id.notificationCover, R.drawable.dummy_thumbnail);
remoteViews.setTextViewText(R.id.notificationSongName, basePlayerImpl.getVideoTitle());
remoteViews.setTextViewText(R.id.notificationArtist, basePlayerImpl.getChannelName());
remoteViews.setTextViewText(R.id.notificationArtist, basePlayerImpl.getUploaderName());
remoteViews.setOnClickPendingIntent(R.id.notificationPlayPause,
PendingIntent.getBroadcast(this, NOTIFICATION_ID, new Intent(ACTION_PLAY_PAUSE), PendingIntent.FLAG_UPDATE_CURRENT));
@@ -342,6 +359,11 @@ public class BackgroundPlayer extends Service {
// Disable default behavior
}
@Override
public void onRepeatModeChanged(int i) {
}
@Override
public void destroy() {
super.destroy();

View File

@@ -1,3 +1,22 @@
/*
* Copyright 2017 Mauricio Colli <mauriciocolli@outlook.com>
* BasePlayer.java is part of NewPipe
*
* License: GPL-3.0+
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.schabi.newpipe.player;
import android.animation.Animator;
@@ -19,10 +38,12 @@ import android.view.View;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.DefaultLoadControl;
import com.google.android.exoplayer2.DefaultRenderersFactory;
import com.google.android.exoplayer2.ExoPlaybackException;
import com.google.android.exoplayer2.ExoPlayer;
import com.google.android.exoplayer2.ExoPlayerFactory;
import com.google.android.exoplayer2.PlaybackParameters;
import com.google.android.exoplayer2.Player;
import com.google.android.exoplayer2.RenderersFactory;
import com.google.android.exoplayer2.SimpleExoPlayer;
import com.google.android.exoplayer2.Timeline;
import com.google.android.exoplayer2.extractor.DefaultExtractorsFactory;
@@ -47,6 +68,9 @@ import com.google.android.exoplayer2.util.Util;
import com.nostra13.universalimageloader.core.ImageLoader;
import com.nostra13.universalimageloader.core.listener.SimpleImageLoadingListener;
import org.schabi.newpipe.Downloader;
import org.schabi.newpipe.R;
import java.io.File;
import java.text.DecimalFormat;
import java.text.NumberFormat;
@@ -60,7 +84,9 @@ import java.util.concurrent.atomic.AtomicBoolean;
* @author mauriciocolli
*/
@SuppressWarnings({"WeakerAccess", "unused"})
public abstract class BasePlayer implements ExoPlayer.EventListener, AudioManager.OnAudioFocusChangeListener {
public abstract class BasePlayer implements Player.EventListener, AudioManager.OnAudioFocusChangeListener {
// TODO: Check api version for deprecated audio manager methods
public static final boolean DEBUG = false;
public static final String TAG = "BasePlayer";
@@ -86,8 +112,8 @@ public abstract class BasePlayer implements ExoPlayer.EventListener, AudioManage
protected String videoUrl = "";
protected String videoTitle = "";
protected String videoThumbnailUrl = "";
protected int videoStartPos = -1;
protected String channelName = "";
protected long videoStartPos = -1;
protected String uploaderName = "";
/*//////////////////////////////////////////////////////////////////////////
// Player
@@ -135,7 +161,7 @@ public abstract class BasePlayer implements ExoPlayer.EventListener, AudioManage
private void initExoPlayerCache() {
if (cacheDataSourceFactory == null) {
DefaultDataSourceFactory dataSourceFactory = new DefaultDataSourceFactory(context, Util.getUserAgent(context, context.getPackageName()), bandwidthMeter);
DefaultDataSourceFactory dataSourceFactory = new DefaultDataSourceFactory(context, Downloader.USER_AGENT, bandwidthMeter);
File cacheDir = new File(context.getExternalCacheDir(), CACHE_FOLDER_NAME);
if (!cacheDir.exists()) {
//noinspection ResultOfMethodCallIgnored
@@ -156,7 +182,8 @@ public abstract class BasePlayer implements ExoPlayer.EventListener, AudioManage
DefaultTrackSelector defaultTrackSelector = new DefaultTrackSelector(trackSelectionFactory);
DefaultLoadControl loadControl = new DefaultLoadControl();
simpleExoPlayer = ExoPlayerFactory.newSimpleInstance(context, defaultTrackSelector, loadControl);
final RenderersFactory renderFactory = new DefaultRenderersFactory(context);
simpleExoPlayer = ExoPlayerFactory.newSimpleInstance(renderFactory, defaultTrackSelector, loadControl);
simpleExoPlayer.addListener(this);
}
@@ -178,8 +205,8 @@ public abstract class BasePlayer implements ExoPlayer.EventListener, AudioManage
videoUrl = intent.getStringExtra(VIDEO_URL);
videoTitle = intent.getStringExtra(VIDEO_TITLE);
videoThumbnailUrl = intent.getStringExtra(VIDEO_THUMBNAIL_URL);
videoStartPos = intent.getIntExtra(START_POSITION, -1);
channelName = intent.getStringExtra(CHANNEL_NAME);
videoStartPos = intent.getLongExtra(START_POSITION, -1L);
uploaderName = intent.getStringExtra(CHANNEL_NAME);
setPlaybackSpeed(intent.getFloatExtra(PLAYBACK_SPEED, getPlaybackSpeed()));
initThumbnail();
@@ -195,7 +222,8 @@ public abstract class BasePlayer implements ExoPlayer.EventListener, AudioManage
@Override
public void onLoadingComplete(String imageUri, View view, Bitmap loadedImage) {
if (simpleExoPlayer == null) return;
if (DEBUG) Log.d(TAG, "onLoadingComplete() called with: imageUri = [" + imageUri + "], view = [" + view + "], loadedImage = [" + loadedImage + "]");
if (DEBUG)
Log.d(TAG, "onLoadingComplete() called with: imageUri = [" + imageUri + "], view = [" + view + "], loadedImage = [" + loadedImage + "]");
videoThumbnail = loadedImage;
onThumbnailReceived(loadedImage);
}
@@ -218,7 +246,7 @@ public abstract class BasePlayer implements ExoPlayer.EventListener, AudioManage
isPrepared = false;
mediaSource = buildMediaSource(url, format);
if (simpleExoPlayer.getPlaybackState() != ExoPlayer.STATE_IDLE) simpleExoPlayer.stop();
if (simpleExoPlayer.getPlaybackState() != Player.STATE_IDLE) simpleExoPlayer.stop();
if (videoStartPos > 0) simpleExoPlayer.seekTo(videoStartPos);
simpleExoPlayer.prepare(mediaSource);
simpleExoPlayer.setPlayWhenReady(autoPlay);
@@ -231,6 +259,10 @@ public abstract class BasePlayer implements ExoPlayer.EventListener, AudioManage
simpleExoPlayer.release();
}
if (progressLoop != null && isProgressLoopRunning.get()) stopProgressLoop();
if (audioManager != null) {
audioManager.abandonAudioFocus(this);
audioManager = null;
}
}
public void destroy() {
@@ -321,10 +353,17 @@ public abstract class BasePlayer implements ExoPlayer.EventListener, AudioManage
}
}
private boolean isResumeAfterAudioFocusGain() {
return sharedPreferences != null && context != null
&& sharedPreferences.getBoolean(context.getString(R.string.resume_on_audio_focus_gain_key), false);
}
protected void onAudioFocusGain() {
if (DEBUG) Log.d(TAG, "onAudioFocusGain() called");
if (simpleExoPlayer != null) simpleExoPlayer.setVolume(DUCK_AUDIO_TO);
animateAudio(DUCK_AUDIO_TO, 1f, DUCK_DURATION);
if (isResumeAfterAudioFocusGain()) simpleExoPlayer.setPlayWhenReady(true);
}
protected void onAudioFocusLoss() {
@@ -457,20 +496,21 @@ public abstract class BasePlayer implements ExoPlayer.EventListener, AudioManage
@Override
public void onPlayerStateChanged(boolean playWhenReady, int playbackState) {
if (DEBUG) Log.d(TAG, "onPlayerStateChanged() called with: playWhenReady = [" + playWhenReady + "], playbackState = [" + playbackState + "]");
if (DEBUG)
Log.d(TAG, "onPlayerStateChanged() called with: playWhenReady = [" + playWhenReady + "], playbackState = [" + playbackState + "]");
if (getCurrentState() == STATE_PAUSED_SEEK) {
if (DEBUG) Log.d(TAG, "onPlayerStateChanged() currently on PausedSeek");
return;
}
switch (playbackState) {
case ExoPlayer.STATE_IDLE: // 1
case Player.STATE_IDLE: // 1
isPrepared = false;
break;
case ExoPlayer.STATE_BUFFERING: // 2
case Player.STATE_BUFFERING: // 2
if (isPrepared && getCurrentState() != STATE_LOADING) changeState(STATE_BUFFERING);
break;
case ExoPlayer.STATE_READY: //3
case Player.STATE_READY: //3
if (!isPrepared) {
isPrepared = true;
onPrepared(playWhenReady);
@@ -479,7 +519,7 @@ public abstract class BasePlayer implements ExoPlayer.EventListener, AudioManage
if (currentState == STATE_PAUSED_SEEK) break;
changeState(playWhenReady ? STATE_PLAYING : STATE_PAUSED);
break;
case ExoPlayer.STATE_ENDED: // 4
case Player.STATE_ENDED: // 4
changeState(STATE_COMPLETED);
isPrepared = false;
break;
@@ -500,7 +540,9 @@ public abstract class BasePlayer implements ExoPlayer.EventListener, AudioManage
// General Player
//////////////////////////////////////////////////////////////////////////*/
public abstract void onError(Exception exception);
public void onError(Exception exception){
destroy();
}
public void onPrepared(boolean playWhenReady) {
if (DEBUG) Log.d(TAG, "onPrepared() called with: playWhenReady = [" + playWhenReady + "]");
@@ -519,7 +561,7 @@ public abstract class BasePlayer implements ExoPlayer.EventListener, AudioManage
}
if (!isPlaying()) audioManager.requestAudioFocus(this, AudioManager.STREAM_MUSIC, AudioManager.AUDIOFOCUS_GAIN);
else audioManager.abandonAudioFocus(null);
else audioManager.abandonAudioFocus(this);
simpleExoPlayer.setPlayWhenReady(!isPlaying());
}
@@ -549,14 +591,15 @@ public abstract class BasePlayer implements ExoPlayer.EventListener, AudioManage
public void seekBy(int milliSeconds) {
if (DEBUG) Log.d(TAG, "seekBy() called with: milliSeconds = [" + milliSeconds + "]");
if (simpleExoPlayer == null || (isCompleted() && milliSeconds > 0) || ((milliSeconds < 0 && simpleExoPlayer.getCurrentPosition() == 0))) return;
if (simpleExoPlayer == null || (isCompleted() && milliSeconds > 0) || ((milliSeconds < 0 && simpleExoPlayer.getCurrentPosition() == 0)))
return;
int progress = (int) (simpleExoPlayer.getCurrentPosition() + milliSeconds);
if (progress < 0) progress = 0;
simpleExoPlayer.seekTo(progress);
}
public boolean isPlaying() {
return simpleExoPlayer.getPlaybackState() == ExoPlayer.STATE_READY && simpleExoPlayer.getPlayWhenReady();
return simpleExoPlayer.getPlaybackState() == Player.STATE_READY && simpleExoPlayer.getPlayWhenReady();
}
/*//////////////////////////////////////////////////////////////////////////
@@ -677,11 +720,11 @@ public abstract class BasePlayer implements ExoPlayer.EventListener, AudioManage
this.videoUrl = videoUrl;
}
public int getVideoStartPos() {
public long getVideoStartPos() {
return videoStartPos;
}
public void setVideoStartPos(int videoStartPos) {
public void setVideoStartPos(long videoStartPos) {
this.videoStartPos = videoStartPos;
}
@@ -693,16 +736,16 @@ public abstract class BasePlayer implements ExoPlayer.EventListener, AudioManage
this.videoTitle = videoTitle;
}
public String getChannelName() {
return channelName;
public String getUploaderName() {
return uploaderName;
}
public void setChannelName(String channelName) {
this.channelName = channelName;
public void setUploaderName(String uploaderName) {
this.uploaderName = uploaderName;
}
public boolean isCompleted() {
return simpleExoPlayer != null && simpleExoPlayer.getPlaybackState() == ExoPlayer.STATE_ENDED;
return simpleExoPlayer != null && simpleExoPlayer.getPlaybackState() == Player.STATE_ENDED;
}
public boolean isPrepared() {

View File

@@ -1,3 +1,22 @@
/*
* Copyright 2017 Mauricio Colli <mauriciocolli@outlook.com>
* MainVideoPlayer.java is part of NewPipe
*
* License: GPL-3.0+
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.schabi.newpipe.player;
import android.app.Activity;
@@ -169,14 +188,14 @@ public class MainVideoPlayer extends Activity {
@Override
public void initViews(View rootView) {
super.initViews(rootView);
this.titleTextView = (TextView) rootView.findViewById(R.id.titleTextView);
this.channelTextView = (TextView) rootView.findViewById(R.id.channelTextView);
this.volumeTextView = (TextView) rootView.findViewById(R.id.volumeTextView);
this.brightnessTextView = (TextView) rootView.findViewById(R.id.brightnessTextView);
this.repeatButton = (ImageButton) rootView.findViewById(R.id.repeatButton);
this.titleTextView = rootView.findViewById(R.id.titleTextView);
this.channelTextView = rootView.findViewById(R.id.channelTextView);
this.volumeTextView = rootView.findViewById(R.id.volumeTextView);
this.brightnessTextView = rootView.findViewById(R.id.brightnessTextView);
this.repeatButton = rootView.findViewById(R.id.repeatButton);
this.screenRotationButton = (ImageButton) rootView.findViewById(R.id.screenRotationButton);
this.playPauseButton = (ImageButton) rootView.findViewById(R.id.playPauseButton);
this.screenRotationButton = rootView.findViewById(R.id.screenRotationButton);
this.playPauseButton = rootView.findViewById(R.id.playPauseButton);
// Due to a bug on lower API, lets set the alpha instead of using a drawable
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) repeatButton.setImageAlpha(77);
@@ -205,7 +224,7 @@ public class MainVideoPlayer extends Activity {
public void handleIntent(Intent intent) {
super.handleIntent(intent);
titleTextView.setText(getVideoTitle());
channelTextView.setText(getChannelName());
channelTextView.setText(getUploaderName());
}
@Override
@@ -422,6 +441,11 @@ public class MainVideoPlayer extends Activity {
public ImageButton getPlayPauseButton() {
return playPauseButton;
}
@Override
public void onRepeatModeChanged(int i) {
}
}
private class MySimpleOnGestureListener extends GestureDetector.SimpleOnGestureListener implements View.OnTouchListener {

View File

@@ -1,3 +1,22 @@
/*
* Copyright 2017 Mauricio Colli <mauriciocolli@outlook.com>
* PopupVideoPlayer.java is part of NewPipe
*
* License: GPL-3.0+
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.schabi.newpipe.player;
import android.annotation.SuppressLint;
@@ -15,6 +34,7 @@ import android.os.Build;
import android.os.Handler;
import android.os.IBinder;
import android.preference.PreferenceManager;
import android.support.annotation.NonNull;
import android.support.v4.app.NotificationCompat;
import android.util.DisplayMetrics;
import android.util.Log;
@@ -38,13 +58,29 @@ import org.schabi.newpipe.MainActivity;
import org.schabi.newpipe.R;
import org.schabi.newpipe.ReCaptchaActivity;
import org.schabi.newpipe.extractor.MediaFormat;
import org.schabi.newpipe.extractor.NewPipe;
import org.schabi.newpipe.extractor.StreamingService;
import org.schabi.newpipe.extractor.stream_info.StreamInfo;
import org.schabi.newpipe.extractor.exceptions.ContentNotAvailableException;
import org.schabi.newpipe.extractor.exceptions.ParsingException;
import org.schabi.newpipe.extractor.exceptions.ReCaptchaException;
import org.schabi.newpipe.extractor.services.youtube.YoutubeStreamExtractor;
import org.schabi.newpipe.extractor.stream.StreamInfo;
import org.schabi.newpipe.player.old.PlayVideoActivity;
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.ListHelper;
import org.schabi.newpipe.util.NavigationHelper;
import org.schabi.newpipe.util.ThemeHelper;
import org.schabi.newpipe.util.Utils;
import org.schabi.newpipe.workers.StreamExtractorWorker;
import java.io.IOException;
import java.util.ArrayList;
import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.disposables.Disposable;
import io.reactivex.functions.Consumer;
import io.reactivex.schedulers.Schedulers;
import static org.schabi.newpipe.util.AnimationUtils.animateView;
@@ -87,7 +123,7 @@ public class PopupVideoPlayer extends Service {
private DisplayImageOptions displayImageOptions = new DisplayImageOptions.Builder().cacheInMemory(true).build();
private VideoPlayerImpl playerImpl;
private StreamExtractorWorker currentExtractorWorker;
private Disposable currentWorker;
/*//////////////////////////////////////////////////////////////////////////
// Service LifeCycle
@@ -105,15 +141,33 @@ public class PopupVideoPlayer extends Service {
@Override
@SuppressWarnings("unchecked")
public int onStartCommand(final Intent intent, int flags, int startId) {
if (DEBUG) Log.d(TAG, "onStartCommand() called with: intent = [" + intent + "], flags = [" + flags + "], startId = [" + startId + "]");
if (DEBUG)
Log.d(TAG, "onStartCommand() called with: intent = [" + intent + "], flags = [" + flags + "], startId = [" + startId + "]");
if (playerImpl.getPlayer() == null) initPopup();
if (!playerImpl.isPlaying()) playerImpl.getPlayer().setPlayWhenReady(true);
if (imageLoader != null) imageLoader.clearMemoryCache();
if (intent.getStringExtra(Constants.KEY_URL) != null) {
final int serviceId = intent.getIntExtra(Constants.KEY_SERVICE_ID, 0);
final String url = intent.getStringExtra(Constants.KEY_URL);
playerImpl.setStartedFromNewPipe(false);
currentExtractorWorker = new StreamExtractorWorker(this, 0, intent.getStringExtra(Constants.KEY_URL), new FetcherRunnable(this));
currentExtractorWorker.start();
final FetcherHandler fetcherRunnable = new FetcherHandler(this, serviceId, url);
currentWorker = ExtractorHelper.getStreamInfo(serviceId,url,false)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(new Consumer<StreamInfo>() {
@Override
public void accept(@NonNull StreamInfo info) throws Exception {
fetcherRunnable.onReceive(info);
}
}, new Consumer<Throwable>() {
@Override
public void accept(@NonNull Throwable throwable) throws Exception {
fetcherRunnable.onError(throwable);
}
});
} else {
playerImpl.setStartedFromNewPipe(true);
playerImpl.handleIntent(intent);
@@ -137,11 +191,7 @@ public class PopupVideoPlayer extends Service {
if (playerImpl.getRootView() != null) windowManager.removeView(playerImpl.getRootView());
}
if (notificationManager != null) notificationManager.cancel(NOTIFICATION_ID);
if (currentExtractorWorker != null) {
currentExtractorWorker.cancel();
currentExtractorWorker = null;
}
if (currentWorker != null) currentWorker.dispose();
savePositionAndSize();
}
@@ -168,9 +218,11 @@ public class PopupVideoPlayer extends Service {
float defaultSize = getResources().getDimension(R.dimen.popup_default_width);
popupWidth = popupRememberSizeAndPos ? sharedPreferences.getFloat(POPUP_SAVED_WIDTH, defaultSize) : defaultSize;
final int layoutParamType = Build.VERSION.SDK_INT < android.os.Build.VERSION_CODES.O ? WindowManager.LayoutParams.TYPE_PHONE : WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY;
windowLayoutParams = new WindowManager.LayoutParams(
(int) popupWidth, (int) getMinimumVideoHeight(popupWidth),
WindowManager.LayoutParams.TYPE_PHONE,
layoutParamType,
WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE,
PixelFormat.TRANSLUCENT);
windowLayoutParams.gravity = Gravity.LEFT | Gravity.TOP;
@@ -202,7 +254,7 @@ public class PopupVideoPlayer extends Service {
else notRemoteView.setImageViewBitmap(R.id.notificationCover, playerImpl.getVideoThumbnail());
notRemoteView.setTextViewText(R.id.notificationSongName, playerImpl.getVideoTitle());
notRemoteView.setTextViewText(R.id.notificationArtist, playerImpl.getChannelName());
notRemoteView.setTextViewText(R.id.notificationArtist, playerImpl.getUploaderName());
notRemoteView.setOnClickPendingIntent(R.id.notificationPlayPause,
PendingIntent.getBroadcast(this, NOTIFICATION_ID, new Intent(ACTION_PLAY_PAUSE), PendingIntent.FLAG_UPDATE_CURRENT));
@@ -225,7 +277,7 @@ public class PopupVideoPlayer extends Service {
break;
}
return new NotificationCompat.Builder(this)
return new NotificationCompat.Builder(this, getString(R.string.notification_channel_id))
.setOngoing(true)
.setSmallIcon(R.drawable.ic_play_arrow_white)
.setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
@@ -273,9 +325,11 @@ public class PopupVideoPlayer extends Service {
//////////////////////////////////////////////////////////////////////////*/
private void checkPositionBounds() {
if (windowLayoutParams.x > screenWidth - windowLayoutParams.width) windowLayoutParams.x = (int) (screenWidth - windowLayoutParams.width);
if (windowLayoutParams.x > screenWidth - windowLayoutParams.width)
windowLayoutParams.x = (int) (screenWidth - windowLayoutParams.width);
if (windowLayoutParams.x < 0) windowLayoutParams.x = 0;
if (windowLayoutParams.y > screenHeight - windowLayoutParams.height) windowLayoutParams.y = (int) (screenHeight - windowLayoutParams.height);
if (windowLayoutParams.y > screenHeight - windowLayoutParams.height)
windowLayoutParams.y = (int) (screenHeight - windowLayoutParams.height);
if (windowLayoutParams.y < 0) windowLayoutParams.y = 0;
}
@@ -350,7 +404,7 @@ public class PopupVideoPlayer extends Service {
@Override
public void initViews(View rootView) {
super.initViews(rootView);
resizingIndicator = (TextView) rootView.findViewById(R.id.resizing_indicator);
resizingIndicator = rootView.findViewById(R.id.resizing_indicator);
}
@Override
@@ -429,7 +483,8 @@ public class PopupVideoPlayer extends Service {
hideControls(100, 0);
}
}
/*//////////////////////////////////////////////////////////////////////////
/*//////////////////////////////////////////////////////////////////////////
// Broadcast Receiver
//////////////////////////////////////////////////////////////////////////*/
@@ -509,6 +564,10 @@ public class PopupVideoPlayer extends Service {
public TextView getResizingIndicator() {
return resizingIndicator;
}
@Override
public void onRepeatModeChanged(int i) {
}
}
private class MySimpleOnGestureListener extends GestureDetector.SimpleOnGestureListener implements View.OnTouchListener {
@@ -521,7 +580,8 @@ public class PopupVideoPlayer extends Service {
@Override
public boolean onDoubleTap(MotionEvent e) {
if (DEBUG) Log.d(TAG, "onDoubleTap() called with: e = [" + e + "]" + "rawXy = " + e.getRawX() + ", " + e.getRawY() + ", xy = " + e.getX() + ", " + e.getY());
if (DEBUG)
Log.d(TAG, "onDoubleTap() called with: e = [" + e + "]" + "rawXy = " + e.getRawX() + ", " + e.getRawY() + ", xy = " + e.getX() + ", " + e.getY());
if (!playerImpl.isPlaying()) return false;
if (e.getX() > popupWidth / 2) playerImpl.onFastForward();
else playerImpl.onFastRewind();
@@ -615,7 +675,8 @@ public class PopupVideoPlayer extends Service {
}
if (event.getAction() == MotionEvent.ACTION_UP) {
if (DEBUG) Log.d(TAG, "onTouch() ACTION_UP > v = [" + v + "], e1.getRaw = [" + event.getRawX() + ", " + event.getRawY() + "]");
if (DEBUG)
Log.d(TAG, "onTouch() ACTION_UP > v = [" + v + "], e1.getRaw = [" + event.getRawX() + ", " + event.getRawY() + "]");
if (isMoving) {
isMoving = false;
onScrollEnd();
@@ -634,32 +695,36 @@ public class PopupVideoPlayer extends Service {
}
/**
* Fetcher used if open by a link out of NewPipe
* Fetcher handler used if open by a link out of NewPipe
*/
private class FetcherRunnable implements StreamExtractorWorker.OnStreamInfoReceivedListener {
private class FetcherHandler {
private final int serviceId;
private final String url;
private final Context context;
private final Handler mainHandler;
FetcherRunnable(Context context) {
FetcherHandler(Context context, int serviceId, String url) {
this.mainHandler = new Handler(PopupVideoPlayer.this.getMainLooper());
this.context = context;
this.url = url;
this.serviceId = serviceId;
}
@Override
public void onReceive(StreamInfo info) {
playerImpl.setVideoTitle(info.title);
playerImpl.setVideoUrl(info.webpage_url);
playerImpl.setVideoTitle(info.name);
playerImpl.setVideoUrl(info.url);
playerImpl.setVideoThumbnailUrl(info.thumbnail_url);
playerImpl.setChannelName(info.uploader);
playerImpl.setUploaderName(info.uploader_name);
playerImpl.setVideoStreamsList(Utils.getSortedStreamVideosList(context, info.video_streams, info.video_only_streams, false));
playerImpl.setAudioStream(Utils.getHighestQualityAudio(info.audio_streams));
playerImpl.setVideoStreamsList(new ArrayList<>(ListHelper.getSortedStreamVideosList(context, info.video_streams, info.video_only_streams, false)));
playerImpl.setAudioStream(ListHelper.getHighestQualityAudio(info.audio_streams));
int defaultResolution = Utils.getPopupDefaultResolution(context, playerImpl.getVideoStreamsList());
int defaultResolution = ListHelper.getPopupDefaultResolutionIndex(context, playerImpl.getVideoStreamsList());
playerImpl.setSelectedIndexStream(defaultResolution);
if (DEBUG) {
Log.d(TAG, "FetcherRunnable.StreamExtractor: chosen = "
Log.d(TAG, "FetcherHandler.StreamExtractor: chosen = "
+ MediaFormat.getNameById(info.video_streams.get(defaultResolution).format) + " "
+ info.video_streams.get(defaultResolution).resolution + " > "
+ info.video_streams.get(defaultResolution).url);
@@ -680,7 +745,7 @@ public class PopupVideoPlayer extends Service {
@Override
public void onLoadingComplete(final String imageUri, View view, final Bitmap loadedImage) {
if (playerImpl == null || playerImpl.getPlayer() == null) return;
if (DEBUG) Log.d(TAG, "FetcherRunnable.imageLoader.onLoadingComplete() called with: imageUri = [" + imageUri + "]");
if (DEBUG) Log.d(TAG, "FetcherHandler.imageLoader.onLoadingComplete() called with: imageUri = [" + imageUri + "]");
mainHandler.post(new Runnable() {
@Override
public void run() {
@@ -693,70 +758,40 @@ public class PopupVideoPlayer extends Service {
});
}
@Override
public void onError(final int messageId) {
protected void onError(final Throwable exception) {
if (DEBUG) Log.d(TAG, "onError() called with: exception = [" + exception + "]");
exception.printStackTrace();
mainHandler.post(new Runnable() {
@Override
public void run() {
Toast.makeText(context, messageId, Toast.LENGTH_LONG).show();
if (exception instanceof ReCaptchaException) {
onReCaptchaException();
} else if (exception instanceof IOException) {
Toast.makeText(context, R.string.network_error, Toast.LENGTH_LONG).show();
} else if (exception instanceof YoutubeStreamExtractor.GemaException) {
Toast.makeText(context, R.string.blocked_by_gema, Toast.LENGTH_LONG).show();
} else if (exception instanceof YoutubeStreamExtractor.LiveStreamException) {
Toast.makeText(context, R.string.live_streams_not_supported, Toast.LENGTH_LONG).show();
} else if (exception instanceof ContentNotAvailableException) {
Toast.makeText(context, R.string.content_not_available, Toast.LENGTH_LONG).show();
} else {
int errorId = exception instanceof YoutubeStreamExtractor.DecryptException ? R.string.youtube_signature_decryption_error :
exception instanceof ParsingException ? R.string.parsing_error : R.string.general_error;
ErrorActivity.reportError(mainHandler, context, exception, MainActivity.class, null, ErrorActivity.ErrorInfo.make(UserAction.REQUESTED_STREAM, NewPipe.getNameOfService(serviceId), url, errorId));
}
}
});
stopSelf();
}
@Override
public void onReCaptchaException() {
mainHandler.post(new Runnable() {
@Override
public void run() {
Toast.makeText(context, R.string.recaptcha_request_toast, Toast.LENGTH_LONG).show();
}
});
Toast.makeText(context, R.string.recaptcha_request_toast, Toast.LENGTH_LONG).show();
// Starting ReCaptcha Challenge Activity
Intent intent = new Intent(context, ReCaptchaActivity.class);
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
context.startActivity(intent);
stopSelf();
}
@Override
public void onBlockedByGemaError() {
mainHandler.post(new Runnable() {
@Override
public void run() {
Toast.makeText(context, R.string.blocked_by_gema, Toast.LENGTH_LONG).show();
}
});
stopSelf();
}
@Override
public void onContentErrorWithMessage(final int messageId) {
mainHandler.post(new Runnable() {
@Override
public void run() {
Toast.makeText(context, messageId, Toast.LENGTH_LONG).show();
}
});
stopSelf();
}
@Override
public void onContentError() {
mainHandler.post(new Runnable() {
@Override
public void run() {
Toast.makeText(context, R.string.content_not_available, Toast.LENGTH_LONG).show();
}
});
stopSelf();
}
@Override
public void onUnrecoverableError(Exception exception) {
exception.printStackTrace();
stopSelf();
}
}
}

View File

@@ -1,3 +1,22 @@
/*
* Copyright 2017 Mauricio Colli <mauriciocolli@outlook.com>
* VideoPlayer.java is part of NewPipe
*
* License: GPL-3.0+
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.schabi.newpipe.player;
import android.animation.Animator;
@@ -26,7 +45,7 @@ import android.widget.ProgressBar;
import android.widget.SeekBar;
import android.widget.TextView;
import com.google.android.exoplayer2.ExoPlayer;
import com.google.android.exoplayer2.Player;
import com.google.android.exoplayer2.SimpleExoPlayer;
import com.google.android.exoplayer2.source.ExtractorMediaSource;
import com.google.android.exoplayer2.source.MediaSource;
@@ -35,8 +54,8 @@ import com.google.android.exoplayer2.ui.AspectRatioFrameLayout;
import org.schabi.newpipe.R;
import org.schabi.newpipe.extractor.MediaFormat;
import org.schabi.newpipe.extractor.stream_info.AudioStream;
import org.schabi.newpipe.extractor.stream_info.VideoStream;
import org.schabi.newpipe.extractor.stream.AudioStream;
import org.schabi.newpipe.extractor.stream.VideoStream;
import org.schabi.newpipe.util.AnimationUtils;
import java.io.Serializable;
@@ -52,7 +71,7 @@ import static org.schabi.newpipe.util.AnimationUtils.animateView;
* @author mauriciocolli
*/
@SuppressWarnings({"WeakerAccess", "unused"})
public abstract class VideoPlayer extends BasePlayer implements SimpleExoPlayer.VideoListener, SeekBar.OnSeekBarChangeListener, View.OnClickListener, ExoPlayer.EventListener, PopupMenu.OnMenuItemClickListener, PopupMenu.OnDismissListener {
public abstract class VideoPlayer extends BasePlayer implements SimpleExoPlayer.VideoListener, SeekBar.OnSeekBarChangeListener, View.OnClickListener, Player.EventListener, PopupMenu.OnMenuItemClickListener, PopupMenu.OnDismissListener {
public static final boolean DEBUG = BasePlayer.DEBUG;
public final String TAG;
@@ -73,7 +92,7 @@ public abstract class VideoPlayer extends BasePlayer implements SimpleExoPlayer.
// Player
//////////////////////////////////////////////////////////////////////////*/
public static final int DEFAULT_CONTROLS_HIDE_TIME = 3000; // 3 Seconds
public static final int DEFAULT_CONTROLS_HIDE_TIME = 2000; // 2 Seconds
private static final float[] PLAYBACK_SPEEDS = {0.5f, 0.75f, 1f, 1.25f, 1.5f, 1.75f, 2f};
private boolean startedFromNewPipe = true;
@@ -132,26 +151,27 @@ public abstract class VideoPlayer extends BasePlayer implements SimpleExoPlayer.
public void initViews(View rootView) {
this.rootView = rootView;
this.aspectRatioFrameLayout = (AspectRatioFrameLayout) rootView.findViewById(R.id.aspectRatioLayout);
this.surfaceView = (SurfaceView) rootView.findViewById(R.id.surfaceView);
this.aspectRatioFrameLayout = rootView.findViewById(R.id.aspectRatioLayout);
this.surfaceView = rootView.findViewById(R.id.surfaceView);
this.surfaceForeground = rootView.findViewById(R.id.surfaceForeground);
this.loadingPanel = rootView.findViewById(R.id.loading_panel);
this.endScreen = (ImageView) rootView.findViewById(R.id.endScreen);
this.controlAnimationView = (ImageView) rootView.findViewById(R.id.controlAnimationView);
this.endScreen = rootView.findViewById(R.id.endScreen);
this.controlAnimationView = rootView.findViewById(R.id.controlAnimationView);
this.controlsRoot = rootView.findViewById(R.id.playbackControlRoot);
this.currentDisplaySeek = (TextView) rootView.findViewById(R.id.currentDisplaySeek);
this.playbackSeekBar = (SeekBar) rootView.findViewById(R.id.playbackSeekBar);
this.playbackCurrentTime = (TextView) rootView.findViewById(R.id.playbackCurrentTime);
this.playbackEndTime = (TextView) rootView.findViewById(R.id.playbackEndTime);
this.playbackSpeed = (TextView) rootView.findViewById(R.id.playbackSpeed);
this.currentDisplaySeek = rootView.findViewById(R.id.currentDisplaySeek);
this.playbackSeekBar = rootView.findViewById(R.id.playbackSeekBar);
this.playbackCurrentTime = rootView.findViewById(R.id.playbackCurrentTime);
this.playbackEndTime = rootView.findViewById(R.id.playbackEndTime);
this.playbackSpeed = rootView.findViewById(R.id.playbackSpeed);
this.bottomControlsRoot = rootView.findViewById(R.id.bottomControls);
this.topControlsRoot = rootView.findViewById(R.id.topControls);
this.qualityTextView = (TextView) rootView.findViewById(R.id.qualityTextView);
this.fullScreenButton = (ImageButton) rootView.findViewById(R.id.fullScreenButton);
this.qualityTextView = rootView.findViewById(R.id.qualityTextView);
this.fullScreenButton = rootView.findViewById(R.id.fullScreenButton);
//this.aspectRatioFrameLayout.setAspectRatio(16.0f / 9.0f);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) playbackSeekBar.getThumb().setColorFilter(Color.RED, PorterDuff.Mode.SRC_IN);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN)
playbackSeekBar.getThumb().setColorFilter(Color.RED, PorterDuff.Mode.SRC_IN);
this.playbackSeekBar.getProgressDrawable().setColorFilter(Color.RED, PorterDuff.Mode.MULTIPLY);
this.qualityPopupMenu = new PopupMenu(context, qualityTextView);
@@ -225,7 +245,7 @@ public abstract class VideoPlayer extends BasePlayer implements SimpleExoPlayer.
@Override
public MediaSource buildMediaSource(String url, String overrideExtension) {
MediaSource mediaSource = super.buildMediaSource(url, overrideExtension);
if (!getSelectedVideoStream().isVideoOnly) return mediaSource;
if (!getSelectedVideoStream().isVideoOnly || videoOnlyAudioStream == null) return mediaSource;
Uri audioUri = Uri.parse(videoOnlyAudioStream.url);
return new MergingMediaSource(mediaSource, new ExtractorMediaSource(audioUri, cacheDataSourceFactory, extractorsFactory, null, null));
@@ -268,7 +288,8 @@ public abstract class VideoPlayer extends BasePlayer implements SimpleExoPlayer.
playbackSeekBar.setProgress(0);
// Bug on lower api, disabling and enabling the seekBar resets the thumb color -.-, so sets the color again
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) playbackSeekBar.getThumb().setColorFilter(Color.RED, PorterDuff.Mode.SRC_IN);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN)
playbackSeekBar.getThumb().setColorFilter(Color.RED, PorterDuff.Mode.SRC_IN);
animateView(endScreen, false, 0);
loadingPanel.setBackgroundColor(Color.BLACK);
@@ -323,7 +344,8 @@ public abstract class VideoPlayer extends BasePlayer implements SimpleExoPlayer.
playbackEndTime.setText(getTimeString(playbackSeekBar.getMax()));
playbackCurrentTime.setText(playbackEndTime.getText());
// Bug on lower api, disabling and enabling the seekBar resets the thumb color -.-, so sets the color again
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) playbackSeekBar.getThumb().setColorFilter(Color.RED, PorterDuff.Mode.SRC_IN);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN)
playbackSeekBar.getThumb().setColorFilter(Color.RED, PorterDuff.Mode.SRC_IN);
animateView(surfaceForeground, true, 100);
@@ -359,8 +381,8 @@ public abstract class VideoPlayer extends BasePlayer implements SimpleExoPlayer.
if (DEBUG) Log.d(TAG, "onPrepared() called with: playWhenReady = [" + playWhenReady + "]");
if (videoStartPos > 0) {
playbackSeekBar.setProgress(videoStartPos);
playbackCurrentTime.setText(getTimeString(videoStartPos));
playbackSeekBar.setProgress((int) videoStartPos);
playbackCurrentTime.setText(getTimeString((int) videoStartPos));
videoStartPos = -1;
}
@@ -443,11 +465,12 @@ public abstract class VideoPlayer extends BasePlayer implements SimpleExoPlayer.
*/
@Override
public boolean onMenuItemClick(MenuItem menuItem) {
if (DEBUG) Log.d(TAG, "onMenuItemClick() called with: menuItem = [" + menuItem + "], menuItem.getItemId = [" + menuItem.getItemId() + "]");
if (DEBUG)
Log.d(TAG, "onMenuItemClick() called with: menuItem = [" + menuItem + "], menuItem.getItemId = [" + menuItem.getItemId() + "]");
if (qualityPopupMenuGroupId == menuItem.getGroupId()) {
if (selectedIndexStream == menuItem.getItemId()) return true;
setVideoStartPos((int) simpleExoPlayer.getCurrentPosition());
setVideoStartPos(simpleExoPlayer.getCurrentPosition());
selectedIndexStream = menuItem.getItemId();
if (!(getCurrentState() == STATE_COMPLETED)) play(wasPlaying);

View File

@@ -1,4 +1,4 @@
package org.schabi.newpipe.player;
package org.schabi.newpipe.player.old;
import android.content.Context;
import android.content.Intent;
@@ -29,7 +29,7 @@ import android.widget.VideoView;
import org.schabi.newpipe.R;
/**
/*
* Copyright (C) Christian Schabesberger 2015 <chris.schabesberger@mailbox.org>
* PlayVideoActivity.java is part of NewPipe.
*
@@ -122,8 +122,8 @@ public class PlayVideoActivity extends AppCompatActivity {
position = intent.getIntExtra(START_POSITION, 0)*1000;//convert from seconds to milliseconds
videoView = (VideoView) findViewById(R.id.video_view);
progressBar = (ProgressBar) findViewById(R.id.play_video_progress_bar);
videoView = findViewById(R.id.video_view);
progressBar = findViewById(R.id.play_video_progress_bar);
try {
videoView.setMediaController(mediaController);
videoView.setVideoURI(Uri.parse(intent.getStringExtra(STREAM_URL)));
@@ -146,7 +146,7 @@ public class PlayVideoActivity extends AppCompatActivity {
});
videoUrl = intent.getStringExtra(VIDEO_URL);
Button button = (Button) findViewById(R.id.content_button);
Button button = findViewById(R.id.content_button);
button.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {

View File

@@ -8,7 +8,7 @@ import org.acra.sender.ReportSender;
import org.acra.sender.ReportSenderException;
import org.schabi.newpipe.R;
/**
/*
* Created by Christian Schabesberger on 13.09.16.
*
* Copyright (C) Christian Schabesberger 2015 <chris.schabesberger@mailbox.org>

View File

@@ -6,9 +6,8 @@ import android.support.annotation.NonNull;
import org.acra.config.ACRAConfiguration;
import org.acra.sender.ReportSender;
import org.acra.sender.ReportSenderFactory;
import org.schabi.newpipe.report.AcraReportSender;
/**
/*
* Created by Christian Schabesberger on 13.09.16.
*
* Copyright (C) Christian Schabesberger 2015 <chris.schabesberger@mailbox.org>

View File

@@ -36,7 +36,7 @@ import org.schabi.newpipe.BuildConfig;
import org.schabi.newpipe.Downloader;
import org.schabi.newpipe.MainActivity;
import org.schabi.newpipe.R;
import org.schabi.newpipe.extractor.Parser;
import org.schabi.newpipe.extractor.utils.Parser;
import org.schabi.newpipe.util.ThemeHelper;
import java.io.PrintWriter;
@@ -47,7 +47,7 @@ import java.util.List;
import java.util.TimeZone;
import java.util.Vector;
/**
/*
* Created by Christian Schabesberger on 24.10.15.
*
* Copyright (C) Christian Schabesberger 2016 <chris.schabesberger@mailbox.org>
@@ -95,37 +95,34 @@ public class ErrorActivity extends AppCompatActivity {
public static void reportError(final Context context, final List<Throwable> el,
final Class returnActivity, View rootView, final ErrorInfo errorInfo) {
if (rootView != null) {
Snackbar.make(rootView, R.string.error_snackbar_message, Snackbar.LENGTH_LONG)
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) {
ActivityCommunicator ac = ActivityCommunicator.getCommunicator();
ac.returnActivity = returnActivity;
Intent intent = new Intent(context, ErrorActivity.class);
intent.putExtra(ERROR_INFO, errorInfo);
intent.putExtra(ERROR_LIST, elToSl(el));
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
context.startActivity(intent);
startErrorActivity(returnActivity, context, errorInfo, el);
}
}).show();
} else {
ActivityCommunicator ac = ActivityCommunicator.getCommunicator();
ac.returnActivity = returnActivity;
Intent intent = new Intent(context, ErrorActivity.class);
intent.putExtra(ERROR_INFO, errorInfo);
intent.putExtra(ERROR_LIST, elToSl(el));
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
context.startActivity(intent);
startErrorActivity(returnActivity, context, errorInfo, el);
}
}
private static void startErrorActivity(Class returnActivity, Context context, ErrorInfo errorInfo, List<Throwable> el) {
ActivityCommunicator ac = ActivityCommunicator.getCommunicator();
ac.returnActivity = returnActivity;
Intent intent = new Intent(context, ErrorActivity.class);
intent.putExtra(ERROR_INFO, errorInfo);
intent.putExtra(ERROR_LIST, elToSl(el));
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
context.startActivity(intent);
}
public static void reportError(final Context context, final Throwable e,
final Class returnActivity, View rootView, final ErrorInfo errorInfo) {
List<Throwable> el = null;
if(e != null) {
if (e != null) {
el = new Vector<>();
el.add(e);
}
@@ -137,7 +134,7 @@ public class ErrorActivity extends AppCompatActivity {
final Class returnActivity, final View rootView, final ErrorInfo errorInfo) {
List<Throwable> el = null;
if(e != null) {
if (e != null) {
el = new Vector<>();
el.add(e);
}
@@ -158,12 +155,12 @@ public class ErrorActivity extends AppCompatActivity {
public static void reportError(final Context context, final CrashReportData report, final ErrorInfo errorInfo) {
// get key first (don't ask about this solution)
ReportField key = null;
for(ReportField k : report.keySet()) {
if(k.toString().equals("STACK_TRACE")) {
for (ReportField k : report.keySet()) {
if (k.toString().equals("STACK_TRACE")) {
key = k;
}
}
String[] el = new String[] { report.get(key) };
String[] el = new String[]{report.get(key)};
Intent intent = new Intent(context, ErrorActivity.class);
intent.putExtra(ERROR_INFO, errorInfo);
@@ -196,7 +193,7 @@ public class ErrorActivity extends AppCompatActivity {
Intent intent = getIntent();
Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
Toolbar toolbar = findViewById(R.id.toolbar);
setSupportActionBar(toolbar);
ActionBar actionBar = getSupportActionBar();
@@ -206,11 +203,11 @@ public class ErrorActivity extends AppCompatActivity {
actionBar.setDisplayShowTitleEnabled(true);
}
reportButton = (Button) findViewById(R.id.errorReportButton);
userCommentBox = (EditText) findViewById(R.id.errorCommentBox);
errorView = (TextView) findViewById(R.id.errorView);
infoView = (TextView) findViewById(R.id.errorInfosView);
errorMessageView = (TextView) findViewById(R.id.errorMessageView);
reportButton = findViewById(R.id.errorReportButton);
userCommentBox = findViewById(R.id.errorCommentBox);
errorView = findViewById(R.id.errorView);
infoView = findViewById(R.id.errorInfosView);
errorMessageView = findViewById(R.id.errorMessageView);
ActivityCommunicator ac = ActivityCommunicator.getCommunicator();
returnActivity = ac.returnActivity;
@@ -240,7 +237,7 @@ public class ErrorActivity extends AppCompatActivity {
// normal bugreport
buildInfo(errorInfo);
if(errorInfo.message != 0) {
if (errorInfo.message != 0) {
errorMessageView.setText(errorInfo.message);
} else {
errorMessageView.setVisibility(View.GONE);
@@ -250,7 +247,7 @@ public class ErrorActivity extends AppCompatActivity {
errorView.setText(formErrorText(errorList));
//print stack trace once again for debugging:
for(String e : errorList) {
for (String e : errorList) {
Log.e(TAG, e);
}
}
@@ -283,7 +280,7 @@ public class ErrorActivity extends AppCompatActivity {
private String formErrorText(String[] el) {
String text = "";
if(el != null) {
if (el != null) {
for (String e : el) {
text += "-------------------------------------\n"
+ e;
@@ -295,13 +292,14 @@ public class ErrorActivity extends AppCompatActivity {
/**
* Get the checked activity.
*
* @param returnActivity the activity to return to
* @return the casted return activity or null
*/
@Nullable
static Class<? extends Activity> getReturnActivity(Class<?> returnActivity) {
Class<? extends Activity> checkedReturnActivity = null;
if (returnActivity != null){
if (returnActivity != null) {
if (Activity.class.isAssignableFrom(returnActivity)) {
checkedReturnActivity = returnActivity.asSubclass(Activity.class);
} else {
@@ -323,8 +321,8 @@ public class ErrorActivity extends AppCompatActivity {
}
private void buildInfo(ErrorInfo info) {
TextView infoLabelView = (TextView) findViewById(R.id.errorInfoLabelsView);
TextView infoView = (TextView) findViewById(R.id.errorInfosView);
TextView infoLabelView = findViewById(R.id.errorInfoLabelsView);
TextView infoView = findViewById(R.id.errorInfosView);
String text = "";
infoLabelView.setText(getString(R.string.info_labels).replace("\\n", "\n"));
@@ -356,7 +354,7 @@ public class ErrorActivity extends AppCompatActivity {
.put("ip_range", globIpRange);
JSONArray exceptionArray = new JSONArray();
if(errorList != null) {
if (errorList != null) {
for (String e : errorList) {
exceptionArray.put(e);
}
@@ -375,7 +373,7 @@ public class ErrorActivity extends AppCompatActivity {
}
private String getUserActionString(UserAction userAction) {
if(userAction == null) {
if (userAction == null) {
return "Your description is in another castle.";
} else {
return userAction.getMessage();
@@ -397,7 +395,7 @@ public class ErrorActivity extends AppCompatActivity {
private void addGuruMeditaion() {
//just an easter egg
TextView sorryView = (TextView) findViewById(R.id.errorSorryView);
TextView sorryView = findViewById(R.id.errorSorryView);
String text = sorryView.getText().toString();
text += "\n" + getString(R.string.guru_meditation);
sorryView.setText(text);
@@ -467,6 +465,7 @@ public class ErrorActivity extends AppCompatActivity {
private class IpRangeRequester implements Runnable {
Handler h = new Handler();
public void run() {
String ipRange = "none";
try {
@@ -475,7 +474,7 @@ public class ErrorActivity extends AppCompatActivity {
ipRange = Parser.matchGroup1("([0-9]*\\.[0-9]*\\.)[0-9]*\\.[0-9]*", ip)
+ "0.0";
} catch(Throwable e) {
} catch (Throwable e) {
Log.w(TAG, "Error while error: could not get iprange", e);
} finally {
h.post(new IpRangeReturnRunnable(ipRange));
@@ -485,12 +484,14 @@ public class ErrorActivity extends AppCompatActivity {
private class IpRangeReturnRunnable implements Runnable {
String ipRange;
public IpRangeReturnRunnable(String ipRange) {
this.ipRange = ipRange;
}
public void run() {
globIpRange = ipRange;
if(infoView != null) {
if (infoView != null) {
String text = infoView.getText().toString();
text += "\n" + globIpRange;
infoView.setText(text);

View File

@@ -4,14 +4,16 @@ package org.schabi.newpipe.report;
* The user actions that can cause an error.
*/
public enum UserAction {
SEARCHED("searched"),
REQUESTED_STREAM("requested stream"),
GET_SUGGESTIONS("get suggestions"),
SOMETHING_ELSE("something"),
USER_REPORT("user report"),
LOAD_IMAGE("load image"),
UI_ERROR("ui error"),
REQUESTED_CHANNEL("requested channel");
SUBSCRIPTION("subscription"),
LOAD_IMAGE("load image"),
SOMETHING_ELSE("something"),
SEARCHED("searched"),
GET_SUGGESTIONS("get suggestions"),
REQUESTED_STREAM("requested stream"),
REQUESTED_CHANNEL("requested channel"),
REQUESTED_PLAYLIST("requested playlist");
private final String message;

View File

@@ -0,0 +1,42 @@
package org.schabi.newpipe.settings;
import android.os.Bundle;
import android.support.annotation.Nullable;
import android.support.v7.preference.Preference;
import org.schabi.newpipe.R;
import org.schabi.newpipe.util.Constants;
public class AppearanceSettingsFragment extends BasePreferenceFragment {
/**
* Theme that was applied when the settings was opened (or recreated after a theme change)
*/
private String startThemeKey;
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
String themeKey = getString(R.string.theme_key);
startThemeKey = defaultPreferences.getString(themeKey, getString(R.string.default_theme_value));
findPreference(themeKey).setOnPreferenceChangeListener(themePreferenceChange);
}
@Override
public void onCreatePreferences(Bundle savedInstanceState, String rootKey) {
addPreferencesFromResource(R.xml.appearance_settings);
}
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
getActivity().recreate();
}
return false;
}
};
}

View File

@@ -0,0 +1,45 @@
package org.schabi.newpipe.settings;
import android.content.SharedPreferences;
import android.os.Bundle;
import android.preference.PreferenceManager;
import android.support.annotation.Nullable;
import android.support.v7.app.ActionBar;
import android.support.v7.app.AppCompatActivity;
import android.support.v7.preference.PreferenceFragmentCompat;
import android.view.View;
import org.schabi.newpipe.MainActivity;
public abstract class BasePreferenceFragment extends PreferenceFragmentCompat {
protected final String TAG = getClass().getSimpleName() + "@" + Integer.toHexString(hashCode());
protected boolean DEBUG = MainActivity.DEBUG;
protected SharedPreferences defaultPreferences;
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
defaultPreferences = PreferenceManager.getDefaultSharedPreferences(getActivity());
}
@Override
public void onViewCreated(View view, @Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
setDivider(null);
updateTitle();
}
@Override
public void onResume() {
super.onResume();
updateTitle();
}
private void updateTitle() {
if (getActivity() instanceof AppCompatActivity) {
ActionBar actionBar = ((AppCompatActivity) getActivity()).getSupportActionBar();
if (actionBar != null) actionBar.setTitle(getPreferenceScreen().getTitle());
}
}
}

View File

@@ -0,0 +1,12 @@
package org.schabi.newpipe.settings;
import android.os.Bundle;
import org.schabi.newpipe.R;
public class ContentSettingsFragment extends BasePreferenceFragment {
@Override
public void onCreatePreferences(Bundle savedInstanceState, String rootKey) {
addPreferencesFromResource(R.xml.content_settings);
}
}

View File

@@ -0,0 +1,79 @@
package org.schabi.newpipe.settings;
import android.app.Activity;
import android.content.Intent;
import android.os.Bundle;
import android.support.annotation.Nullable;
import android.support.v7.preference.Preference;
import android.util.Log;
import com.nononsenseapps.filepicker.FilePickerActivity;
import org.schabi.newpipe.R;
public class DownloadSettingsFragment extends BasePreferenceFragment {
private static final int REQUEST_DOWNLOAD_PATH = 0x1235;
private static final int REQUEST_DOWNLOAD_AUDIO_PATH = 0x1236;
private String DOWNLOAD_PATH_PREFERENCE;
private String DOWNLOAD_PATH_AUDIO_PREFERENCE;
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
initKeys();
updatePreferencesSummary();
}
@Override
public void onCreatePreferences(Bundle savedInstanceState, String rootKey) {
addPreferencesFromResource(R.xml.download_settings);
}
private void initKeys() {
DOWNLOAD_PATH_PREFERENCE = getString(R.string.download_path_key);
DOWNLOAD_PATH_AUDIO_PREFERENCE = getString(R.string.download_path_audio_key);
}
private void updatePreferencesSummary() {
findPreference(DOWNLOAD_PATH_PREFERENCE).setSummary(defaultPreferences.getString(DOWNLOAD_PATH_PREFERENCE, getString(R.string.download_path_summary)));
findPreference(DOWNLOAD_PATH_AUDIO_PREFERENCE).setSummary(defaultPreferences.getString(DOWNLOAD_PATH_AUDIO_PREFERENCE, getString(R.string.download_path_audio_summary)));
}
@Override
public boolean onPreferenceTreeClick(Preference preference) {
if (DEBUG) {
Log.d(TAG, "onPreferenceTreeClick() called with: preference = [" + preference + "]");
}
if (preference.getKey().equals(DOWNLOAD_PATH_PREFERENCE) || preference.getKey().equals(DOWNLOAD_PATH_AUDIO_PREFERENCE)) {
Intent i = new Intent(getActivity(), FilePickerActivity.class)
.putExtra(FilePickerActivity.EXTRA_ALLOW_MULTIPLE, false)
.putExtra(FilePickerActivity.EXTRA_ALLOW_CREATE_DIR, true)
.putExtra(FilePickerActivity.EXTRA_MODE, FilePickerActivity.MODE_DIR);
if (preference.getKey().equals(DOWNLOAD_PATH_PREFERENCE)) {
startActivityForResult(i, REQUEST_DOWNLOAD_PATH);
} else if (preference.getKey().equals(DOWNLOAD_PATH_AUDIO_PREFERENCE)) {
startActivityForResult(i, REQUEST_DOWNLOAD_AUDIO_PATH);
}
}
return super.onPreferenceTreeClick(preference);
}
@Override
public void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if (DEBUG) {
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) {
String key = getString(requestCode == REQUEST_DOWNLOAD_PATH ? R.string.download_path_key : R.string.download_path_audio_key);
String path = data.getData().getPath();
defaultPreferences.edit().putString(key, path).apply();
updatePreferencesSummary();
}
}
}

View File

@@ -0,0 +1,12 @@
package org.schabi.newpipe.settings;
import android.os.Bundle;
import org.schabi.newpipe.R;
public class HistorySettingsFragment extends BasePreferenceFragment {
@Override
public void onCreatePreferences(Bundle savedInstanceState, String rootKey) {
addPreferencesFromResource(R.xml.history_settings);
}
}

View File

@@ -0,0 +1,12 @@
package org.schabi.newpipe.settings;
import android.os.Bundle;
import org.schabi.newpipe.R;
public class MainSettingsFragment extends BasePreferenceFragment {
@Override
public void onCreatePreferences(Bundle savedInstanceState, String rootKey) {
addPreferencesFromResource(R.xml.main_settings);
}
}

View File

@@ -1,4 +1,4 @@
/**
/*
* Created by k3b on 07.01.2016.
*
* Copyright (C) Christian Schabesberger 2015 <chris.schabesberger@mailbox.org>
@@ -30,13 +30,11 @@ import org.schabi.newpipe.R;
import java.io.File;
import us.shandian.giga.util.Utility;
/**
* Helper for global settings
*/
/**
/*
* Copyright (C) Christian Schabesberger 2016 <chris.schabesberger@mailbox.org>
* NewPipeSettings.java is part of NewPipe.
*
@@ -60,7 +58,13 @@ public class NewPipeSettings {
}
public static void initSettings(Context context) {
PreferenceManager.setDefaultValues(context, R.xml.settings, false);
PreferenceManager.setDefaultValues(context, R.xml.appearance_settings, true);
PreferenceManager.setDefaultValues(context, R.xml.content_settings, true);
PreferenceManager.setDefaultValues(context, R.xml.download_settings, true);
PreferenceManager.setDefaultValues(context, R.xml.history_settings, true);
PreferenceManager.setDefaultValues(context, R.xml.main_settings, true);
PreferenceManager.setDefaultValues(context, R.xml.video_audio_settings, true);
getVideoDownloadFolder(context);
getAudioDownloadFolder(context);
}
@@ -93,14 +97,13 @@ public class NewPipeSettings {
final File folder = getFolder(defaultDirectoryName);
SharedPreferences.Editor spEditor = prefs.edit();
spEditor.putString(key
, new File(folder,"NewPipe").getAbsolutePath());
spEditor.putString(key, new File(folder, "NewPipe").getAbsolutePath());
spEditor.apply();
return folder;
}
@NonNull
private static File getFolder(String defaultDirectoryName) {
return new File(Environment.getExternalStorageDirectory(),defaultDirectoryName);
return new File(Environment.getExternalStorageDirectory(), defaultDirectoryName);
}
}

View File

@@ -2,16 +2,20 @@ package org.schabi.newpipe.settings;
import android.content.Context;
import android.os.Bundle;
import android.support.v4.app.Fragment;
import android.support.v7.app.ActionBar;
import android.support.v7.app.AppCompatActivity;
import android.support.v7.preference.Preference;
import android.support.v7.preference.PreferenceFragmentCompat;
import android.support.v7.widget.Toolbar;
import android.view.Menu;
import android.view.MenuItem;
import org.schabi.newpipe.R;
import org.schabi.newpipe.util.ThemeHelper;
/**
/*
* Created by Christian Schabesberger on 31.08.15.
*
* Copyright (C) Christian Schabesberger 2015 <chris.schabesberger@mailbox.org>
@@ -31,7 +35,7 @@ import org.schabi.newpipe.util.ThemeHelper;
* along with NewPipe. If not, see <http://www.gnu.org/licenses/>.
*/
public class SettingsActivity extends AppCompatActivity {
public class SettingsActivity extends AppCompatActivity implements BasePreferenceFragment.OnPreferenceStartFragmentCallback {
public static void initSettings(Context context) {
NewPipeSettings.initSettings(context);
@@ -43,21 +47,25 @@ public class SettingsActivity extends AppCompatActivity {
super.onCreate(savedInstanceBundle);
setContentView(R.layout.settings_layout);
Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
Toolbar toolbar = findViewById(R.id.toolbar);
setSupportActionBar(toolbar);
if (savedInstanceBundle == null) {
getSupportFragmentManager().beginTransaction()
.replace(R.id.fragment_holder, new MainSettingsFragment())
.commit();
}
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
ActionBar actionBar = getSupportActionBar();
if (actionBar != null) {
actionBar.setDisplayHomeAsUpEnabled(true);
actionBar.setTitle(R.string.settings);
actionBar.setDisplayShowTitleEnabled(true);
}
if (savedInstanceBundle == null) {
getFragmentManager().beginTransaction()
.replace(R.id.fragment_holder, new SettingsFragment())
.commit();
}
return super.onCreateOptionsMenu(menu);
}
@Override
@@ -68,4 +76,15 @@ public class SettingsActivity extends AppCompatActivity {
}
return true;
}
@Override
public boolean onPreferenceStartFragment(PreferenceFragmentCompat caller, Preference preference) {
Fragment fragment = Fragment.instantiate(this, preference.getFragment(), preference.getExtras());
getSupportFragmentManager().beginTransaction()
.setCustomAnimations(R.animator.custom_fade_in, R.animator.custom_fade_out, R.animator.custom_fade_in, R.animator.custom_fade_out)
.replace(R.id.fragment_holder, fragment)
.addToBackStack(null)
.commit();
return true;
}
}

View File

@@ -1,142 +0,0 @@
package org.schabi.newpipe.settings;
import android.app.Activity;
import android.content.Intent;
import android.content.SharedPreferences;
import android.os.Bundle;
import android.preference.Preference;
import android.preference.PreferenceFragment;
import android.preference.PreferenceManager;
import android.preference.PreferenceScreen;
import android.text.TextUtils;
import android.util.Log;
import com.nononsenseapps.filepicker.FilePickerActivity;
import org.schabi.newpipe.App;
import org.schabi.newpipe.MainActivity;
import org.schabi.newpipe.R;
import org.schabi.newpipe.util.Constants;
import info.guardianproject.netcipher.proxy.OrbotHelper;
public class SettingsFragment extends PreferenceFragment implements SharedPreferences.OnSharedPreferenceChangeListener {
private static final int REQUEST_INSTALL_ORBOT = 0x1234;
private static final int REQUEST_DOWNLOAD_PATH = 0x1235;
private static final int REQUEST_DOWNLOAD_AUDIO_PATH = 0x1236;
private String DOWNLOAD_PATH_PREFERENCE;
private String DOWNLOAD_PATH_AUDIO_PREFERENCE;
private String USE_TOR_KEY;
private String THEME;
private String currentTheme;
private SharedPreferences defaultPreferences;
private Activity activity;
@Override
public void onCreate(final Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
activity = getActivity();
addPreferencesFromResource(R.xml.settings);
defaultPreferences = PreferenceManager.getDefaultSharedPreferences(activity);
initKeys();
updatePreferencesSummary();
currentTheme = defaultPreferences.getString(THEME, getString(R.string.default_theme_value));
}
@Override
public void onResume() {
super.onResume();
defaultPreferences.registerOnSharedPreferenceChangeListener(this);
}
@Override
public void onStop() {
super.onStop();
defaultPreferences.unregisterOnSharedPreferenceChangeListener(this);
}
private void initKeys() {
DOWNLOAD_PATH_PREFERENCE = getString(R.string.download_path_key);
DOWNLOAD_PATH_AUDIO_PREFERENCE = getString(R.string.download_path_audio_key);
THEME = getString(R.string.theme_key);
USE_TOR_KEY = getString(R.string.use_tor_key);
}
@Override
public boolean onPreferenceTreeClick(PreferenceScreen preferenceScreen, Preference preference) {
if (MainActivity.DEBUG) Log.d("TAG", "onPreferenceTreeClick() called with: preferenceScreen = [" + preferenceScreen + "], preference = [" + preference + "]");
if (preference.getKey().equals(DOWNLOAD_PATH_PREFERENCE) || preference.getKey().equals(DOWNLOAD_PATH_AUDIO_PREFERENCE)) {
Intent i = new Intent(activity, FilePickerActivity.class)
.putExtra(FilePickerActivity.EXTRA_ALLOW_MULTIPLE, false)
.putExtra(FilePickerActivity.EXTRA_ALLOW_CREATE_DIR, true)
.putExtra(FilePickerActivity.EXTRA_MODE, FilePickerActivity.MODE_DIR);
if (preference.getKey().equals(DOWNLOAD_PATH_PREFERENCE)) {
startActivityForResult(i, REQUEST_DOWNLOAD_PATH);
} else if (preference.getKey().equals(DOWNLOAD_PATH_AUDIO_PREFERENCE)) {
startActivityForResult(i, REQUEST_DOWNLOAD_AUDIO_PATH);
}
}
return super.onPreferenceTreeClick(preferenceScreen, preference);
}
@Override
public void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if (MainActivity.DEBUG) 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) {
String key = getString(requestCode == REQUEST_DOWNLOAD_PATH ? R.string.download_path_key : R.string.download_path_audio_key);
String path = data.getData().getPath();
defaultPreferences.edit().putString(key, path).apply();
updatePreferencesSummary();
} else if (requestCode == REQUEST_INSTALL_ORBOT) {
// try to start tor regardless of resultCode since clicking back after
// installing the app does not necessarily return RESULT_OK
App.configureTor(OrbotHelper.requestStartTor(activity));
}
}
/*
* Update ONLY the summary of some preferences that don't fire in the onSharedPreferenceChanged or CAN'T be update via xml (%s)
*
* For example, the download_path use the startActivityForResult, firing the onStop of this fragment,
* unregistering the listener (unregisterOnSharedPreferenceChangeListener)
*/
private void updatePreferencesSummary() {
findPreference(DOWNLOAD_PATH_PREFERENCE).setSummary(defaultPreferences.getString(DOWNLOAD_PATH_PREFERENCE, getString(R.string.download_path_summary)));
findPreference(DOWNLOAD_PATH_AUDIO_PREFERENCE).setSummary(defaultPreferences.getString(DOWNLOAD_PATH_AUDIO_PREFERENCE, getString(R.string.download_path_audio_summary)));
}
@Override
public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) {
if (MainActivity.DEBUG) Log.d("TAG", "onSharedPreferenceChanged() called with: sharedPreferences = [" + sharedPreferences + "], key = [" + key + "]");
String summary = null;
if (key.equals(USE_TOR_KEY)) {
if (defaultPreferences.getBoolean(USE_TOR_KEY, false)) {
if (OrbotHelper.isOrbotInstalled(activity)) {
App.configureTor(true);
OrbotHelper.requestStartTor(activity);
} else {
Intent intent = OrbotHelper.getOrbotInstallIntent(activity);
startActivityForResult(intent, REQUEST_INSTALL_ORBOT);
}
} else App.configureTor(false);
return;
} else if (key.equals(THEME)) {
summary = sharedPreferences.getString(THEME, getString(R.string.default_theme_value));
if (!summary.equals(currentTheme)) { // If it's not the current theme
getActivity().recreate();
}
defaultPreferences.edit().putBoolean(Constants.KEY_THEME_CHANGE, true).apply();
}
if (!TextUtils.isEmpty(summary)) findPreference(key).setSummary(summary);
}
}

View File

@@ -0,0 +1,12 @@
package org.schabi.newpipe.settings;
import android.os.Bundle;
import org.schabi.newpipe.R;
public class VideoAudioSettingsFragment extends BasePreferenceFragment {
@Override
public void onCreatePreferences(Bundle savedInstanceState, String rootKey) {
addPreferencesFromResource(R.xml.video_audio_settings);
}
}

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