1
0
mirror of https://github.com/TeamNewPipe/NewPipe synced 2026-01-14 21:48:00 +00:00

Compare commits

..

305 Commits

Author SHA1 Message Date
Tobi
94b4c76749 Merge pull request #6840 from TeamNewPipe/release_0.21.9
Release 0.21.9
2021-08-22 22:21:36 +02:00
TobiGr
8715e7dd98 Only show "mark as watched" context menu entry when watch history is enabled 2021-08-22 22:15:05 +02:00
TobiGr
ccc2d892c1 Update extractor version to 0.21.9 2021-08-22 20:23:01 +02:00
TobiGr
d1ce8e7baa Removed unsued string from translations: item_in_history 2021-08-22 20:23:01 +02:00
TobiGr
82fbbbecac Fixed plurals 2021-08-22 20:23:01 +02:00
Hosted Weblate
4e15f0ddac Translated using Weblate (Finnish)
Currently translated at 10.7% (6 of 56 strings)

Translated using Weblate (Finnish)

Currently translated at 100.0% (678 of 678 strings)

Translated using Weblate (Spanish)

Currently translated at 53.5% (30 of 56 strings)

Translated using Weblate (Polish)

Currently translated at 51.7% (29 of 56 strings)

Translated using Weblate (Swedish)

Currently translated at 99.5% (675 of 678 strings)

Translated using Weblate (Swedish)

Currently translated at 99.5% (675 of 678 strings)

Translated using Weblate (Swedish)

Currently translated at 99.5% (675 of 678 strings)

Translated using Weblate (Galician)

Currently translated at 93.6% (635 of 678 strings)

Translated using Weblate (Estonian)

Currently translated at 100.0% (678 of 678 strings)

Translated using Weblate (Croatian)

Currently translated at 100.0% (678 of 678 strings)

Translated using Weblate (Indonesian)

Currently translated at 99.8% (677 of 678 strings)

Translated using Weblate (Japanese)

Currently translated at 99.8% (677 of 678 strings)

Translated using Weblate (Croatian)

Currently translated at 97.0% (658 of 678 strings)

Translated using Weblate (Croatian)

Currently translated at 97.0% (658 of 678 strings)

Translated using Weblate (Vietnamese)

Currently translated at 100.0% (678 of 678 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (678 of 678 strings)

Translated using Weblate (Greek)

Currently translated at 100.0% (678 of 678 strings)

Translated using Weblate (Somali)

Currently translated at 100.0% (678 of 678 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (678 of 678 strings)

Translated using Weblate (Swedish)

Currently translated at 3.5% (2 of 56 strings)

Translated using Weblate (Swedish)

Currently translated at 98.0% (665 of 678 strings)

Translated using Weblate (Turkish)

Currently translated at 100.0% (678 of 678 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (678 of 678 strings)

Translated using Weblate (German)

Currently translated at 99.8% (677 of 678 strings)

Translated using Weblate (Ukrainian)

Currently translated at 53.5% (30 of 56 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (678 of 678 strings)

Translated using Weblate (French)

Currently translated at 100.0% (678 of 678 strings)

Translated using Weblate (Portuguese (Portugal))

Currently translated at 53.5% (30 of 56 strings)

Translated using Weblate (Portuguese)

Currently translated at 53.5% (30 of 56 strings)

Translated using Weblate (Croatian)

Currently translated at 96.7% (656 of 678 strings)

Translated using Weblate (Swedish)

Currently translated at 97.1% (659 of 678 strings)

Translated using Weblate (Portuguese)

Currently translated at 100.0% (678 of 678 strings)

Translated using Weblate (English)

Currently translated at 99.8% (678 of 679 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (679 of 679 strings)

Translated using Weblate (Ukrainian)

Currently translated at 53.5% (30 of 56 strings)

Translated using Weblate (Portuguese (Portugal))

Currently translated at 100.0% (679 of 679 strings)

Translated using Weblate (Albanian)

Currently translated at 100.0% (679 of 679 strings)

Translated using Weblate (Arabic)

Currently translated at 100.0% (679 of 679 strings)

Translated using Weblate (Portuguese)

Currently translated at 99.8% (678 of 679 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (56 of 56 strings)

Translated using Weblate (Polish)

Currently translated at 48.2% (27 of 56 strings)

Translated using Weblate (Portuguese (Portugal))

Currently translated at 94.2% (640 of 679 strings)

Translated using Weblate (Lithuanian)

Currently translated at 100.0% (679 of 679 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (679 of 679 strings)

Translated using Weblate (Persian)

Currently translated at 94.4% (641 of 679 strings)

Translated using Weblate (Polish)

Currently translated at 100.0% (679 of 679 strings)

Translated using Weblate (French)

Currently translated at 99.8% (678 of 679 strings)

Translated using Weblate (Sardinian)

Currently translated at 100.0% (679 of 679 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 99.8% (678 of 679 strings)

Translated using Weblate (Somali)

Currently translated at 100.0% (679 of 679 strings)

Translated using Weblate (Hebrew)

Currently translated at 48.1% (26 of 54 strings)

Translated using Weblate (Hebrew)

Currently translated at 100.0% (679 of 679 strings)

Translated using Weblate (Chinese (Traditional))

Currently translated at 100.0% (679 of 679 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (679 of 679 strings)

Translated using Weblate (Polish)

Currently translated at 100.0% (679 of 679 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (679 of 679 strings)

Translated using Weblate (Arabic)

Currently translated at 99.7% (677 of 679 strings)

Translated using Weblate (Czech)

Currently translated at 100.0% (679 of 679 strings)

Translated using Weblate (Czech)

Currently translated at 100.0% (679 of 679 strings)

Translated using Weblate (Greek)

Currently translated at 99.7% (677 of 679 strings)

Translated using Weblate (Russian)

Currently translated at 100.0% (679 of 679 strings)

Translated using Weblate (Russian)

Currently translated at 100.0% (679 of 679 strings)

Translated using Weblate (French)

Currently translated at 99.8% (678 of 679 strings)

Translated using Weblate (Spanish)

Currently translated at 99.8% (678 of 679 strings)

Translated using Weblate (German)

Currently translated at 99.8% (678 of 679 strings)

Translated using Weblate (German)

Currently translated at 99.8% (678 of 679 strings)

Translated using Weblate (German)

Currently translated at 99.8% (678 of 679 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (679 of 679 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (679 of 679 strings)

Translated using Weblate (Tamil)

Currently translated at 36.6% (248 of 677 strings)

Translated using Weblate (Turkish)

Currently translated at 100.0% (677 of 677 strings)

Translated using Weblate (Esperanto)

Currently translated at 85.6% (580 of 677 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 26.4% (14 of 53 strings)

Translated using Weblate (Estonian)

Currently translated at 99.8% (676 of 677 strings)

Translated using Weblate (Swedish)

Currently translated at 97.4% (660 of 677 strings)

Translated using Weblate (Polish)

Currently translated at 100.0% (677 of 677 strings)

Translated using Weblate (Korean)

Currently translated at 76.0% (515 of 677 strings)

Translated using Weblate (Russian)

Currently translated at 100.0% (677 of 677 strings)

Translated using Weblate (Spanish)

Currently translated at 28.3% (15 of 53 strings)

Translated using Weblate (Estonian)

Currently translated at 97.0% (657 of 677 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (677 of 677 strings)

Translated using Weblate (Basque)

Currently translated at 100.0% (677 of 677 strings)

Translated using Weblate (Hungarian)

Currently translated at 85.6% (580 of 677 strings)

Translated using Weblate (Hungarian)

Currently translated at 85.6% (580 of 677 strings)

Translated using Weblate (Hungarian)

Currently translated at 85.6% (580 of 677 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (677 of 677 strings)

Translated using Weblate (Swedish)

Currently translated at 3.7% (2 of 53 strings)

Translated using Weblate (French)

Currently translated at 67.9% (36 of 53 strings)

Translated using Weblate (Vietnamese)

Currently translated at 100.0% (677 of 677 strings)

Translated using Weblate (Swedish)

Currently translated at 97.3% (659 of 677 strings)

Translated using Weblate (Swedish)

Currently translated at 97.3% (659 of 677 strings)

Translated using Weblate (Polish)

Currently translated at 100.0% (677 of 677 strings)

Translated using Weblate (Latvian)

Currently translated at 94.5% (640 of 677 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 24.5% (13 of 53 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (677 of 677 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (677 of 677 strings)

Translated using Weblate (Polish)

Currently translated at 100.0% (677 of 677 strings)

Translated using Weblate (Turkish)

Currently translated at 100.0% (677 of 677 strings)

Translated using Weblate (Czech)

Currently translated at 100.0% (677 of 677 strings)

Translated using Weblate (German)

Currently translated at 100.0% (677 of 677 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (677 of 677 strings)

Translated using Weblate (Sardinian)

Currently translated at 100.0% (677 of 677 strings)

Translated using Weblate (Galician)

Currently translated at 91.5% (620 of 677 strings)

Translated using Weblate (Arabic)

Currently translated at 100.0% (677 of 677 strings)

Translated using Weblate (Arabic)

Currently translated at 100.0% (677 of 677 strings)

Translated using Weblate (Japanese)

Currently translated at 100.0% (677 of 677 strings)

Translated using Weblate (Russian)

Currently translated at 100.0% (677 of 677 strings)

Translated using Weblate (French)

Currently translated at 100.0% (677 of 677 strings)

Translated using Weblate (Japanese)

Currently translated at 100.0% (677 of 677 strings)

Translated using Weblate (Japanese)

Currently translated at 100.0% (677 of 677 strings)

Translated using Weblate (Greek)

Currently translated at 100.0% (677 of 677 strings)

Translated using Weblate (Bengali (Bangladesh))

Currently translated at 59.6% (404 of 677 strings)

Translated using Weblate (Somali)

Currently translated at 100.0% (677 of 677 strings)

Translated using Weblate (Norwegian Bokmål)

Currently translated at 97.1% (658 of 677 strings)

Translated using Weblate (Lithuanian)

Currently translated at 100.0% (677 of 677 strings)

Translated using Weblate (Hebrew)

Currently translated at 100.0% (677 of 677 strings)

Translated using Weblate (Chinese (Traditional))

Currently translated at 100.0% (677 of 677 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (677 of 677 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (677 of 677 strings)

Translated using Weblate (Polish)

Currently translated at 100.0% (677 of 677 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (677 of 677 strings)

Translated using Weblate (Indonesian)

Currently translated at 99.8% (676 of 677 strings)

Translated using Weblate (Greek)

Currently translated at 100.0% (677 of 677 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (677 of 677 strings)

Translated using Weblate (Russian)

Currently translated at 99.8% (676 of 677 strings)

Translated using Weblate (French)

Currently translated at 99.8% (676 of 677 strings)

Translated using Weblate (Romanian)

Currently translated at 93.0% (626 of 673 strings)

Translated using Weblate (Kurdish (Central))

Currently translated at 3.7% (2 of 53 strings)

Translated using Weblate (Kurdish (Central))

Currently translated at 100.0% (673 of 673 strings)

Translated using Weblate (Chinese (Traditional))

Currently translated at 100.0% (673 of 673 strings)

Translated using Weblate (Kurdish (Central))

Currently translated at 90.3% (608 of 673 strings)

Translated using Weblate (Hebrew)

Currently translated at 100.0% (673 of 673 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (673 of 673 strings)

Translated using Weblate (Arabic)

Currently translated at 100.0% (673 of 673 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (673 of 673 strings)

Translated using Weblate (Kurdish (Central))

Currently translated at 87.9% (592 of 673 strings)

Translated using Weblate (Lithuanian)

Currently translated at 100.0% (673 of 673 strings)

Translated using Weblate (Polish)

Currently translated at 100.0% (673 of 673 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (673 of 673 strings)

Translated using Weblate (Greek)

Currently translated at 100.0% (673 of 673 strings)

Translated using Weblate (Russian)

Currently translated at 100.0% (673 of 673 strings)

Translated using Weblate (French)

Currently translated at 100.0% (673 of 673 strings)

Translated using Weblate (Somali)

Currently translated at 100.0% (672 of 672 strings)

Translated using Weblate (Albanian)

Currently translated at 100.0% (672 of 672 strings)

Translated using Weblate (Polish)

Currently translated at 100.0% (672 of 672 strings)

Translated using Weblate (Polish)

Currently translated at 100.0% (672 of 672 strings)

Translated using Weblate (Polish)

Currently translated at 100.0% (672 of 672 strings)

Translated using Weblate (Polish)

Currently translated at 100.0% (672 of 672 strings)

Translated using Weblate (Bulgarian)

Currently translated at 1.8% (1 of 53 strings)

Translated using Weblate (Bulgarian)

Currently translated at 57.8% (389 of 672 strings)

Translated using Weblate (Bulgarian)

Currently translated at 57.8% (389 of 672 strings)

Translated using Weblate (Bulgarian)

Currently translated at 57.8% (389 of 672 strings)

Translated using Weblate (Polish)

Currently translated at 100.0% (672 of 672 strings)

Translated using Weblate (Bulgarian)

Currently translated at 57.4% (386 of 672 strings)

Translated using Weblate (Bulgarian)

Currently translated at 57.4% (386 of 672 strings)

Translated using Weblate (Polish)

Currently translated at 100.0% (672 of 672 strings)

Translated using Weblate (Gujarati)

Currently translated at 15.3% (103 of 672 strings)

Translated using Weblate (Hindi)

Currently translated at 81.6% (549 of 672 strings)

Translated using Weblate (Polish)

Currently translated at 100.0% (672 of 672 strings)

Added translation using Weblate (Gujarati)

Co-authored-by: Agnieszka C <aga_04@o2.pl>
Co-authored-by: AioiLight <info@aioilight.space>
Co-authored-by: Ajeje Brazorf <lmelonimamo@yahoo.it>
Co-authored-by: Allan Nordhøy <epost@anotheragency.no>
Co-authored-by: Andrij Mizyk <andmizyk@gmail.com>
Co-authored-by: AntonAkovP <anton.akov@gmail.com>
Co-authored-by: Anxhelo Lushka <anxhelo1995@gmail.com>
Co-authored-by: Ashune <ashune@protonmail.com>
Co-authored-by: Blaise Pascal <blaisepcl00@gmail.com>
Co-authored-by: ButterflyOfFire <ButterflyOfFire@protonmail.com>
Co-authored-by: Cerins <cerins4141@gmail.com>
Co-authored-by: Christian Draxl <draxl.koever@gmail.com>
Co-authored-by: Christian Eichert <c@zp1.net>
Co-authored-by: Danial Behzadi <dani.behzi@ubuntu.com>
Co-authored-by: Deleted User <noreply+34051@weblate.org>
Co-authored-by: Eduardo Caron <eduardocaron10@gmail.com>
Co-authored-by: Emin Tufan Çetin <etcetin@gmail.com>
Co-authored-by: Eric <spice2wolf@gmail.com>
Co-authored-by: Evo <weblate@verahawk.com>
Co-authored-by: Garden Hose <maxmammath@gmail.com>
Co-authored-by: Gediminas Murauskas <muziejusinfo@gmail.com>
Co-authored-by: GnuPGを使うべきだ <dieeeazpnnqbpddh@cock.email>
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Igor Nedoboy <i.nedoboy@mail.ru>
Co-authored-by: Ihor Hordiichuk <igor_ck@outlook.com>
Co-authored-by: Isak Holmström <isak@kajko.se>
Co-authored-by: J. Lavoie <j.lavoie@net-c.ca>
Co-authored-by: JY3 <GeeyunJY3@gmail.com>
Co-authored-by: Jeff Huang <s8321414@gmail.com>
Co-authored-by: Jesus Cass <cjesusenrique1@gmail.com>
Co-authored-by: Joel A <joeax910@student.liu.se>
Co-authored-by: Jonatan Nyberg <jonatan@autistici.org>
Co-authored-by: Kaantaja <ufdbvgoljrjkrkyyub@ianvvn.com>
Co-authored-by: Kristjan Räts <kristjanrats@gmail.com>
Co-authored-by: Laszlo Almasi <almalaci@posteo.net>
Co-authored-by: Ldm Public <ldmpub@gmail.com>
Co-authored-by: Martin Constantino–Bodin <martin.bodin@ens-lyon.org>
Co-authored-by: Matyas-Cerny <matyas.c.404@gmail.com>
Co-authored-by: MohammedSR Vevo <mohammednajmidin@gmail.com>
Co-authored-by: Nadir Nour <dudethatwascool2@gmail.com>
Co-authored-by: Nikita Epifanov <nikgreens@protonmail.com>
Co-authored-by: Ordtrogen Översättning <johan@ordtrogen.se>
Co-authored-by: Rahul Dev Sharma <sci94tune@gmail.com>
Co-authored-by: Rex_sa <rex.sa@pm.me>
Co-authored-by: Ricardo <contatorms7@tutamail.com>
Co-authored-by: S3aBreeze <paperwork@evilcorp.ltd>
Co-authored-by: Saravanan Selvaraju <saravanan036@outlook.com>
Co-authored-by: Sergio Varela <sergitroll9@gmail.com>
Co-authored-by: SomeRetardedThatTranslatesStuff <the.eumitosis@simplelogin.fr>
Co-authored-by: Thiago Carmona Monteiro <Guarakami1807@protonmail.ch>
Co-authored-by: TiA4f8R <avdivers84@gmail.com>
Co-authored-by: TobiGr <tobigr@mail.de>
Co-authored-by: ToldYouThat <itoldyouthat@protonmail.com>
Co-authored-by: Vasilis K <skyhirules@gmail.com>
Co-authored-by: VfBFan <drop0815@posteo.de>
Co-authored-by: WB <web0nst@tuta.io>
Co-authored-by: WaldiS <sto@tutanota.de>
Co-authored-by: Yaron Shahrabani <sh.yaron@gmail.com>
Co-authored-by: bomzhellino <adm.bomzh@gmail.com>
Co-authored-by: brokenPipe <ythunar@btcminers.tk>
Co-authored-by: bruh <quangtrung02hn16@gmail.com>
Co-authored-by: chr56 <chr0056@gmail.com>
Co-authored-by: michaloM <michalsvoboda2004@gmail.com>
Co-authored-by: nautilusx <translate@disroot.org>
Co-authored-by: nzgha <nzghafoss.ldxwe@slmail.me>
Co-authored-by: nzgha <osmshrn21.upogs@slmail.me>
Co-authored-by: pjammo <adrianoghr@hotmail.it>
Co-authored-by: random r <epsilin@yopmail.com>
Co-authored-by: ssantos <ssantos@web.de>
Co-authored-by: thami simo <simo.azad@gmail.com>
Co-authored-by: translator <yasinoc375@advew.com>
Co-authored-by: zeritti <woodenmo@posteo.de>
Co-authored-by: zmni <zmni@outlook.com>
Co-authored-by: Ács Zoltán <acszoltan111@gmail.com>
Co-authored-by: Ákos Surányi <akosuranyi@tutanota.com>
Co-authored-by: Андрей Станков <astankov84@gmail.com>
Co-authored-by: мачко <martinpeev@tutanota.com>
Co-authored-by: 정주찬 <ju1801@outlook.com>
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/bg/
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/ckb/
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/es/
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/fi/
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/fr/
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/he/
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/pl/
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/pt/
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/pt_BR/
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/pt_PT/
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/sv/
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/uk/
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/zh_Hans/
Translation: NewPipe/Metadata
2021-08-22 19:55:52 +02:00
TobiGr
9dd2a82b7d Update extractor version 2021-08-10 12:20:08 +02:00
Stypox
a3440cc8ef Merge pull request #6814 from Stypox/channel-grid-span-count
Fix channel item span count for SubscriptionFragment
2021-08-05 14:25:39 +02:00
Tobi
c3349e18a5 Merge pull request #6847 from Stypox/play-queue-theme
Play queue theme
2021-08-04 22:30:17 +02:00
Stypox
a2297fb5b8 Fix play queue theme 2021-08-04 18:41:23 +02:00
Stypox
23a6973291 v0.21.9 (975) changelog 2021-08-04 11:51:29 +02:00
Stypox
340a84e583 Release 0.21.9 (975) 2021-08-04 10:38:59 +02:00
Stypox
4291877830 Merge branch 'master' into dev 2021-08-04 10:36:59 +02:00
Tobi
8f6d608a43 Merge pull request #6834 from TeamNewPipe/release_0.21.8
Hotfix release 0.21.8
2021-08-03 21:27:43 +02:00
Stypox
45dd98e639 v0.21.8 (974) changelog 2021-08-03 21:12:53 +02:00
Stypox
2ac265a6f5 Release 0.21.8 (974) 2021-08-03 21:00:57 +02:00
Stypox
e100806fd9 Update extractor version to 0.21.8 2021-08-03 20:51:14 +02:00
Mohammed Anas
c7f75bf7d1 Ignore paths unrelated to builds in CI (#6789) 2021-08-02 13:29:39 +00:00
Stypox
4bf5ddbfe9 Merge pull request #6792 from XiangRongLin/update_extractor
Update extractor, thus including throttling fixes
2021-08-01 20:11:21 +02:00
Stypox
a9623f8e6a Merge pull request #6550 from Douile/fix/clickthrough-feed-refresh
Disable feed click events while refresh overlay is shown
2021-08-01 13:11:24 +02:00
Stypox
bc74bb6bf6 Merge pull request #6633 from Isira-Seneviratne/Use_NotificationChannelCompat
Use NotificationChannelCompat.
2021-08-01 11:58:38 +02:00
Isira Seneviratne
d32450255c Use NotificationChannelCompat. 2021-08-01 14:59:30 +05:30
Robin
896aec5295 Merge pull request #6719 from TacoTheDank/core-lifecycle-bump
Update some AndroidX libraries
2021-08-01 11:24:33 +02:00
Stypox
d42a534fc3 Merge pull request #6741 from KalleStruik/comment-hearts
Show hearts in comments
2021-08-01 11:12:58 +02:00
XiangRongLin
398007ca90 Update extractor, thus including throttling fixes 2021-08-01 10:36:03 +02:00
Stypox
551e8df8b8 Merge pull request #6773 from nschulzke/mark-as-played
Add ability to mark an item as played
2021-08-01 10:30:36 +02:00
Nathan Schulzke
dc0a28b93d Upsert the complete info if we fetch it for marking as watched 2021-07-31 09:50:41 -06:00
Stypox
644396149b Fix channel item span count for SubscriptionFragment 2021-07-31 11:02:57 +02:00
Stypox
a25bb2618a Merge pull request #6808 from litetex/ci-run-format-ktlin-before-building
Check formatting of kotlin files in CI
2021-07-31 10:35:53 +02:00
Nathan Schulzke
0e12cdea7c Save the fetched duration to the database so that it can render the view correctly. 2021-07-29 20:59:23 -06:00
litetex
903296014a Check formatting of kotlin files in CI 2021-07-28 21:03:51 +02:00
Tobi
cd713db029 Merge pull request #6778 from Stypox/invalid-storage-npe
Fix NullPointerException when checking if storage exists
2021-07-28 16:54:57 +02:00
Nathan Schulzke
bdd16e06e0 Add comments describing the purpose of the markAsWatched method 2021-07-28 08:25:39 -06:00
Nathan Schulzke
4c632810ec Fetch the stream info via a network request if no duration is found when attempting to mark as watched. 2021-07-27 15:21:56 -06:00
Nathan Schulzke
f451bdbfa4 Do not add Mark as Watched to a live stream. 2021-07-27 15:21:56 -06:00
Kalle Struik
bfac73b992 Make heart visible in android studio and move logic to the right file. 2021-07-27 22:34:59 +02:00
Nathan Schulzke
2b41f710a8 Change played to watched 2021-07-27 13:26:51 -06:00
Stypox
5924edb289 Merge pull request #6782 from TacoTheDank/fix-fill-parent
Fix deprecated fill_parent attributes
2021-07-27 19:45:51 +02:00
Stypox
5ceec31adf Merge pull request #6720 from TacoTheDank/alertdialog-edittext
Consolidate edittext alert dialogs into one common layout
2021-07-27 19:42:51 +02:00
TacoTheDank
e2791cdf0f Fix deprecated fill_parent attributes 2021-07-27 13:38:59 -04:00
TacoTheDank
50f3b08c59 Consolidate edittext alert dialogs into one layout 2021-07-27 13:31:58 -04:00
Stypox
2aebf6ceaf Add log when existsAsFile() is called on an invalid StoredFileHelper 2021-07-27 17:56:41 +02:00
Stypox
7ceea2cd8d Merge pull request #6771 from litetex/fix-ToolbarSearchInputTheme
Fixed the ToolbarSearchTheme
2021-07-27 11:49:55 +02:00
Stypox
0cb801179c Merge pull request #6733 from Douile/fix/recaptcha-webview-background-activity
Prevent recaptcha webview from keeping youtube loaded in background
2021-07-27 11:41:17 +02:00
Stypox
1822d21676 Fix NullPointerException when checking if storage exists 2021-07-27 11:36:14 +02:00
Nathan Schulzke
7fd2ebc252 Add ability to mark an item as played 2021-07-26 20:51:41 -06:00
litetex
f709ac16f8 Fixed the toolbarSearchTheme
The toolbarSearchTheme was accidently broken with https://github.com/TeamNewPipe/NewPipe/pull/6456, see https://github.com/TeamNewPipe/NewPipe/pull/6456#issuecomment-885920235 for details.
This commit restores the old behavior
2021-07-26 21:05:12 +02:00
Kalle Struik
74173317de Change heart color to be red, add else clause for non hearted comments, and apply some code style suggestions. 2021-07-23 19:43:25 +02:00
Kalle Struik
3874e16187 Added support for showing when a comment has received a heart from the creator of a video. 2021-07-23 17:30:47 +02:00
Tobi
39722a5563 Merge pull request #6721 from Stypox/pending-mission-crash
Delete pending missions with invalid storage
2021-07-22 16:22:58 +02:00
Robin
1f9ad12593 Merge pull request #6712 from Stypox/fix-duplicate-items-queue
Fix duplicate items in queue causing endless buffering
2021-07-22 13:26:01 +02:00
Tom
52c136439e Use loadUrl instead of loadData
Co-authored-by: Stypox <stypox@pm.me>
2021-07-22 10:47:47 +00:00
Douile
cd86ed3877 Prevent recaptcha webview from keeping youtube loaded in background
After the cookies are extracted from the recaptcha webview make it load an empty
page to prevent youtube being loaded unecessarily in the background.
2021-07-22 02:41:01 +01:00
TacoTheDank
1d85661ab9 Update some AndroidX libraries 2021-07-21 19:31:41 -04:00
Stypox
736cefed5a Add tests for play queue items' equals() 2021-07-21 18:22:17 +02:00
Stypox
fa8630ddae Use url comparison between queue items when disabling preloading
From #4562: Disable player stream preloading only if the current stream is going to be replaced for sure (see this). equals() was implemented for PlayQueueItems, so that (only) the url is compared when checking them.
2021-07-21 18:09:18 +02:00
Stypox
4a2bd7bd7b Remove equals() method from PlayQueueItem 2021-07-21 18:09:18 +02:00
Stypox
a9e21a35ea Delete pending missions with invalid storage 2021-07-21 10:52:04 +02:00
Tobi
fd4e1b8d2c Merge pull request #6715 from TeamNewPipe/readd_api_29
Readd api level 29 to android CI tests
2021-07-20 23:46:49 +02:00
TobiGr
420f0505ae Merge branch 'master' into dev 2021-07-20 23:29:12 +02:00
Tobi
b58f7856a1 Merge pull request #6716 from TeamNewPipe/release_0.21.7
Hotfix release 0.21.7
2021-07-20 20:05:10 +02:00
Stypox
44a6429267 v0.21.7 (973) changelog 2021-07-20 18:41:55 +02:00
Stypox
472bde9eea Release 0.21.7 (973) 2021-07-20 18:30:28 +02:00
XiangRongLin
c422f65935 Readd api level 29 to android CI tests
The action got fixed and released https://github.com/ReactiveCircus/android-emulator-runner/releases/tag/v2.19.1
2021-07-20 18:28:46 +02:00
Stypox
f5962375f8 Call DownloadDialog dismiss() in the correct way 2021-07-20 18:25:44 +02:00
Stypox
4e33f2dcb6 Improve method order in DownloadDialog and add separator comments 2021-07-20 18:25:30 +02:00
TacoTheDank
dce874bbc7 Fix onActivityResult deprecation in MissionsFragment 2021-07-20 18:25:05 +02:00
TacoTheDank
7d69dfa62a Fix onActivityResult deprecation in DownloadDialog 2021-07-20 18:24:55 +02:00
TacoTheDank
a56f17cc3b Fix onActivityResult deprecation in DownloadSettingsFragment 2021-07-20 18:24:43 +02:00
TacoTheDank
7be7a32d70 Update AndroidX Fragment to 1.3.5 2021-07-20 18:24:33 +02:00
Stypox
a7dd3af4e5 Fix grid span count calculation; remove duplicate methods 2021-07-20 18:20:44 +02:00
Tobi
63fdc100d6 Merge pull request #6705 from Stypox/big-text-info-items
Fix grid span count calculation
2021-07-19 22:45:48 +02:00
Tobi
9e2ece78dd Merge pull request #6701 from Stypox/dismiss-download-dialog
Dismiss download dialog correctly
2021-07-19 21:47:12 +02:00
Tobi
cebcaf4d6a Merge pull request #6706 from litetex/fix-format-of-some-kotlin-files
Fix format of some kotlin files
2021-07-19 21:20:00 +02:00
Stypox
4a242e43a7 Merge pull request #6689 from Isira-Seneviratne/Use_WindowInsetsCompat_getInsets
Use WindowInsetsCompat's getInsets() method.
2021-07-19 21:19:06 +02:00
Tobi
d8f442cc89 Merge pull request #6707 from litetex/use-correct-extractor-dependency
Use the correct extractor dependency
2021-07-19 21:17:27 +02:00
litetex
f6923e073e Use the correct extractor dependency 2021-07-19 21:03:15 +02:00
litetex
f02c6be10d Fix format of some kotlin files
so that it doesn't annoy people that are building this repo ;)
2021-07-19 20:59:29 +02:00
Stypox
5ba3ef0a25 Fix grid span count calculation; remove duplicate methods 2021-07-19 20:47:50 +02:00
Stypox
ca282f2be8 Merge pull request #6675 from Isira-Seneviratne/Use_Kotlin_methods
Use Kotlin methods in LicenseFragment.
2021-07-19 13:19:02 +02:00
Robin
0cde08c46e Merge pull request #6702 from Isira-Seneviratne/Update_AppCompat_to_1.3.0
Update AppCompat to 1.3.0.
2021-07-19 11:58:17 +02:00
Stypox
bec8512c7b Merge pull request #6659 from TeamNewPipe/Redirion-kotlin-section
Added a Kotlin section in CONTRIBUTING.md
2021-07-19 11:54:48 +02:00
Stypox
46e7da4e21 Merge pull request #6688 from litetex/fix-some-build-warnings
Fix some build warnings
2021-07-19 11:52:24 +02:00
Isira Seneviratne
c7b8bd3436 Update AppCompat to 1.3.0. 2021-07-19 15:20:44 +05:30
Isira Seneviratne
1721817fdb Use WindowInsetsCompat's getInsets() method. 2021-07-19 15:17:44 +05:30
Stypox
d57bfde604 Merge pull request #6434 from litetex/playerSeekbarPreview
Player seekbar thumbnail preview
2021-07-19 11:42:10 +02:00
Stypox
3167ab3ba0 Merge pull request #6654 from Isira-Seneviratne/Bump_compileSdk
Bump compileSdkVersion to 30.
2021-07-19 11:11:18 +02:00
Stypox
8f559965f6 Call DownloadDialog dismiss() in the correct way 2021-07-19 10:59:45 +02:00
Stypox
35e005caaa Improve method order in DownloadDialog and add separator comments 2021-07-18 14:23:38 +02:00
Stypox
6c25ce56a3 Merge pull request #6456 from TeamNewPipe/feature/switch-theme
Apply theme to switches
2021-07-18 13:12:47 +02:00
Stypox
baa12c7069 Merge pull request #6536 from TacoTheDank/moar-onactivityresult
More onActivityResult deprecation fixes
2021-07-18 10:24:00 +02:00
Isira Seneviratne
e2b044d2ee Use Kotlin methods in LicenseFragment. 2021-07-18 07:47:12 +05:30
litetex
621af8d812 Removed unused import (rebasing/merge problem) 2021-07-17 16:52:24 +02:00
litetex
efd038a536 Increased padding of preview thumbnail 2021-07-17 16:43:04 +02:00
litetex
0b2629e910 Moved time to the top 2021-07-17 16:43:03 +02:00
litetex
a9b5ef3bd3 Set minWidth to 10dp so that the popup player works (mostly) correctly 2021-07-17 16:43:03 +02:00
litetex
2a24532e1d Fine tuned padding
Moved seekbar preview up a bit, so the finger is not obstructing the view
2021-07-17 16:43:02 +02:00
litetex
88c4195260 Enlarged currentDisplaySeek-text on large-handed player 2021-07-17 16:43:01 +02:00
litetex
c5f2eb1dd8 Enlarged currentDisplaySeek a bit 2021-07-17 16:43:01 +02:00
litetex
384d964827 Added seekbarThumbnailPreview 2021-07-17 16:43:00 +02:00
litetex
253526e565 Updated build.gradle so the PR-build works 2021-07-17 16:42:18 +02:00
litetex
2e2dbaf77f Added seekbar-preview to the player layout 2021-07-17 16:41:54 +02:00
litetex
43133df2ad Added settings for seekbar-preview-thumbnail 2021-07-17 16:41:53 +02:00
Stypox
eef568b24c Merge pull request #5531 from XiangRongLin/tests
Add instrumented tests for LocalPlaylistManager.createPlaylist
2021-07-17 13:21:20 +02:00
Stypox
e7d5011f42 Merge pull request #6483 from litetex/addDisabledComments
Added comments disabled functionallity
2021-07-17 13:19:34 +02:00
litetex
36c198fc33 One textview is enough for disabled comments
Ref: https://github.com/TeamNewPipe/NewPipe/pull/6483#discussion_r654793920
2021-07-17 13:14:50 +02:00
litetex
75a8edf20f Added corresponding required code changes from Extractor branch 2021-07-17 13:14:48 +02:00
litetex
81107df53f Added comments disabled functionallity 2021-07-17 13:10:44 +02:00
Stypox
a932bc2503 Merge pull request #6637 from Isira-Seneviratne/Use_GestureDetectorCompat
Use GestureDetectorCompat.
2021-07-17 12:58:43 +02:00
litetex
f4e2eca256 Simplified code and adjusted the style so that it's similar to FeedFragment 2021-07-16 21:21:10 +02:00
litetex
08d5dfa49c Removed updateRelativeTimeViews when the activity is paused
We don't need to call ``updateRelativeTimeViews`` when the activity is paused, because the user likely won't  notice it.
Despite that onResume already calls ``updateRelativeTimeViews`` so there is no need to do that twice.
2021-07-16 21:04:32 +02:00
Tobi
e7f339a946 Merge pull request #6678 from TeamNewPipe/XiangRongLin-patch-1
Remove api level 29 from android ci tests
2021-07-16 11:30:23 +02:00
XiangRongLin
d3375a921d Remove api level 29 from android ci tests 2021-07-16 10:19:58 +02:00
Robin
a2eb810df0 removed Extractor line 2021-07-14 13:23:01 +02:00
Robin
6e576a165c Added a Kotlin section in CONTRIBUTING.md
Core team does not want to convert to Kotlin yet and sees Java as the easier to learn and more well adopted language.

This stance might of course change in the future. For example it could be reasonable to do a complete transition to Kotlin once it is decides that the minSdk is raised to 21 or higher, as we then could use Jetpack particularly Lifecycle and Compose.
2021-07-14 13:08:07 +02:00
Tobi
dfa941a9e7 Merge pull request #6503 from evermind-zz/fixes-for-upstream
Prevent error msg: 'Unrecoverable player error occurred' while playin…
2021-07-14 09:53:30 +02:00
Tobi
1584028995 Merge pull request #6531 from XiangRongLin/immediat_pref_commit
Remove option to immediately commit pref changes on import
2021-07-14 09:48:58 +02:00
Tobi
14dab85ff0 Merge pull request #6566 from evermind-zz/various-fixes-for-upstream
Convert PlayerHolder to Singleton; cleanup in VideoDetailFragment; Player/MainPlayer do not call onDestroy() directly
2021-07-14 09:46:04 +02:00
Isira Seneviratne
403e336a64 Bump compileSdkVersion to 30. 2021-07-13 08:06:56 +05:30
XiangRongLin
2aa5f68b7b Add comment explaining usage Schedulers.trampoline in detail 2021-07-12 18:31:37 +02:00
XiangRongLin
56ea526cce Add instrumented tests for LocalPlaylistManager.createPlaylist 2021-07-12 18:31:37 +02:00
Tobi
96f5cd9f17 Merge pull request #6463 from Stypox/metadata-tags
Improved metadata layout, better tags accessibility
2021-07-12 16:18:11 +02:00
Tobi
64efb89cce Merge pull request #6616 from litetex/fix-minimized-player-thumbnail
Made the thumbnail in the minimized player visible again
2021-07-12 16:17:12 +02:00
Tobi
4d5b68792b Merge pull request #6560 from TeamNewPipe/fix_ci_emulator
Specify emulator-build version in CI job
2021-07-12 16:16:06 +02:00
Tobi
85d813a94b Merge pull request #6540 from TacoTheDank/library-bumps
Update some libraries
2021-07-12 16:15:21 +02:00
Tobi
e9b008ee84 Merge pull request #6538 from TacoTheDank/bump-gradle
Bump gradle
2021-07-12 16:14:04 +02:00
Tobi
b795c5f017 Merge pull request #6576 from TeamNewPipe/release_0.21.6
Release 0.21.6
2021-07-12 15:56:21 +02:00
TobiGr
1e4686463b Release 0.21.6 (972) 2021-07-12 15:47:55 +02:00
TobiGr
4e9631a8d8 Update extractor version to 0.21.6 2021-07-12 15:47:55 +02:00
Hosted Weblate
3a83062670 Translated using Weblate (Polish)
Currently translated at 100.0% (672 of 672 strings)

Translated using Weblate (Polish)

Currently translated at 100.0% (672 of 672 strings)

Translated using Weblate (Polish)

Currently translated at 100.0% (672 of 672 strings)

Translated using Weblate (Polish)

Currently translated at 100.0% (672 of 672 strings)

Translated using Weblate (Polish)

Currently translated at 100.0% (672 of 672 strings)

Translated using Weblate (Polish)

Currently translated at 100.0% (672 of 672 strings)

Translated using Weblate (Polish)

Currently translated at 100.0% (672 of 672 strings)

Translated using Weblate (Polish)

Currently translated at 100.0% (672 of 672 strings)

Translated using Weblate (Polish)

Currently translated at 100.0% (672 of 672 strings)

Translated using Weblate (Polish)

Currently translated at 99.8% (671 of 672 strings)

Translated using Weblate (Polish)

Currently translated at 99.8% (671 of 672 strings)

Translated using Weblate (Bavarian)

Currently translated at 8.7% (59 of 672 strings)

Translated using Weblate (Bavarian)

Currently translated at 1.8% (1 of 53 strings)

Translated using Weblate (Polish)

Currently translated at 100.0% (672 of 672 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (672 of 672 strings)

Co-authored-by: Agnieszka C <aga_04@o2.pl>
Co-authored-by: Bert <robert@it-aicher.de>
Co-authored-by: Evo <weblate@verahawk.com>
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Ihor Hordiichuk <igor_ck@outlook.com>
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/bar/
Translation: NewPipe/Metadata
2021-07-12 15:47:11 +02:00
Douile
2e053ea25a Fix crash when refreshing feed 2021-07-11 03:00:32 +01:00
Hosted Weblate
fd3d46c813 Translated using Weblate (Polish)
Currently translated at 100.0% (672 of 672 strings)

Translated using Weblate (Somali)

Currently translated at 100.0% (672 of 672 strings)

Translated using Weblate (Polish)

Currently translated at 100.0% (672 of 672 strings)

Co-authored-by: Agnieszka C <aga_04@o2.pl>
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Nadir Nour <dudethatwascool2@gmail.com>
2021-07-10 17:02:34 +02:00
Tobi
ab838fd84f Merge pull request #6639 from TeamNewPipe/fix/db_transaction
Fix crash when refreshing feed after importing database or subscriptions
2021-07-10 17:00:42 +02:00
TobiGr
9ca2691a2c Add close() method to NewPipeDatabase 2021-07-10 14:46:51 +02:00
TobiGr
7c3f5a62c5 Fix crash when refreshing feed after importing database or subscriptions
The database was not closed correctly
2021-07-10 13:09:01 +02:00
Isira Seneviratne
6711dae4e0 Use GestureDetectorCompat. 2021-07-10 15:35:11 +05:30
Tobi
a73a4afcad Fix APK testing section (#6623)
* Fix APK testing section

Correct the instructions to download a test APK

* Update .github/PULL_REQUEST_TEMPLATE.md

Co-authored-by: Mohammed Anas <32234660+mhmdanas@users.noreply.github.com>

Co-authored-by: Mohammed Anas <32234660+mhmdanas@users.noreply.github.com>
2021-07-09 14:47:44 +00:00
TobiGr
4ea2d8e7ba Removed translations with incorrect number of arguments for formatting 2021-07-09 10:32:10 +02:00
TobiGr
bb386fea16 Remove unused translations of "downloads_storage_ask_summary_kitkat" 2021-07-09 10:25:54 +02:00
Hosted Weblate
82cdb0fdb3 Translated using Weblate (Ukrainian)
Currently translated at 100.0% (672 of 672 strings)

Translated using Weblate (Bavarian)

Currently translated at 6.6% (45 of 672 strings)

Translated using Weblate (Polish)

Currently translated at 100.0% (672 of 672 strings)

Added translation using Weblate (Bavarian)

Translated using Weblate (Somali)

Currently translated at 100.0% (672 of 672 strings)

Translated using Weblate (Polish)

Currently translated at 100.0% (672 of 672 strings)

Translated using Weblate (German)

Currently translated at 100.0% (672 of 672 strings)

Translated using Weblate (French)

Currently translated at 67.9% (36 of 53 strings)

Translated using Weblate (Polish)

Currently translated at 49.0% (26 of 53 strings)

Translated using Weblate (Ukrainian)

Currently translated at 52.8% (28 of 53 strings)

Translated using Weblate (Polish)

Currently translated at 100.0% (672 of 672 strings)

Translated using Weblate (Polish)

Currently translated at 100.0% (672 of 672 strings)

Translated using Weblate (Polish)

Currently translated at 100.0% (672 of 672 strings)

Translated using Weblate (Russian)

Currently translated at 100.0% (672 of 672 strings)

Translated using Weblate (Dutch)

Currently translated at 100.0% (672 of 672 strings)

Translated using Weblate (Ukrainian)

Currently translated at 18.8% (10 of 53 strings)

Co-authored-by: Agnieszka C <aga_04@o2.pl>
Co-authored-by: Bert <robert@it-aicher.de>
Co-authored-by: Evo <weblate@verahawk.com>
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Igor Nedoboy <i.nedoboy@mail.ru>
Co-authored-by: Ihor Hordiichuk <igor_ck@outlook.com>
Co-authored-by: Nadir Nour <dudethatwascool2@gmail.com>
Co-authored-by: Terry Louwers <t.louwers@gmail.com>
Co-authored-by: VfBFan <drop0815@posteo.de>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: translator <yasinoc375@advew.com>
Co-authored-by: webweblate <webweblate@riseup.net>
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/fr/
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/pl/
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/uk/
Translation: NewPipe/Metadata
2021-07-09 10:14:03 +02:00
Tobi
a94dacf03c Merge pull request #6622 from TeamNewPipe/fix/RepeatModeChange
Fix crash when using repeatButtonin tablet layout
2021-07-08 17:45:22 +02:00
TobiGr
de312eb768 Fix ClassCastException
See https://github.com/TeamNewPipe/NewPipe/issues/6577#issuecomment-876095378

java.lang.ClassCastException: android.widget.ImageButton cannot be cast to androidx.appcompat.widget.AppCompatImageButton
	at org.schabi.newpipe.player.Player.onRepeatModeChanged(Player.java:2263)
	at com.google.android.exoplayer2.ExoPlayerImpl.lambda$setRepeatMode$2(ExoPlayerImpl.java:564)
	at com.google.android.exoplayer2.-$$Lambda$ExoPlayerImpl$rgrcbaqP9Y8LgzdByBnAfUO4ydU.invokeListener(lambda)
	at com.google.android.exoplayer2.BasePlayer$ListenerHolder.invoke(BasePlayer.java:279)
	at com.google.android.exoplayer2.ExoPlayerImpl.invokeAll(ExoPlayerImpl.java:1498)
	at com.google.android.exoplayer2.ExoPlayerImpl.lambda$notifyListeners$6(ExoPlayerImpl.java:1318)
	at com.google.android.exoplayer2.-$$Lambda$ExoPlayerImpl$b59raXxaB-trjwE5bgpZInm1QnU.run(lambda)
	at com.google.android.exoplayer2.ExoPlayerImpl.notifyListeners(ExoPlayerImpl.java:1328)
	at com.google.android.exoplayer2.ExoPlayerImpl.notifyListeners(ExoPlayerImpl.java:1318)
	at com.google.android.exoplayer2.ExoPlayerImpl.setRepeatMode(ExoPlayerImpl.java:564)
	at com.google.android.exoplayer2.SimpleExoPlayer.setRepeatMode(SimpleExoPlayer.java:1636)
	at org.schabi.newpipe.player.Player.setRepeatMode(Player.java:2253)
	at org.schabi.newpipe.player.Player.onRepeatClicked(Player.java:2232)
	at org.schabi.newpipe.player.Player.onBroadcastReceived(Player.java:1123)
	at org.schabi.newpipe.player.Player.access$200(Player.java:190)
	at org.schabi.newpipe.player.Player$3.onReceive(Player.java:1060)
	at android.app.LoadedApk$ReceiverDispatcher$Args.run(LoadedApk.java:1185)
	... 7 more
2021-07-08 10:34:32 +02:00
Stypox
29aa1de4e3 Merge pull request #6617 from litetex/improve-wording-in-bug-report
Fixed a typo / make the wording consistent in the bug_report.md file
2021-07-07 19:12:38 +02:00
litetex
09435a1b63 Update bug_report.md
Fixed a typo / made the wording consistent
2021-07-06 21:45:15 +02:00
litetex
85e864a01e Made the thumbnail in the minimized player visible again 2021-07-06 21:40:57 +02:00
TacoTheDank
573839c0ff Update Gradle to 7.x, AGP to 4.2.x 2021-07-06 12:16:20 -04:00
XiangRongLin
9c636f5ee2 Specify emulator-build version in CI job
This is a workaround for the emulator bug https://github.com/ReactiveCircus/android-emulator-runner/issues/160
2021-07-06 16:26:01 +02:00
evermind
f78d2a5ed8 Prevent error msg: 'Unrecoverable player error occurred' while playing video during rotation (#6502)
Playing a video in VideoDetailFragment and rotating the screen to landscape (back and forth more often)
can trigger this error message. Especially if rotation for whatever reason takes long or
playing a high resolution (1080p) video.

The underlying logcat error messages:
05-12 16:38:38.251 24920 26037 E Surface : getSlotFromBufferLocked: unknown buffer: 0x923fc810
05-12 16:38:38.251 24920 26037 W ACodec  : [OMX.qcom.video.decoder.avc] can not return buffer 35 to native window

The problem is that that Exoplayer is trying to write to our -- during rotation -- no longer existant
(VideoDetailFragment) SurfaceView.

Solution:
Implementing SurfaceHolder.Callback and using DummySurface we can now handle the lifecycle of the Surface.

How?: In case we are no longer able to write to the Surface eg. through rotation/putting in
background we can set a DummySurface. Although it only works on API >= 23.
Result: we get a little video interruption (audio is still fine) but we won't get the
'Unrecoverable player error occurred' error message.

This implementation is based on and more background information:
 'ExoPlayer stuck in buffering after re-adding the surface view a few time 2703'

 -> exoplayer fix suggestion link
  https://github.com/google/ExoPlayer/issues/2703#issuecomment-300599981
2021-07-06 12:49:56 +02:00
evermind
48c2c156cb convert PlayerHolder to Singleton, handle context within, bugfix ServiceConnection leak
- bugfix: have ServiceConnection created only once!

- select the context within the PlayerHolder to start, stop, bind or unbind the service
  -> we have to make sure the Service is started AND stopped within the same context
  -> so let PlayerHolder be the one to select the context

- remove removeListener() and replace the call with setListener(null)
- Compatibility: use ContextCompat.startForegroundService instead of startService()
2021-07-06 12:31:26 +02:00
evermind
435813355f use viewBinding correctly 2021-07-06 07:56:05 +02:00
evermind
e30a552b6c remove duplicated code for toggle Fullscreen 2021-07-06 07:56:00 +02:00
evermind
22a4a4b2df move null checks for player and playerService to helper methods
- code is easier to read
- duplication of code reduced
2021-07-06 07:55:52 +02:00
opusforlife2
5ac418aa61 Merge pull request #6593 from mhmdanas/add-question-template
Add question template
2021-07-06 04:45:39 +00:00
Mohammed Anas
d8a0a74d47 Allow asking multiple questions at once 2021-07-05 13:12:54 +00:00
Tobi
3931c0d200 Merge pull request #6545 from chr56/fix_zh
[Localization]Fix Simplified Chinese Plural
2021-07-04 20:30:36 +02:00
Hosted Weblate
e26607fbd1 Translated using Weblate (Bengali)
Currently translated at 22.6% (12 of 53 strings)

Translated using Weblate (Bengali)

Currently translated at 92.4% (621 of 672 strings)

Translated using Weblate (Basque)

Currently translated at 100.0% (672 of 672 strings)

Translated using Weblate (Basque)

Currently translated at 100.0% (672 of 672 strings)

Translated using Weblate (Basque)

Currently translated at 99.2% (667 of 672 strings)

Translated using Weblate (Basque)

Currently translated at 99.2% (667 of 672 strings)

Translated using Weblate (Basque)

Currently translated at 99.1% (666 of 672 strings)

Translated using Weblate (Hungarian)

Currently translated at 85.8% (577 of 672 strings)

Translated using Weblate (Serbian)

Currently translated at 20.7% (11 of 53 strings)

Translated using Weblate (Ukrainian)

Currently translated at 7.5% (4 of 53 strings)

Translated using Weblate (Galician)

Currently translated at 90.6% (609 of 672 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (672 of 672 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (672 of 672 strings)

Translated using Weblate (Esperanto)

Currently translated at 82.7% (556 of 672 strings)

Translated using Weblate (Basque)

Currently translated at 98.3% (661 of 672 strings)

Translated using Weblate (Basque)

Currently translated at 98.3% (661 of 672 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (672 of 672 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (672 of 672 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (53 of 53 strings)

Translated using Weblate (Polish)

Currently translated at 49.0% (26 of 53 strings)

Translated using Weblate (Lithuanian)

Currently translated at 5.6% (3 of 53 strings)

Translated using Weblate (Sardinian)

Currently translated at 100.0% (672 of 672 strings)

Translated using Weblate (Vietnamese)

Currently translated at 100.0% (672 of 672 strings)

Translated using Weblate (Lithuanian)

Currently translated at 100.0% (672 of 672 strings)

Translated using Weblate (Lithuanian)

Currently translated at 100.0% (672 of 672 strings)

Translated using Weblate (Hebrew)

Currently translated at 100.0% (672 of 672 strings)

Translated using Weblate (Chinese (Traditional))

Currently translated at 100.0% (672 of 672 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (672 of 672 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (672 of 672 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (672 of 672 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (672 of 672 strings)

Translated using Weblate (Polish)

Currently translated at 100.0% (672 of 672 strings)

Translated using Weblate (Polish)

Currently translated at 100.0% (672 of 672 strings)

Translated using Weblate (Turkish)

Currently translated at 100.0% (672 of 672 strings)

Translated using Weblate (Turkish)

Currently translated at 100.0% (672 of 672 strings)

Translated using Weblate (Ukrainian)

Currently translated at 99.8% (671 of 672 strings)

Translated using Weblate (Indonesian)

Currently translated at 100.0% (672 of 672 strings)

Translated using Weblate (Arabic)

Currently translated at 100.0% (672 of 672 strings)

Translated using Weblate (Czech)

Currently translated at 100.0% (672 of 672 strings)

Translated using Weblate (Slovak)

Currently translated at 99.4% (668 of 672 strings)

Translated using Weblate (Greek)

Currently translated at 100.0% (672 of 672 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (672 of 672 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (672 of 672 strings)

Translated using Weblate (Japanese)

Currently translated at 99.7% (670 of 672 strings)

Translated using Weblate (Russian)

Currently translated at 100.0% (672 of 672 strings)

Translated using Weblate (Serbian)

Currently translated at 100.0% (672 of 672 strings)

Translated using Weblate (French)

Currently translated at 100.0% (672 of 672 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (672 of 672 strings)

Translated using Weblate (German)

Currently translated at 100.0% (672 of 672 strings)

Translated using Weblate (English)

Currently translated at 99.8% (671 of 672 strings)

Co-authored-by: Agnieszka C <aga_04@o2.pl>
Co-authored-by: Andrij Mizyk <andmizyk@gmail.com>
Co-authored-by: David Braz <davidbrazps2@gmail.com>
Co-authored-by: Emin Tufan Çetin <etcetin@gmail.com>
Co-authored-by: Eric <spice2wolf@gmail.com>
Co-authored-by: GM <muziejusinfo@gmail.com>
Co-authored-by: Gontzal Manuel Pujana Onaindia <thadahdenyse@gmail.com>
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Igor Nedoboy <i.nedoboy@mail.ru>
Co-authored-by: Ihor Hordiichuk <igor_ck@outlook.com>
Co-authored-by: JY3 <GeeyunJY3@gmail.com>
Co-authored-by: Josu <bi000@protonmail.com>
Co-authored-by: Oymate <dhruboadittya96@gmail.com>
Co-authored-by: Web0nst <web0nst@tuta.io>
Co-authored-by: chr56 <chr0056@gmail.com>
Co-authored-by: pjammo <adrianoghr@hotmail.it>
Co-authored-by: tdayris-perso <tdayris@tutanota.de>
Co-authored-by: Ács Zoltán <acszoltan111@gmail.com>
Co-authored-by: Слободан Симић(Slobodan Simić) <slsimic@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/bn/
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/lt/
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/pl/
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/sr/
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/uk/
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/zh_Hans/
Translation: NewPipe/Metadata
2021-07-04 17:53:05 +02:00
TobiGr
a63683e6b8 Update extractor version 2021-07-04 17:46:07 +02:00
Tobi
83b198f6fe Merge pull request #6570 from Redirion/fixnpe
Fix NPE when connecting via BT
2021-07-03 11:18:02 +02:00
mhmdanas
23f6e1084b Add question template 2021-07-02 00:31:20 +03:00
TobiGr
99335bab7a Add changelog for 0.21.6 (972) 2021-06-26 17:34:09 +02:00
Hosted Weblate
33fbc889fb Translated using Weblate (Polish)
Currently translated at 48.0% (25 of 52 strings)

Translated using Weblate (Interlingua)

Currently translated at 40.9% (275 of 672 strings)

Translated using Weblate (Telugu)

Currently translated at 20.9% (141 of 672 strings)

Translated using Weblate (Russian)

Currently translated at 100.0% (672 of 672 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (672 of 672 strings)

Translated using Weblate (Serbian)

Currently translated at 19.2% (10 of 52 strings)

Translated using Weblate (Portuguese)

Currently translated at 51.9% (27 of 52 strings)

Translated using Weblate (Estonian)

Currently translated at 96.1% (646 of 672 strings)

Translated using Weblate (Vietnamese)

Currently translated at 100.0% (672 of 672 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (672 of 672 strings)

Translated using Weblate (Turkish)

Currently translated at 100.0% (672 of 672 strings)

Translated using Weblate (Ukrainian)

Currently translated at 99.8% (671 of 672 strings)

Translated using Weblate (Esperanto)

Currently translated at 82.2% (553 of 672 strings)

Translated using Weblate (Portuguese)

Currently translated at 97.0% (652 of 672 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (672 of 672 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (672 of 672 strings)

Translated using Weblate (French)

Currently translated at 100.0% (672 of 672 strings)

Translated using Weblate (Ukrainian)

Currently translated at 98.8% (664 of 672 strings)

Translated using Weblate (Hebrew)

Currently translated at 48.0% (25 of 52 strings)

Translated using Weblate (Bengali)

Currently translated at 21.1% (11 of 52 strings)

Translated using Weblate (Sardinian)

Currently translated at 100.0% (672 of 672 strings)

Translated using Weblate (Hebrew)

Currently translated at 100.0% (672 of 672 strings)

Translated using Weblate (Chinese (Traditional))

Currently translated at 100.0% (672 of 672 strings)

Translated using Weblate (Indonesian)

Currently translated at 100.0% (672 of 672 strings)

Translated using Weblate (Arabic)

Currently translated at 100.0% (672 of 672 strings)

Translated using Weblate (Czech)

Currently translated at 100.0% (672 of 672 strings)

Translated using Weblate (Portuguese)

Currently translated at 95.3% (641 of 672 strings)

Translated using Weblate (Russian)

Currently translated at 100.0% (672 of 672 strings)

Translated using Weblate (Serbian)

Currently translated at 100.0% (672 of 672 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (52 of 52 strings)

Translated using Weblate (Indonesian)

Currently translated at 100.0% (52 of 52 strings)

Translated using Weblate (German)

Currently translated at 57.6% (30 of 52 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (672 of 672 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (672 of 672 strings)

Translated using Weblate (Polish)

Currently translated at 100.0% (672 of 672 strings)

Translated using Weblate (Indonesian)

Currently translated at 100.0% (672 of 672 strings)

Translated using Weblate (Greek)

Currently translated at 99.8% (671 of 672 strings)

Translated using Weblate (Japanese)

Currently translated at 99.7% (670 of 672 strings)

Translated using Weblate (French)

Currently translated at 99.5% (669 of 672 strings)

Translated using Weblate (German)

Currently translated at 100.0% (672 of 672 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (52 of 52 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 99.4% (668 of 672 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 99.4% (668 of 672 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 98.9% (665 of 672 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 98.9% (665 of 672 strings)

Co-authored-by: AioiLight <info@aioilight.space>
Co-authored-by: Ajeje Brazorf <lmelonimamo@yahoo.it>
Co-authored-by: Andrij Mizyk <andmizyk@gmail.com>
Co-authored-by: Babul Hossain <babulssf@gmail.com>
Co-authored-by: Emin Tufan Çetin <etcetin@gmail.com>
Co-authored-by: Eric <spice2wolf@gmail.com>
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Igor Nedoboy <i.nedoboy@mail.ru>
Co-authored-by: J. Lavoie <j.lavoie@net-c.ca>
Co-authored-by: JY3 <GeeyunJY3@gmail.com>
Co-authored-by: Jeff Huang <s8321414@gmail.com>
Co-authored-by: Karl Tammik <karltammik@protonmail.com>
Co-authored-by: Ldm Public <ldmpub@gmail.com>
Co-authored-by: MS-PC <MSPCtranslator@gmail.com>
Co-authored-by: Michal L <michalrmsmi@wp.pl>
Co-authored-by: Rama Devi <nramadevini@gmail.com>
Co-authored-by: Rex_sa <rex.sa@pm.me>
Co-authored-by: Software In Interlingua <softinterlingua@gmail.com>
Co-authored-by: Vasilis K <skyhirules@gmail.com>
Co-authored-by: Volk VDolgu <volkvdolg@gmail.com>
Co-authored-by: WaldiS <sto@tutanota.de>
Co-authored-by: Yaron Shahrabani <sh.yaron@gmail.com>
Co-authored-by: bruh <quangtrung02hn16@gmail.com>
Co-authored-by: chr56 <chr0056@gmail.com>
Co-authored-by: nautilusx <translate@disroot.org>
Co-authored-by: pjammo <adrianoghr@hotmail.it>
Co-authored-by: ssantos <ssantos@web.de>
Co-authored-by: tdayris-perso <tdayris@tutanota.de>
Co-authored-by: whenwesober <naomi16i_1298q@cikuh.com>
Co-authored-by: zeritti <woodenmo@posteo.de>
Co-authored-by: zmni <zamani.karmana@gmail.com>
Co-authored-by: Слободан Симић(Slobodan Simić) <slsimic@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/bn/
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/de/
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/he/
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/id/
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/pl/
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/pt/
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/sr/
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/zh_Hans/
Translation: NewPipe/Metadata
2021-06-26 15:57:58 +02:00
Robin
201e5ee09d Fix NPE when connecting via BT 2021-06-25 09:14:15 +02:00
bopol
c398308872 Merge pull request #6539 from TeamNewPipe/screenshots_for_UI_changes
Ask for screenshots/video of UI changes in PR template
2021-06-24 17:25:15 +02:00
opusforlife2
090c063644 Address Bopol's review
Changed the location, added option to delete, and also fixed a tiny grammar thingy.
2021-06-24 15:15:12 +00:00
opusforlife2
ec40c8ed1e Merge pull request #6482 from Stypox/build-instructions
Update Build instructions to make it easier for contributors to start contributing.
2021-06-24 15:02:59 +00:00
TiA4f8R
78a99526a9 Merge pull request #6557 from B0pol/invidious-instances
Update Invidious instances and support Piped links
2021-06-24 12:24:11 +02:00
Stypox
25914b0263 Third round of review 2021-06-24 10:46:53 +02:00
evermind
aaa3e20c5a service.onDestroy() should only be called from the system and not manually
instead use service.stopService() which inturn calls stopSelf() and
triggers hopefully onDestroy() to be called. Eventually we have to make
sure that all ServiceConnections are closed to successfully stop the service
now!

Cleanup within stopService() and not only onDestroy()

So we make sure that all listeners can react to onServiceStopped()
and close their ServiceConnections. Afterwards the android framework
is ready to stop the Service.
2021-06-24 10:15:07 +02:00
bopol
0da8e28651 Merge pull request #6558 from Douile/fix/searchfragment-null-service
Fix null pointer exception when displaying SearchFragment
2021-06-24 09:16:00 +02:00
bopol
d7dcfa5729 Update Extractor version 2021-06-24 09:00:15 +02:00
bopol
65824ff64d Update Invidious instances 2021-06-24 08:59:02 +02:00
bopol
63cad7ebb0 Merge pull request #6562 from TeamNewPipe/disable_instrumented_ci
Comment out test-android CI job
2021-06-24 08:55:12 +02:00
XiangRongLin
b996fa7eef Add comments explaining decisions in test-android ci job 2021-06-23 20:16:23 +02:00
XiangRongLin
5ebf3726ed Comment out test-android CI job 2021-06-23 20:14:27 +02:00
TiA4f8R
484c852efd Merge pull request #6556 from B0pol/kotlin-style
Refactoring by Android Studio
2021-06-23 18:39:53 +02:00
bopol
25cf8dc20a Refactoring by Android Studio 2021-06-23 14:30:01 +02:00
Douile
cb1a138140 #6081: Disable feed click handlers during refresh
This patch changes click handlers for feed (Whats new) so that they do
nothing while the feed is refreshing and the items being clicked are not
visible.
2021-06-22 19:42:20 +01:00
Douile
384ca66205 #6522: Fix null pointer exception when displaying SearchFragment
It seems due to #6394 updating the FragmentX library there was a
change to the order of lifecycle calls, as such onResume() was no longer
before onCreateOptionsMenu() creating a null pointer exception when
using service in onCreateOptionsMenu() as it is only set in onResume().

By moving the initialization of service to onStart() which still happens
before onCreateOptionsMenu() this crash can be avoided. This commit also
adds a check for a null service to prevent future crashes for similar
issues.
2021-06-22 16:52:02 +01:00
chr_56
46bfec66cb Localization: Fix Simplified Chinese Plural
one -> other
2021-06-22 18:52:28 +08:00
TacoTheDank
afe06b379f Update some libraries 2021-06-20 17:26:59 -04:00
opusforlife2
a9e85abd7f Ask for screenshots/video of UI changes
If the PR author has changed the UI of the app, they should include screenshots or a video so that developers and users alike can pinpoint exactly what changed.
2021-06-20 21:15:25 +00:00
TacoTheDank
08d4651ef0 Add mavenCentral, de-prioritize jcenter 2021-06-20 15:44:17 -04:00
Mohammed Anas
62b4f333bb Don't enable SAF on Fire TV (#6516) 2021-06-20 19:01:06 +00:00
TacoTheDank
02b0909829 Fix onActivityResult deprecation in MissionsFragment 2021-06-20 14:14:54 -04:00
TacoTheDank
ae39b31c68 Fix onActivityResult deprecation in DownloadDialog 2021-06-20 14:14:44 -04:00
TacoTheDank
e5a1438673 Fix onActivityResult deprecation in DownloadSettingsFragment 2021-06-20 14:11:00 -04:00
TacoTheDank
72d305b283 Update AndroidX Fragment to 1.3.5 2021-06-20 13:47:12 -04:00
XiangRongLin
785c0376f8 Remove variable ContentSettingsFragment.lastImportExportDataUri
Instead pass the value through the methods as parameter
2021-06-20 09:30:59 +02:00
XiangRongLin
0bdf8de38e Resolve sonar issues in ContentSettingsFragment
https://sonarcloud.io/organizations/teamnewpipe/rules?open=java%3AS2885&rule_key=java%3AS2885

https://sonarcloud.io/organizations/teamnewpipe/rules?open=java%3AS112&rule_key=java%3AS112
2021-06-20 09:30:59 +02:00
bopol
6c575511be Merge pull request #6528 from TacoTheDank/annotate-methods
Annotate some overridden methods and parameters as NonNull
2021-06-20 09:26:05 +02:00
XiangRongLin
9767e98e50 Remove option to immediately commit pref changes on import
System is now not restarted with `System.exit(0)`.
Instead it is done properly by finishing the activity and restarting the activity. This allows preference changes which are queued up asynchronously through `apply` to be applied.
2021-06-20 09:17:55 +02:00
TacoTheDank
79deff3261 Annotate some overridden methods and parameters as NonNull 2021-06-19 18:37:02 -04:00
Stypox
74ad488f4a Merge pull request #6394 from TacoTheDank/androidx-fragment-134
Update AndroidX Fragment to 1.3.4
2021-06-18 13:14:30 +02:00
Stypox
0db3406ad8 Fix SonarLint references and explanation 2021-06-18 12:26:50 +02:00
TobiGr
6e377dd3c5 Translated using Weblate (Serbian)
Currently translated at 100.0% (667 of 667 strings)

Translated using Weblate (Kurdish)

Currently translated at 1.9% (1 of 51 strings)

Translated using Weblate (Sardinian)

Currently translated at 100.0% (667 of 667 strings)

Translated using Weblate (Kurdish (Central))

Currently translated at 87.8% (586 of 667 strings)

Translated using Weblate (Estonian)

Currently translated at 92.2% (615 of 667 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (667 of 667 strings)

Translated using Weblate (Polish)

Currently translated at 100.0% (667 of 667 strings)

Translated using Weblate (Turkish)

Currently translated at 100.0% (667 of 667 strings)

Translated using Weblate (Japanese)

Currently translated at 99.8% (666 of 667 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (667 of 667 strings)

Translated using Weblate (Punjabi)

Currently translated at 3.9% (2 of 51 strings)

Translated using Weblate (Greek)

Currently translated at 31.3% (16 of 51 strings)

Translated using Weblate (Bengali)

Currently translated at 89.6% (598 of 667 strings)

Translated using Weblate (Bengali (India))

Currently translated at 54.1% (361 of 667 strings)

Translated using Weblate (Interlingua)

Currently translated at 41.0% (274 of 667 strings)

Translated using Weblate (Punjabi)

Currently translated at 98.9% (660 of 667 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (667 of 667 strings)

Translated using Weblate (Czech)

Currently translated at 100.0% (667 of 667 strings)

Translated using Weblate (Slovak)

Currently translated at 100.0% (667 of 667 strings)

Translated using Weblate (Serbian)

Currently translated at 100.0% (667 of 667 strings)

Translated using Weblate (Greek)

Currently translated at 31.3% (16 of 51 strings)

Translated using Weblate (Indonesian)

Currently translated at 100.0% (667 of 667 strings)

Translated using Weblate (Greek)

Currently translated at 100.0% (667 of 667 strings)

Translated using Weblate (Greek)

Currently translated at 100.0% (667 of 667 strings)

Translated using Weblate (Serbian)

Currently translated at 100.0% (667 of 667 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (667 of 667 strings)

Translated using Weblate (German)

Currently translated at 100.0% (667 of 667 strings)

Translated using Weblate (Hebrew)

Currently translated at 47.0% (24 of 51 strings)

Translated using Weblate (Interlingua)

Currently translated at 39.8% (266 of 667 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (667 of 667 strings)

Translated using Weblate (German)

Currently translated at 100.0% (667 of 667 strings)

Translated using Weblate (Spanish)

Currently translated at 27.4% (14 of 51 strings)

Translated using Weblate (Hebrew)

Currently translated at 100.0% (667 of 667 strings)

Translated using Weblate (Chinese (Traditional))

Currently translated at 100.0% (667 of 667 strings)

Translated using Weblate (Polish)

Currently translated at 99.8% (666 of 667 strings)

Translated using Weblate (Ukrainian)

Currently translated at 99.4% (663 of 667 strings)

Translated using Weblate (Japanese)

Currently translated at 98.3% (656 of 667 strings)

Translated using Weblate (Spanish)

Currently translated at 99.2% (662 of 667 strings)

Translated using Weblate (German)

Currently translated at 100.0% (667 of 667 strings)

Translated using Weblate (Santali)

Currently translated at 14.2% (95 of 667 strings)

Translated using Weblate (Arabic)

Currently translated at 66.6% (34 of 51 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (667 of 667 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (667 of 667 strings)

Translated using Weblate (Polish)

Currently translated at 99.7% (665 of 667 strings)

Translated using Weblate (Arabic)

Currently translated at 100.0% (667 of 667 strings)

Translated using Weblate (Greek)

Currently translated at 99.8% (666 of 667 strings)

Translated using Weblate (Russian)

Currently translated at 100.0% (667 of 667 strings)

Translated using Weblate (French)

Currently translated at 98.9% (660 of 667 strings)

Translated using Weblate (German)

Currently translated at 100.0% (667 of 667 strings)

Translated using Weblate (German)

Currently translated at 100.0% (667 of 667 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (667 of 667 strings)

Co-authored-by: AioiLight <info@aioilight.space>
Co-authored-by: Ajeje Brazorf <lmelonimamo@yahoo.it>
Co-authored-by: Andrij Mizyk <andmizyk@gmail.com>
Co-authored-by: C. Rüdinger <Mail-an-CR@web.de>
Co-authored-by: David Braz <davidbrazps2@gmail.com>
Co-authored-by: Digiwizkid <subhadiplayek@gmail.com>
Co-authored-by: Emin Tufan Çetin <etcetin@gmail.com>
Co-authored-by: Eric <spice2wolf@gmail.com>
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: JY3 <GeeyunJY3@gmail.com>
Co-authored-by: Jeff Huang <s8321414@gmail.com>
Co-authored-by: Karl Tammik <karltammik@protonmail.com>
Co-authored-by: Ldm Public <ldmpub@gmail.com>
Co-authored-by: Marian Hanzel <marulinko@gmail.com>
Co-authored-by: Michal L <michalrmsmi@wp.pl>
Co-authored-by: MohammedSR Vevo <mohammednajmidin@gmail.com>
Co-authored-by: Prasanta-Hembram <Prasantahembram720@gmail.com>
Co-authored-by: Rex_sa <rex.sa@pm.me>
Co-authored-by: Santiago <abridgement_phasiron@aleeas.com>
Co-authored-by: Software In Interlingua <softinterlingua@gmail.com>
Co-authored-by: SomeRetardedThatTranslatesStuff <the.eumitosis@simplelogin.fr>
Co-authored-by: THANOS SIOURDAKIS <siourdakisthanos@gmail.com>
Co-authored-by: Tari <tumbleweed@tuta.io>
Co-authored-by: TobiGr <tobigr@mail.de>
Co-authored-by: Vasilis K <skyhirules@gmail.com>
Co-authored-by: VfBFan <drop0815@posteo.de>
Co-authored-by: WaldiS <sto@tutanota.de>
Co-authored-by: Yaron Shahrabani <sh.yaron@gmail.com>
Co-authored-by: Yngvar Skjaldulfsson <irrorate_browns@slmail.me>
Co-authored-by: bomzhellino <adm.bomzh@gmail.com>
Co-authored-by: chr56 <chr0056@gmail.com>
Co-authored-by: gnu-ewm <gnu.ewm@protonmail.com>
Co-authored-by: nautilusx <translate@disroot.org>
Co-authored-by: pjammo <adrianoghr@hotmail.it>
Co-authored-by: thami simo <simo.azad@gmail.com>
Co-authored-by: zeritti <woodenmo@posteo.de>
Co-authored-by: zmni <zmni@outlook.com>
Co-authored-by: Обилић <mudo2233@tutanota.com>
Co-authored-by: Слободан Симић(Slobodan Simić) <slsimic@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/ar/
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/el/
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/es/
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/he/
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/ku/
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/pa/
Translation: NewPipe/Metadata

Translated using Weblate (English)

Currently translated at 100.0% (667 of 667 strings)

Translated using Weblate (German)

Currently translated at 100.0% (667 of 667 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (667 of 667 strings)

Translated using Weblate (French)

Currently translated at 98.9% (660 of 667 strings)

Translated using Weblate (Serbian)

Currently translated at 100.0% (667 of 667 strings)

Translated using Weblate (Russian)

Currently translated at 100.0% (667 of 667 strings)

Translated using Weblate (Japanese)

Currently translated at 99.8% (666 of 667 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (667 of 667 strings)

Translated using Weblate (Greek)

Currently translated at 100.0% (667 of 667 strings)

Translated using Weblate (Slovak)

Currently translated at 100.0% (667 of 667 strings)

Translated using Weblate (Czech)

Currently translated at 100.0% (667 of 667 strings)

Translated using Weblate (Arabic)

Currently translated at 100.0% (667 of 667 strings)

Translated using Weblate (Indonesian)

Currently translated at 100.0% (667 of 667 strings)

Translated using Weblate (Ukrainian)

Currently translated at 99.4% (663 of 667 strings)

Translated using Weblate (Turkish)

Currently translated at 100.0% (667 of 667 strings)

Translated using Weblate (Polish)

Currently translated at 100.0% (667 of 667 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (667 of 667 strings)

Translated using Weblate (Chinese (Traditional))

Currently translated at 100.0% (667 of 667 strings)

Translated using Weblate (Hebrew)

Currently translated at 100.0% (667 of 667 strings)

Translated using Weblate (Sardinian)

Currently translated at 100.0% (667 of 667 strings)

Translated using Weblate (Bengali)

Currently translated at 93.1% (621 of 667 strings)
2021-06-18 09:33:18 +02:00
Tobi
be676ad93c Merge pull request #3371 from mauriciocolli/feed-hide-played-items
Add ability to hide played items in a feed
2021-06-18 09:18:48 +02:00
Robin
0803d9f2b5 Merge pull request #6504 from evermind-zz/fixes-choice-dialog
dismiss choice dialog in onStop() to avoid a leaked window Exception:
2021-06-17 21:17:35 +02:00
Stypox
841fb4cfc5 Merge pull request #6495 from danielmbutler/Replace-System.exit-calls
Replace the System.exit calls with getActivity.finishAffinity()
2021-06-17 21:07:48 +02:00
Stypox
8b3e32b6eb First round of review 2021-06-17 20:47:22 +02:00
danielmbutler
90de75968d Replaced System.Exit calls with FinishAffinity and Start Activity calls
Implemented "RestartApp" method defined in NavigationHelper.java.
 This method is used in ExitActivity.java and ContentSettingsFragment.java
2021-06-17 17:18:15 +01:00
TobiGr
2de9d7b4a7 Merge branch 'master' into dev 2021-06-17 08:20:43 +02:00
evermind
a9ab2f54ea dismiss choice dialog in onStop() to avoid a leaked window Exception:
E/WindowManager: android.view.WindowLeaked: Activity org.schabi.newpipe.RouterActivity has leaked window DecorView@d99fe3b[] that was originally added here
        at android.view.ViewRootImpl.<init>(ViewRootImpl.java:418)
2021-06-17 06:53:47 +02:00
TacoTheDank
a1432e939f Fix onActivityResult deprecation in SubscriptionFragment 2021-06-15 22:09:19 -04:00
TacoTheDank
cae160b5be Fix onActivityResult deprecation in SubscriptionsImportFragment 2021-06-15 22:09:07 -04:00
TacoTheDank
aa4e5da146 Fix onActivityResult deprecation in ContentSettingsFragment 2021-06-15 22:08:55 -04:00
TacoTheDank
1061fca6a3 Add super.onRequestPermissionsResult where missing 2021-06-15 22:08:41 -04:00
TacoTheDank
e4885e3c52 Fix some older deprecations from previous Fragment versions 2021-06-15 22:08:25 -04:00
Tobi
a98c0bdec7 Merge pull request #6493 from TeamNewPipe/hotfix-v0.21.5
Hotfix release 0.21.5
2021-06-15 23:58:52 +02:00
Stypox
d6e0bd8c26 Merge pull request #6059 from Dakkaron/manual-tablet-mode-setting
Adds manual tablet mode setting
2021-06-15 21:46:19 +02:00
Dakkaron
e01ef42d31 Adds manual tablet mode setting
Co-authored-by: TiA4f8R <74829229+TiA4f8R@users.noreply.github.com>
2021-06-15 21:43:06 +02:00
Stypox
92910eb227 Merge pull request #5523 from TiA4f8R/share-improvements
Recognize timestamps and hashtags in descriptions and do some sharing fixes and improvements
2021-06-15 20:56:37 +02:00
TobiGr
cdfe686322 v0.21.5 (971) changelog 2021-06-15 19:14:42 +02:00
bopol
553943ab93 Release NewPipe 0.21.5 (971) 2021-06-15 19:12:24 +02:00
Stypox
32df4d39a4 Reshow feed if grid/list view mode changed 2021-06-15 18:40:25 +02:00
litetex
1281ea858c Added itemsListHeaderDuration to large-hand player 2021-06-15 18:00:48 +02:00
Robin
30a303f873 Increase buffer for playback after rebuffer 2021-06-15 18:00:19 +02:00
Stypox
fdb6679d2d Make list stream item ConstraintLayouts and use chain 2021-06-15 14:17:40 +02:00
Stypox
7145b117cc Fix long press menu in feed 2021-06-14 19:02:57 +02:00
Stypox
4698d07323 Do not hide feed buttons (show/hide & help) behind three-dots menu 2021-06-14 19:02:57 +02:00
Stypox
2142f05a88 Fix hiding finished streams in groups; new stream state validity condition
Consider stream state valid also if >1/4 of video was watched
2021-06-14 19:01:01 +02:00
Stypox
40a2df847b Move tags layout at the bottom, use multiple lines 2021-06-13 21:56:06 +02:00
Stypox
6063ff063b CONTRIBUTING.md: add "Build instructions" section and improve ordering 2021-06-13 21:06:59 +02:00
Tobi
547a1a9970 Merge pull request #6480 from litetex/fixMissingitemsListHeaderDuration
Added itemsListHeaderDuration to large-hand player
2021-06-12 13:36:08 +02:00
litetex
8c52a812d9 Added itemsListHeaderDuration to large-hand player 2021-06-11 22:00:15 +02:00
TiA4f8R
4eef498d24 Only call addClickListenersOnTimestamps if relatedInfo is instance of StreamInfo 2021-06-11 14:08:08 +02:00
TiA4f8R
32b0bdb98c Fix the compile error 2021-06-11 12:19:29 +02:00
Stypox
edfe0f9c30 Fix disposables handling for text linkifier
also use differently Markwon methods to convert plain text to markdown
2021-06-11 12:12:12 +02:00
Stypox
eef418a757 Improve text linkifier function parameters 2021-06-11 12:12:11 +02:00
TiA4f8R
218f25c171 Annotate params and methods with NonNull 2021-06-11 12:12:10 +02:00
TiA4f8R
f02df6d80c Fix an error and add a new method in the ShareUtils class
Fix the error due to the rebase on the dev branch of this branch
Add a shareText method in the ShareUtils class which has 3 parameters and calls
the original shareText method with an empty string for the
imagePreviewUrl param.
2021-06-11 12:12:09 +02:00
TiA4f8R
da4d379b22 Initial work: use disposables for timestamps parsing in YouTube video descriptions and YouTube comments 2021-06-11 12:12:08 +02:00
TiA4f8R
f13f4cc5d2 Split handleUrl method into two methods
Split handleURL method, now private, into two methods:
handleUrlCommentsTimestamp and handleUrlDescriptionTimestamp. Code is
now more proper.
2021-06-11 12:12:07 +02:00
TiA4f8R
a79badd783 Adress requested changes and try some cleanup in handleUrl method of InternalUrlsHandler class 2021-06-11 12:12:06 +02:00
TiA4f8R
2702700d10 Don't use a chooser for other intents than opening a content in a browser or sharing a content to other apps
Use an ACTION_CHOOSER intent has a negative impact for user experience, because user cannot set as default an activity for an intent
2021-06-11 12:12:05 +02:00
TiA4f8R
267686fd37 Initial work: add support for opening hashtags in plain text descriptions
This commit adds supports for opening hashtags in plain text descriptions, using the same logic as timestamps.
Every hashtag opens a search on the current service with the text in the hashtag.
Also use a better regular expression for parsing timestamps.
2021-06-11 12:12:04 +02:00
TiA4f8R
e5df2f65b8 Move some classes to a new subpackage and adress requested changes
Rename URLHandler and KoreUtil classes to InternalUrlsHandler and KoreUtils.
Move InternalUrlsHandler, KoreUtils, TextLinkfier, ShareUtils classes to external_communication subpackage.
Remove unused param showPreviewText in shareText method of ShareUtils class.
Add initial work to be able to display an image preview of the content shared (not for downloads).
Use a better regular expression to parse timestamps in plain text descriptions.
2021-06-11 12:12:03 +02:00
TiA4f8R
d6decc05d7 Move some classes to a new subpackage and adress requested changes
Rename URLHandler and KoreUtil classes to InternalUrlsHandler and KoreUtils.
Move InternalUrlsHandler, KoreUtils, TextLinkfier, ShareUtils classes to external_communication subpackage.
Remove unused param showPreviewText in shareText method of ShareUtils class.
Add initial work to be able to display an image preview of the content shared (not for downloads).
Use a better regular expression to parse timestamps in plain text descriptions.
2021-06-11 12:12:02 +02:00
TiA4f8R
d85afd6435 Initial work to add the image of the content in the share sheet
Also do some fixes when sharing a file in downloads and some improvements in JavaDocs of ShareUtils class.
2021-06-11 12:12:00 +02:00
TiA4f8R
2fb86364ab Fix title of the subject when sharing an URL 2021-06-11 12:11:59 +02:00
TiA4f8R
c972940338 Apply suggested changes and fix some warnings 2021-06-11 12:11:58 +02:00
TiA4f8R
6abdd2a6d8 Try to change message of the system chooser for the update notification
This commit tries to change the title of the system chooser shown, which is from Android System ("Open links with"), when no defaut browser is present, for the update notification.
2021-06-11 12:11:57 +02:00
TiA4f8R
9e9d1a04e4 Fix toast shown when falling back to Google Play Store URL and the action of Open with Kodi button in the player
Add a boolean param, showToast, in ShareUtils.openIntentInApp and only show toast "No app on your device can open this" if this boolean is true.
Fix the action of play with Kodi button by applying the fix provided in #5599 (adding the flag Intent.FLAG_ACTIVITY_NEW_TASK to the intent in NavigationHelper.playWithKore method).
Do also some cleanup in viewWithFileProvider and shareFile methods of MissionAdapter class.
2021-06-11 12:11:56 +02:00
TiA4f8R
ae9349e36c Initial work: add support for opening timestamps in plain text descriptions
This commit adds support for opening plain text timestamps by parsing the description text using a regular expression, add a click listener for each timestamp which opens the popup player at the indicated time in the timestamp.
In order to do this, playOnPopup method of the URLHandler class. Also, handleUrl method of this class has been renamed to canHandleUrl.
2021-06-11 12:11:55 +02:00
TiA4f8R
4031777606 Open recognized timestamps in the description of contents in the popup player
This commit adds support of opening recognized timestamps in the popup
player instead of starting an intent which opens the YouTube website with
the video timestamp.
2021-06-11 12:11:44 +02:00
TacoTheDank
9591f14551 Update AndroidX Fragment to 1.3.4 2021-06-10 12:51:30 -04:00
Stypox
06d10cf9aa Merge pull request #6313 from Isira-Seneviratne/Update_Room_to_2.3.0
Update Room to 2.3.0.
2021-06-09 16:28:55 +02:00
Stypox
0113ad5e14 Correctly save stream progress at the end of a video 2021-06-09 15:53:51 +02:00
Stypox
e58feadba9 Remove IN HISTORY label on stream info items 2021-06-09 15:53:51 +02:00
Stypox
360f5ac6f7 Save playback state even if stream is finished and add isFinished() 2021-06-09 15:53:51 +02:00
Mauricio Colli
e846f69e38 Add ability to hide played items in a feed
- Use components from the new Groupie list library for displaying the
feed list.
2021-06-09 15:53:51 +02:00
Stypox
fa1d7ffac3 Const text width for metadata; scrollable tags layout 2021-06-09 15:32:07 +02:00
Stypox
272d589518 Convert related_items_header to ConstraintLayout 2021-06-09 13:10:26 +02:00
Stypox
6ab4787e97 Use SwitchCompat to make switch uniform across versions
Also just use colorControlActivated in the base V19 theme, instead of using the prefix android: in each V21 service theme
2021-06-09 13:04:21 +02:00
TobiGr
060f09ff55 Apply service color to switches 2021-06-08 20:11:05 +02:00
TobiGr
f47ae3668f [Bandcamp Add v21 styles 2021-06-08 20:11:05 +02:00
Tobi
56cd84c1fe Merge pull request #6444 from TeamNewPipe/increasebufferafterdepletion
Increase buffer for playback after rebuffer
2021-06-08 17:02:18 +02:00
Stypox
a2eead521f Merge pull request #6346 from Imericxu/tabs-style-check
Resolve Tabs style checks
2021-06-08 14:49:14 +02:00
Stypox
a2fd5ae20c Update app/src/main/java/org/schabi/newpipe/settings/tabs/Tab.java 2021-06-08 14:45:00 +02:00
Stypox
543440e38d Update app/src/main/java/org/schabi/newpipe/settings/tabs/Tab.java 2021-06-08 14:44:54 +02:00
Stypox
0b64382ef6 Merge pull request #6436 from Abanoub8/add_url_button
Add copy url function to share button
2021-06-08 14:37:10 +02:00
Stypox
bede758507 Use url at current time for long click on player share button 2021-06-08 14:34:51 +02:00
Abanoub Sameh
5532666ad5 Long press on player share button to copy url to clipboard 2021-06-08 14:23:33 +02:00
Stypox
63cff25616 Merge pull request #6314 from Peyman-hme/Add-Open-website-button-to-webview-in-license-fragment-issue-#6296
Add "Open website" button to WebView in license fragment
2021-06-08 11:37:06 +02:00
Stypox
5e2735aaa2 Merge pull request #5462 from Isira-Seneviratne/Convert_abstract_classes_to_interfaces
Convert the abstract class DAOs to interfaces.
2021-06-08 11:32:10 +02:00
Peyman-hme
6fc0d8fce4 Add Open website string resources 2021-06-08 11:29:25 +02:00
Peyman-hme
e0c1ca1209 Change "Ok" Button to "Dismiss" and also change "Open Website" button to neutral button 2021-06-08 11:29:00 +02:00
Peyman-hme
3dc4ed1764 Add "Open website" button in webview in license fragment, issue #6296 2021-06-08 11:29:00 +02:00
Tobi
f63a4ee2ae Merge pull request #5876 from TeamNewPipe/terminated-channels
Better error handling of terminated channels
2021-06-08 10:57:24 +02:00
Tobi
c96bdfcb32 Merge pull request #5415 from Stypox/saf
Support SAF properly
2021-06-08 10:55:38 +02:00
Stypox
2a99e0e435 Reimplement storing backup import/export path
#6319 and #6402 were reverted before adding SAF changes, and have been readded at the end of SAF changes
2021-06-08 10:41:24 +02:00
Stypox
5ffc667bea Remove misplaced comment 2021-06-08 10:40:45 +02:00
Stypox
21b8df0375 Check if file really exists before asking user to overwrite it 2021-06-08 10:40:45 +02:00
Stypox
b78ac7d2e9 Fix strange behaviour when app loses access to saf download folder 2021-06-08 10:40:45 +02:00
Stypox
114dc8ffa0 Pass mime type so that SAF treats file extension correctly 2021-06-08 10:40:45 +02:00
Stypox
eea43d5a73 Add comments to SharpStreams 2021-06-08 10:40:45 +02:00
Stypox
bcb1cf6603 Improve SAF switch descriptions in settings 2021-06-08 10:40:45 +02:00
Stypox
6a0c5a874c Fix ContentSettingsManager tests 2021-06-08 10:40:45 +02:00
Stypox
1e8b3826dc Add setting migration to promote using SAF 2021-06-08 10:40:45 +02:00
wb9688
7efe62ee80 Only ask for storage permissions when not using SAF 2021-06-08 10:40:45 +02:00
wb9688
febb21a01d Fix non-SAF actions 2021-06-08 10:40:44 +02:00
wb9688
cb4e6159c4 Use file picker for export DB 2021-06-08 10:40:44 +02:00
wb9688
1164ea52f9 Move Stored(File|Directory)Helper into NewPipe 2021-06-08 10:40:44 +02:00
wb9688
0f75024e03 Support SAF properly 2021-06-08 10:40:44 +02:00
Stypox
1e09a1768e Revert all commits related to ContentSettingsFragment
Revert "Annotate methode parameters as NonNull"
This reverts commit 004907d306.

Revert "Commit path immediately when import backup"
This reverts commit 05eb0d0fbe.

Revert "Set ImportExportDataPath only on successful import"
This reverts commit f13a1b04e6.

Revert "Set ImportExportDataPath only on successful export"
This reverts commit fd4408e572.

Revert "Invert if condition in ContentSettingsFragment.setImportExportDataPath for better readability"
This reverts commit 92ab9cae27.

Revert "Move ContentSettingsFragment.isValidPath to helpers and add unit test for it."
This reverts commit fa2b11b768.

Revert "Save backup import/export location for feature import/exports"
This reverts commit 82f43ac6a6.

Remove FilePathHelperTest file
2021-06-08 10:40:44 +02:00
Hosted Weblate
7c78d963d9 Translated using Weblate (Indonesian)
Currently translated at 100.0% (51 of 51 strings)

Translated using Weblate (Turkish)

Currently translated at 100.0% (657 of 657 strings)

Translated using Weblate (Russian)

Currently translated at 100.0% (657 of 657 strings)

Co-authored-by: Emin Tufan Çetin <etcetin@gmail.com>
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: bomzhellino <adm.bomzh@gmail.com>
Co-authored-by: whenwesober <naomi16i_1298q@cikuh.com>
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/id/
Translation: NewPipe/Metadata
2021-06-08 10:33:15 +02:00
TobiGr
b57ecae565 Fix getting error cause
Fix error dialog not shown when getting elemets from first subscription failed.
2021-06-08 10:12:36 +02:00
Stypox
89317d4abc Fix feed loading and show a dialog for each invalid subscription 2021-06-08 10:12:36 +02:00
TobiGr
c5dd3dc7a9 Improve error message when loading feed
Add name of unavailable channel
2021-06-08 10:12:36 +02:00
TobiGr
ccc46971b4 Show detailed error message when an account has been terminated by the service 2021-06-08 10:12:36 +02:00
TobiGr
6ad4b425e4 Better error handling of terminated channels when loading feed 2021-06-08 10:12:36 +02:00
Tobi
761e01c3b9 Merge pull request #6443 from Stypox/invalid-attr
Use constraint layout for play queue item
2021-06-08 10:05:38 +02:00
Robin
3b0045917c Increase buffer for playback after rebuffer 2021-06-07 09:25:49 +02:00
Stypox
a102fc9cad Use constraint layout for play queue item
Also remove invalid ic_selected attribute
2021-06-07 08:07:27 +02:00
Eric Xu
448989f32f Add PlaylistTab.hashCode() matching equals 2021-05-26 12:01:30 -04:00
Eric Xu
2fc26bc154 Refactor PlaylistTab.equals 2021-05-26 12:01:30 -04:00
Eric Xu
1812249d37 Add ChannelTab.hashCode() matching equals 2021-05-26 12:01:30 -04:00
Eric Xu
14bbaccb9f Refactor ChannelTab.equals 2021-05-26 12:01:30 -04:00
Eric Xu
d2b03afcf4 Add KioskTab.hashCode() matching equals 2021-05-26 12:01:30 -04:00
Eric Xu
1cac3895dc Refactor KioskTab.equals 2021-05-26 12:01:30 -04:00
Eric Xu
01aab25889 Add Tab.hashCode() to go with equals 2021-05-26 12:01:30 -04:00
Eric Xu
96d731dfc7 Refactor equals method 2021-05-26 12:01:30 -04:00
Isira Seneviratne
3b1c4b043d Convert Converters to a Kotlin object. 2021-05-16 11:14:15 +05:30
Isira Seneviratne
e8b8391868 Rename .java to .kt 2021-05-16 11:14:15 +05:30
Isira Seneviratne
cd0a87785e Update Room to 2.3.0. 2021-05-16 11:04:52 +05:30
Isira Seneviratne
a3c4a10721 Convert the abstract class DAOs to interfaces. 2021-01-20 06:29:50 +05:30
339 changed files with 9433 additions and 4660 deletions

View File

@@ -3,9 +3,9 @@ NewPipe contribution guidelines
## Crash reporting
Report crashes through the automated crash report system of NewPipe.
Report crashes through the **automated crash report system** of NewPipe.
This way all the data needed for debugging is included in your bugreport for GitHub.
You'll see exactly what is sent, be able to add your comments, and then send it.
You'll see *exactly* what is sent, be able to add **your comments**, and then send it.
## Issue reporting/feature requests
@@ -25,22 +25,61 @@ You'll see exactly what is sent, be able to add your comments, and then send it.
## Code contribution
* If you want to help out with an existing bug report or feature request, leave a comment on that issue saying you want to try your hand at it.
* If there is no existing issue for what you want to work on, open a new one describing your changes. This gives the team and the community a chance to give feedback before you spend time on something that is already in development, should be done differently, or should be avoided completely.
* Stick to NewPipe's style conventions of [checkStyle](https://github.com/checkstyle/checkstyle). It runs each time you build the project.
* Do not bring non-free software (e.g. binary blobs) into the project. Make sure you do not introduce Google
libraries.
### Guidelines
* Stick to NewPipe's *style conventions* of [checkStyle](https://github.com/checkstyle/checkstyle) and [ktlint](https://github.com/pinterest/ktlint). They run each time you build the project.
* Stick to [F-Droid contribution guidelines](https://f-droid.org/wiki/page/Inclusion_Policy).
* Make changes on a separate branch with a meaningful name, not on the _master_ branch or the _dev_ branch. This is commonly known as *feature branch workflow*. You may then send your changes as a pull request (PR) on GitHub.
* Please test (compile and run) your code before submitting changes! Ideally, provide test feedback in the PR description. Untested code will **not** be merged!
* 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 must rebase the dev branch manually and resolve the problems on your own. You can find help [on the wiki](https://github.com/TeamNewPipe/NewPipe/wiki/How-to-merge-a-PR). That makes the maintainers' jobs way easier.
* Please show intention to maintain your features and code after you contribute a PR. Unmaintained code is a hassle for core developers. If you do not intend to maintain features you plan to contribute, please rethink your submission, or clearly state that in the PR description.
* In particular **do not bring non-free software** (e.g. binary blobs) into the project. Make sure you do not introduce any closed-source library from Google.
### Before starting development
* If you want to help out with an existing bug report or feature request, **leave a comment** on that issue saying you want to try your hand at it.
* If there is no existing issue for what you want to work on, **open a new one** describing the changes you are planning to introduce. This gives the team and the community a chance to give **feedback** before you spend time on something that is already in development, should be done differently, or should be avoided completely.
* Please show **intention to maintain your features** and code after you contribute a PR. Unmaintained code is a hassle for core developers. If you do not intend to maintain features you plan to contribute, please rethink your submission, or clearly state that in the PR description.
* Create PRs that cover only **one specific issue/solution/bug**. Do not create PRs that are huge monoliths and could have been split into multiple independent contributions.
* NewPipe uses [NewPipeExtractor](https://github.com/TeamNewPipe/NewPipeExtractor) to fetch data from services. If you need to change something there, you must test your changes in NewPipe. Telling NewPipe to use your extractor version can be accomplished by editing the `app/build.gradle` file: the comments under the "NewPipe libraries" section of `dependencies` will help you out.
### Kotlin in NewPipe
* NewPipe will remain mostly Java for time being
* Contributions containing a simple conversion from Java to Kotlin should be avoided. Conversions to Kotlin should only be done if Kotlin actually brings improvements like bug fixes or better performance which are not, or only with much more effort, implementable in Java. The core team sees Java as an easier to learn and generally well adopted programming language.
### Creating a Pull Request (PR)
* Make changes on a **separate branch** with a meaningful name, not on the _master_ branch or the _dev_ branch. This is commonly known as *feature branch workflow*. You may then send your changes as a pull request (PR) on GitHub.
* Please **test** (compile and run) your code before submitting changes! Ideally, provide test feedback in the PR description. Untested code will **not** be merged!
* Respond if someone requests changes or otherwise raises issues about your PRs.
* Send PRs that only cover one specific issue/solution/bug. Do not send PRs that are huge and consist of multiple independent solutions.
* 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 must *rebase* your branch on the `dev` branch manually and resolve the conflicts on your own. You can find help [on the wiki](https://github.com/TeamNewPipe/NewPipe/wiki/How-to-merge-a-PR). Doing this makes the maintainers' job way easier.
## IDE setup & building the app
### Basic setup
NewPipe is developed using [Android Studio](https://developer.android.com/studio/). Learn more about how to install it and how it works in the [official documentation](https://developer.android.com/studio/intro). In particular, make sure you have accepted Android Studio's SDK licences. Once Android Studio is ready, setting up the NewPipe project is fairly simple:
- Clone the NewPipe repository with `git clone https://github.com/TeamNewPipe/NewPipe.git` (or use the link from your own fork, if you want to open a PR).
- Open the folder you just cloned with Android Studio.
- Build and run it just like you would do with any other app, with the green triangle in the top bar.
You may find [SonarLint](https://www.sonarlint.org/intellij)'s **inspections** useful in helping you to write good code and prevent bugs.
### checkStyle setup
The [checkStyle](https://github.com/checkstyle/checkstyle) plugin verifies that Java code abides by the project style. It runs automatically each time you build the project. If you want to view errors directly in the editor, instead of having to skim through the build output, you can install an Android Studio plugin:
- Go to `File -> Settings -> Plugins`, search for `checkstyle` and install `CheckStyle-IDEA`.
- Go to `File -> Settings -> Tools -> Checkstyle`.
- Add NewPipe's configuration file by clicking the `+` in the right toolbar of the "Configuration File" list.
- Under the "Use a local Checkstyle file" bullet, click on `Browse` and pick the file named `checkstyle.xml` in the project's root folder.
- Enable "Store relative to project location" so that moving the directory around does not create issues.
- Insert a description in the top bar, then click `Next` and then `Finish`.
- Activate the configuration file you just added by enabling the checkbox on the left.
- Click `Ok` and you are done.
### ktlint setup
The [ktlint](https://github.com/pinterest/ktlint) plugin does the same job as checkStyle for Kotlin files. Installing the related plugin is as simple as going to `File -> Settings -> Plugins`, searching for `ktlint` and installing `Ktlint (unofficial)`.
## Communication
* The #newpipe channel on Libera Chat (`ircs://irc.libera.chat:6697/newpipe`) has the core team and other developers in it. [Click here for webchat](https://web.libera.chat/#newpipe)!
* You can also use a Matrix account to join the NewPipe channel at [#newpipe:libera.chat](https://matrix.to/#/#newpipe:libera.chat).
* Post suggestions, changes, ideas etc. on GitHub or IRC.
* You can also use a Matrix account to join the NewPipe channel at [#newpipe:libera.chat](https://matrix.to/#/#newpipe:libera.chat). Some convenient clients, available both for phone and desktop, are listed at that link.
* You can post your suggestions, changes, ideas etc. on either GitHub or IRC.

View File

@@ -33,7 +33,7 @@ Oh no, a bug! It happens. Thanks for reporting an issue with NewPipe. To make it
### Actual behaviour
### Actual behavior
<!-- Tell us what happens with the steps given above. -->

24
.github/ISSUE_TEMPLATE/question.md vendored Normal file
View File

@@ -0,0 +1,24 @@
---
name: Question
about: Ask about anything NewPipe-related
labels: question
assignees: ''
---
<!-- IF YOU DON'T FILL IN THE TEMPLATE PROPERLY, YOUR ISSUE IS LIABLE TO BE CLOSED. If you feel tired/lazy right now, open your issue some other time. We'll wait. -->
<!-- The comments between these brackets won't show up in the submitted issue (as you can see in the Preview). -->
### Checklist
<!-- This checklist is COMPULSORY. The first box has been checked for you to show you how it is done. -->
- [x] I checked, but didn't find any duplicates (open OR closed) of this issue in the repo. <!-- Seriously, check. O_O (If there's already an issue but you'd like to see if something changed, just make a comment on the issue instead of opening a new one.) -->
- [ ] I have read the contribution guidelines given at https://github.com/TeamNewPipe/NewPipe/blob/HEAD/.github/CONTRIBUTING.md.
#### What's your question(s)?
#### Additional context
<!-- Add any other context, like screenshots or links, about the question here.
Example: *Here's a photo of my cat!* -->

View File

@@ -12,18 +12,23 @@
- create clones
- take over the world
#### Before/After Screenshots/Screen Record
<!-- If your PR changes the app's UI in any way, please include screenshots or a video showing exactly what changed, so that developers and users can pinpoint it easily. Delete this if it doesn't apply to your PR.-->
- Before:
- After:
#### Fixes the following issue(s)
<!-- Prefix issues with "Fixes" so that GitHub closes them when the PR is merged (note that each "Fixes #" should be in its own item). Also add any other relevant links. -->
- Fixes #
#### Relies on the following changes
<!-- Delete this if it doesn't apply to you. -->
<!-- Delete this if it doesn't apply to your PR. -->
-
#### APK testing
<!-- Use a new, meaningfully named branch. The name is used as a suffix for the app ID to allow installing and testing multiple versions of NewPipe, e.g. "commentfix", if your PR implements a bugfix for comments. (No names like "patch-0" and "feature-1".) -->
<!-- Remove the following line if you directly link the APK created by the CI pipeline. Directly linking is preferred if you need to let users test.-->
On the website the APK can be found by going to the "Checks" tab below the title and then on "artifacts" on the right.
The APK can be found by going to the "Checks" tab below the title. On the left pane, click on "CI", scroll down to "artifacts" and click "app" to download the zip file which contains the debug APK of this PR.
#### Due diligence
- [ ] I read the [contribution guidelines](https://github.com/TeamNewPipe/NewPipe/blob/HEAD/.github/CONTRIBUTING.md).

View File

@@ -5,10 +5,20 @@ on:
branches:
- dev
- master
paths-ignore:
- 'README*.md'
- 'fastlane/**'
- 'assets/**'
- '.github/**/*.md'
push:
branches:
- dev
- master
paths-ignore:
- 'README*.md'
- 'fastlane/**'
- 'assets/**'
- '.github/**/*.md'
jobs:
build-and-test-jvm:
@@ -34,6 +44,9 @@ jobs:
path: ~/.gradle/caches
key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle') }}
restore-keys: ${{ runner.os }}-gradle
- name: Check if kotlin files are formatted correctly
run: ./gradlew runKtlint
- name: Build debug APK and run jvm tests
run: ./gradlew assembleDebug lintDebug testDebugUnitTest --stacktrace
@@ -43,11 +56,14 @@ jobs:
with:
name: app
path: app/build/outputs/apk/debug/*.apk
test-android:
# macos has hardware acceleration. See android-emulator-runner action
runs-on: macos-latest
strategy:
matrix:
api-level: [21, 29]
# api-level 19 is min sdk, but throws errors related to desugaring
api-level: [ 21, 29 ]
steps:
- uses: actions/checkout@v2
@@ -68,7 +84,10 @@ jobs:
uses: reactivecircus/android-emulator-runner@v2
with:
api-level: ${{ matrix.api-level }}
# workaround to emulator bug: https://github.com/ReactiveCircus/android-emulator-runner/issues/160
emulator-build: 7425822
script: ./gradlew connectedCheck
# sonar:
# runs-on: ubuntu-latest
# steps:

View File

@@ -9,16 +9,16 @@ apply plugin: 'kotlin-kapt'
apply plugin: 'checkstyle'
android {
compileSdkVersion 29
buildToolsVersion '29.0.3'
compileSdkVersion 30
buildToolsVersion '30.0.3'
defaultConfig {
applicationId "org.schabi.newpipe"
resValue "string", "app_name", "NewPipe"
minSdkVersion 19
targetSdkVersion 29
versionCode 970
versionName "0.21.4"
versionCode 975
versionName "0.21.9"
multiDexEnabled true
@@ -101,17 +101,17 @@ android {
ext {
checkstyleVersion = '8.38'
androidxLifecycleVersion = '2.2.0'
androidxRoomVersion = '2.3.0-alpha03'
androidxLifecycleVersion = '2.3.1'
androidxRoomVersion = '2.3.0'
icepickVersion = '3.2.0'
exoPlayerVersion = '2.12.3'
googleAutoServiceVersion = '1.0-rc7'
googleAutoServiceVersion = '1.0'
groupieVersion = '2.8.1'
markwonVersion = '4.6.0'
markwonVersion = '4.6.2'
leakCanaryVersion = '2.5'
stethoVersion = '1.5.1'
stethoVersion = '1.6.0'
mockitoVersion = '3.6.0'
}
@@ -121,7 +121,7 @@ configurations {
}
checkstyle {
configDir rootProject.file(".")
getConfigDirectory().set(rootProject.file("."))
ignoreFailures false
showViolations true
toolVersion = checkstyleVersion
@@ -140,8 +140,8 @@ task runCheckstyle(type: Checkstyle) {
showViolations true
reports {
xml.enabled true
html.enabled true
xml.getRequired().set(true)
html.getRequired().set(true)
}
}
@@ -151,7 +151,7 @@ def inputFiles = project.fileTree(dir: "src", include: "**/*.kt")
task runKtlint(type: JavaExec) {
inputs.files(inputFiles)
outputs.dir(outputDir)
main = "com.pinterest.ktlint.Main"
getMainClass().set("com.pinterest.ktlint.Main")
classpath = configurations.ktlint
args "src/**/*.kt"
}
@@ -159,7 +159,7 @@ task runKtlint(type: JavaExec) {
task formatKtlint(type: JavaExec) {
inputs.files(inputFiles)
outputs.dir(outputDir)
main = "com.pinterest.ktlint.Main"
getMainClass().set("com.pinterest.ktlint.Main")
classpath = configurations.ktlint
args "-F", "src/**/*.kt"
}
@@ -182,8 +182,11 @@ dependencies {
/** NewPipe libraries **/
// You can use a local version by uncommenting a few lines in settings.gradle
// Or you can use a commit you pushed to GitHub by just replacing TeamNewPipe with your GitHub
// name and the commit hash with the commit hash of the (pushed) commit you want to test
// This works thanks to JitPack: https://jitpack.io/
implementation 'com.github.TeamNewPipe:nanojson:1d9e1aea9049fc9f85e68b43ba39fe7be1c1f751'
implementation 'com.github.TeamNewPipe:NewPipeExtractor:v0.21.4'
implementation 'com.github.TeamNewPipe:NewPipeExtractor:v0.21.9'
/** Checkstyle **/
checkstyle "com.puppycrawl.tools:checkstyle:${checkstyleVersion}"
@@ -193,15 +196,16 @@ dependencies {
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:${kotlin_version}"
/** AndroidX **/
implementation 'androidx.appcompat:appcompat:1.2.0'
implementation 'androidx.appcompat:appcompat:1.3.1'
implementation 'androidx.cardview:cardview:1.0.0'
implementation 'androidx.constraintlayout:constraintlayout:2.0.4'
implementation 'androidx.core:core-ktx:1.3.2'
implementation 'androidx.core:core-ktx:1.6.0'
implementation 'androidx.documentfile:documentfile:1.0.1'
implementation 'androidx.fragment:fragment-ktx:1.3.6'
implementation "androidx.lifecycle:lifecycle-livedata:${androidxLifecycleVersion}"
implementation "androidx.lifecycle:lifecycle-viewmodel:${androidxLifecycleVersion}"
implementation 'androidx.localbroadcastmanager:localbroadcastmanager:1.0.0'
implementation 'androidx.media:media:1.2.1'
implementation 'androidx.media:media:1.3.1'
implementation 'androidx.multidex:multidex:2.0.1'
implementation 'androidx.preference:preference:1.1.1'
implementation 'androidx.recyclerview:recyclerview:1.1.0'
@@ -233,8 +237,8 @@ dependencies {
kapt "com.google.auto.service:auto-service:${googleAutoServiceVersion}"
// Manager for complex RecyclerView layouts
implementation "com.xwray:groupie:${groupieVersion}"
implementation "com.xwray:groupie-viewbinding:${groupieVersion}"
implementation "com.github.lisawray.groupie:groupie:${groupieVersion}"
implementation "com.github.lisawray.groupie:groupie-viewbinding:${groupieVersion}"
// Circular ImageView
implementation "de.hdodenhof:circleimageview:3.1.0"

View File

@@ -0,0 +1,85 @@
package org.schabi.newpipe.local.playlist
import androidx.room.Room
import androidx.test.core.app.ApplicationProvider
import org.junit.After
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.junit.rules.Timeout
import org.schabi.newpipe.database.AppDatabase
import org.schabi.newpipe.database.stream.model.StreamEntity
import org.schabi.newpipe.extractor.stream.StreamType
import org.schabi.newpipe.testUtil.TrampolineSchedulerRule
import java.util.concurrent.TimeUnit
class LocalPlaylistManagerTest {
private lateinit var manager: LocalPlaylistManager
private lateinit var database: AppDatabase
@get:Rule
val trampolineScheduler = TrampolineSchedulerRule()
@get:Rule
val timeout = Timeout(10, TimeUnit.SECONDS)
@Before
fun setup() {
database = Room.inMemoryDatabaseBuilder(
ApplicationProvider.getApplicationContext(),
AppDatabase::class.java
)
.allowMainThreadQueries()
.build()
manager = LocalPlaylistManager(database)
}
@After
fun cleanUp() {
database.close()
}
@Test
fun createPlaylist() {
val stream = StreamEntity(
serviceId = 1, url = "https://newpipe.net/", title = "title",
streamType = StreamType.VIDEO_STREAM, duration = 1, uploader = "uploader"
)
val result = manager.createPlaylist("name", listOf(stream))
// This should not behave like this.
// Currently list of all stream ids is returned instead of playlist id
result.test().await().assertValue(listOf(1L))
}
@Test
fun createPlaylist_emptyPlaylistMustReturnEmpty() {
val result = manager.createPlaylist("name", emptyList())
// This should not behave like this.
// It should throw an error because currently the result is null
result.test().await().assertComplete()
manager.playlists.test().awaitCount(1).assertValue(emptyList())
}
@Test()
fun createPlaylist_nonExistentStreamsAreUpserted() {
val stream = StreamEntity(
serviceId = 1, url = "https://newpipe.net/", title = "title",
streamType = StreamType.VIDEO_STREAM, duration = 1, uploader = "uploader"
)
database.streamDAO().insert(stream)
val upserted = StreamEntity(
serviceId = 1, url = "https://newpipe.net/2", title = "title2",
streamType = StreamType.VIDEO_STREAM, duration = 1, uploader = "uploader"
)
val result = manager.createPlaylist("name", listOf(stream, upserted))
result.test().await().assertComplete()
database.streamDAO().all.test().awaitCount(1).assertValue(listOf(stream, upserted))
}
}

View File

@@ -0,0 +1,37 @@
package org.schabi.newpipe.testUtil
import io.reactivex.rxjava3.android.plugins.RxAndroidPlugins
import io.reactivex.rxjava3.plugins.RxJavaPlugins
import io.reactivex.rxjava3.schedulers.Schedulers
import org.junit.rules.TestRule
import org.junit.runner.Description
import org.junit.runners.model.Statement
/**
* Always run on [Schedulers.trampoline].
* This executes the task in the current thread in FIFO manner.
* This ensures that tasks are run quickly inside the tests
* and not scheduled away to another thread for later execution
*/
class TrampolineSchedulerRule : TestRule {
private val scheduler = Schedulers.trampoline()
override fun apply(base: Statement, description: Description): Statement =
object : Statement() {
override fun evaluate() {
try {
RxJavaPlugins.setComputationSchedulerHandler { scheduler }
RxJavaPlugins.setIoSchedulerHandler { scheduler }
RxJavaPlugins.setNewThreadSchedulerHandler { scheduler }
RxJavaPlugins.setSingleSchedulerHandler { scheduler }
RxAndroidPlugins.setInitMainThreadSchedulerHandler { scheduler }
base.evaluate()
} finally {
RxJavaPlugins.reset()
RxAndroidPlugins.reset()
}
}
}
}

View File

@@ -22,7 +22,6 @@
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:logo="@mipmap/ic_launcher"
android:requestLegacyExternalStorage="true"
android:theme="@style/OpeningTheme"
android:resizeableActivity="true"
tools:ignore="AllowBackup">
@@ -232,11 +231,10 @@
<data android:host="invidious.snopyta.org" />
<data android:host="yewtu.be" />
<data android:host="tube.connect.cafe" />
<data android:host="invidious.zapashcanon.fr" />
<data android:host="invidious.kavin.rocks" />
<data android:host="invidious.tube" />
<data android:host="invidious-us.kavin.rocks" />
<data android:host="piped.kavin.rocks" />
<data android:host="invidious.site" />
<data android:host="invidious.xyz" />
<data android:host="vid.mint.lgbt" />
<data android:host="invidiou.site" />
<data android:host="invidious.fdn.fr" />
@@ -244,6 +242,15 @@
<data android:host="invidious.zee.li" />
<data android:host="vid.puffyan.us" />
<data android:host="ytprivate.com" />
<data android:host="invidious.namazso.eu" />
<data android:host="invidious.silkky.cloud" />
<data android:host="invidious.exonip.de" />
<data android:host="inv.riverside.rocks" />
<data android:host="invidious.blamefran.net" />
<data android:host="invidious.moomoo.me" />
<data android:host="ytb.trom.tf" />
<data android:host="yt.cyberhost.uk" />
<data android:host="y.com.cm" />
<data android:pathPrefix="/" />
</intent-filter>

View File

@@ -63,8 +63,9 @@ public final class FlingBehavior extends AppBarLayout.Behavior {
return consumed == dy;
}
public boolean onInterceptTouchEvent(final CoordinatorLayout parent, final AppBarLayout child,
final MotionEvent ev) {
public boolean onInterceptTouchEvent(@NonNull final CoordinatorLayout parent,
@NonNull final AppBarLayout child,
@NonNull final MotionEvent ev) {
for (final Integer element : skipInterceptionOfElements) {
final View view = child.findViewById(element);
if (view != null) {

View File

@@ -1,14 +1,13 @@
package org.schabi.newpipe;
import android.app.NotificationChannel;
import android.app.NotificationManager;
import android.content.Context;
import android.content.SharedPreferences;
import android.os.Build;
import android.util.Log;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.core.app.NotificationChannelCompat;
import androidx.core.app.NotificationManagerCompat;
import androidx.multidex.MultiDexApplication;
import androidx.preference.PreferenceManager;
@@ -27,7 +26,7 @@ import org.schabi.newpipe.error.UserAction;
import org.schabi.newpipe.extractor.NewPipe;
import org.schabi.newpipe.extractor.downloader.Downloader;
import org.schabi.newpipe.ktx.ExceptionUtils;
import org.schabi.newpipe.settings.SettingsActivity;
import org.schabi.newpipe.settings.NewPipeSettings;
import org.schabi.newpipe.util.Localization;
import org.schabi.newpipe.util.ServiceHelper;
import org.schabi.newpipe.util.StateSaver;
@@ -91,7 +90,7 @@ public class App extends MultiDexApplication {
app = this;
// Initialize settings first because others inits can use its values
SettingsActivity.initSettings(this);
NewPipeSettings.initSettings(this);
NewPipe.init(getDownloader(),
Localization.getPreferredLocalization(this),
@@ -233,38 +232,31 @@ public class App extends MultiDexApplication {
}
private void initNotificationChannels() {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
return;
}
// Keep the importance below DEFAULT to avoid making noise on every notification update for
// the main and update channels
final NotificationChannelCompat mainChannel = new NotificationChannelCompat
.Builder(getString(R.string.notification_channel_id),
NotificationManagerCompat.IMPORTANCE_LOW)
.setName(getString(R.string.notification_channel_name))
.setDescription(getString(R.string.notification_channel_description))
.build();
String id = getString(R.string.notification_channel_id);
String name = getString(R.string.notification_channel_name);
String description = getString(R.string.notification_channel_description);
final NotificationChannelCompat appUpdateChannel = new NotificationChannelCompat
.Builder(getString(R.string.app_update_notification_channel_id),
NotificationManagerCompat.IMPORTANCE_LOW)
.setName(getString(R.string.app_update_notification_channel_name))
.setDescription(getString(R.string.app_update_notification_channel_description))
.build();
// Keep this below DEFAULT to avoid making noise on every notification update for the main
// and update channels
int importance = NotificationManager.IMPORTANCE_LOW;
final NotificationChannelCompat hashChannel = new NotificationChannelCompat
.Builder(getString(R.string.hash_channel_id),
NotificationManagerCompat.IMPORTANCE_HIGH)
.setName(getString(R.string.hash_channel_name))
.setDescription(getString(R.string.hash_channel_description))
.build();
final NotificationChannel mainChannel = new NotificationChannel(id, name, importance);
mainChannel.setDescription(description);
id = getString(R.string.app_update_notification_channel_id);
name = getString(R.string.app_update_notification_channel_name);
description = getString(R.string.app_update_notification_channel_description);
final NotificationChannel appUpdateChannel = new NotificationChannel(id, name, importance);
appUpdateChannel.setDescription(description);
id = getString(R.string.hash_channel_id);
name = getString(R.string.hash_channel_name);
description = getString(R.string.hash_channel_description);
importance = NotificationManager.IMPORTANCE_HIGH;
final NotificationChannel hashChannel = new NotificationChannel(id, name, importance);
hashChannel.setDescription(description);
final NotificationManager notificationManager = getSystemService(NotificationManager.class);
notificationManager.createNotificationChannels(Arrays.asList(mainChannel,
final NotificationManagerCompat notificationManager = NotificationManagerCompat.from(this);
notificationManager.createNotificationChannelsCompat(Arrays.asList(mainChannel,
appUpdateChannel, hashChannel));
}

View File

@@ -129,13 +129,8 @@ public final class CheckForNewAppVersion {
if (BuildConfig.VERSION_CODE < versionCode) {
// A pending intent to open the apk location url in the browser.
final Intent viewIntent = new Intent(Intent.ACTION_VIEW, Uri.parse(apkLocationUrl));
final Intent intent = new Intent(Intent.ACTION_CHOOSER);
intent.putExtra(Intent.EXTRA_INTENT, viewIntent);
intent.putExtra(Intent.EXTRA_TITLE, R.string.open_with);
final Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(apkLocationUrl));
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
final PendingIntent pendingIntent
= PendingIntent.getActivity(application, 0, intent, 0);

View File

@@ -6,6 +6,8 @@ import android.content.Intent;
import android.os.Build;
import android.os.Bundle;
import org.schabi.newpipe.util.NavigationHelper;
/*
* Copyright (C) Hans-Christoph Steiner 2016 <hans@eds.org>
* ExitActivity.java is part of NewPipe.
@@ -48,6 +50,6 @@ public class ExitActivity extends Activity {
finish();
}
System.exit(0);
NavigationHelper.restartApp(this);
}
}

View File

@@ -402,7 +402,7 @@ public class MainActivity extends AppCompatActivity {
new Handler(Looper.getMainLooper()).postDelayed(() -> {
getSupportFragmentManager().popBackStack(null,
FragmentManager.POP_BACK_STACK_INCLUSIVE);
recreate();
ActivityCompat.recreate(MainActivity.this);
}, 300);
}
@@ -603,6 +603,7 @@ public class MainActivity extends AppCompatActivity {
public void onRequestPermissionsResult(final int requestCode,
@NonNull final String[] permissions,
@NonNull final int[] grantResults) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
for (final int i : grantResults) {
if (i == PackageManager.PERMISSION_DENIED) {
return;
@@ -822,7 +823,7 @@ public class MainActivity extends AppCompatActivity {
return;
}
if (PlayerHolder.isPlayerOpen()) {
if (PlayerHolder.getInstance().isPlayerOpen()) {
// if the player is already open, no need for a broadcast receiver
openMiniPlayerIfMissing();
} else {

View File

@@ -51,4 +51,15 @@ public final class NewPipeDatabase {
throw new RuntimeException("Checkpoint was blocked from completing");
}
}
public static void close() {
if (databaseInstance != null) {
synchronized (NewPipeDatabase.class) {
if (databaseInstance != null) {
databaseInstance.close();
databaseInstance = null;
}
}
}
}
}

View File

@@ -69,7 +69,7 @@ import org.schabi.newpipe.util.ExtractorHelper;
import org.schabi.newpipe.util.ListHelper;
import org.schabi.newpipe.util.NavigationHelper;
import org.schabi.newpipe.util.PermissionHelper;
import org.schabi.newpipe.util.ShareUtils;
import org.schabi.newpipe.util.external_communication.ShareUtils;
import org.schabi.newpipe.util.ThemeHelper;
import org.schabi.newpipe.util.urlfinder.UrlFinder;
import org.schabi.newpipe.views.FocusOverlayView;
@@ -107,6 +107,7 @@ public class RouterActivity extends AppCompatActivity {
protected String currentUrl;
private StreamingService currentService;
private boolean selectionIsDownload = false;
private AlertDialog alertDialogChoice = null;
@Override
protected void onCreate(final Bundle savedInstanceState) {
@@ -126,6 +127,15 @@ public class RouterActivity extends AppCompatActivity {
? R.style.RouterActivityThemeLight : R.style.RouterActivityThemeDark);
}
@Override
protected void onStop() {
super.onStop();
// we need to dismiss the dialog before leaving the activity or we get leaks
if (alertDialogChoice != null) {
alertDialogChoice.dismiss();
}
}
@Override
protected void onSaveInstanceState(@NonNull final Bundle outState) {
super.onSaveInstanceState(outState);
@@ -333,7 +343,7 @@ public class RouterActivity extends AppCompatActivity {
}
};
final AlertDialog alertDialog = new AlertDialog.Builder(themeWrapperContext)
alertDialogChoice = new AlertDialog.Builder(themeWrapperContext)
.setTitle(R.string.preferred_open_action_share_menu_title)
.setView(radioGroup)
.setCancelable(true)
@@ -347,12 +357,12 @@ public class RouterActivity extends AppCompatActivity {
.create();
//noinspection CodeBlock2Expr
alertDialog.setOnShowListener(dialog -> {
setDialogButtonsState(alertDialog, radioGroup.getCheckedRadioButtonId() != -1);
alertDialogChoice.setOnShowListener(dialog -> {
setDialogButtonsState(alertDialogChoice, radioGroup.getCheckedRadioButtonId() != -1);
});
radioGroup.setOnCheckedChangeListener((group, checkedId) ->
setDialogButtonsState(alertDialog, true));
setDialogButtonsState(alertDialogChoice, true));
final View.OnClickListener radioButtonsClickListener = v -> {
final int indexOfChild = radioGroup.indexOfChild(v);
if (indexOfChild == -1) {
@@ -402,10 +412,10 @@ public class RouterActivity extends AppCompatActivity {
}
selectedPreviously = selectedRadioPosition;
alertDialog.show();
alertDialogChoice.show();
if (DeviceUtils.isTv(this)) {
FocusOverlayView.setupFocusObserver(alertDialog);
FocusOverlayView.setupFocusObserver(alertDialogChoice);
}
}
@@ -443,7 +453,7 @@ public class RouterActivity extends AppCompatActivity {
returnList.add(showInfo);
returnList.add(videoPlayer);
} else {
final MainPlayer.PlayerType playerType = PlayerHolder.getType();
final MainPlayer.PlayerType playerType = PlayerHolder.getInstance().getType();
if (capabilities.contains(VIDEO)
&& PlayerHelper.isAutoplayAllowedByUser(context)
&& playerType == null || playerType == MainPlayer.PlayerType.VIDEO) {
@@ -579,9 +589,9 @@ public class RouterActivity extends AppCompatActivity {
downloadDialog.setVideoStreams(sortedVideoStreams);
downloadDialog.setAudioStreams(result.getAudioStreams());
downloadDialog.setSelectedVideoStream(selectedVideoStreamIndex);
downloadDialog.setOnDismissListener(dialog -> finish());
downloadDialog.show(fm, "downloadDialog");
fm.executePendingTransactions();
downloadDialog.requireDialog().setOnDismissListener(dialog -> finish());
}, throwable ->
showUnsupportedUrlDialog(currentUrl)));
}
@@ -590,6 +600,7 @@ public class RouterActivity extends AppCompatActivity {
public void onRequestPermissionsResult(final int requestCode,
@NonNull final String[] permissions,
@NonNull final int[] grantResults) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
for (final int i : grantResults) {
if (i == PackageManager.PERMISSION_DENIED) {
finish();

View File

@@ -17,8 +17,8 @@ import org.schabi.newpipe.R
import org.schabi.newpipe.databinding.ActivityAboutBinding
import org.schabi.newpipe.databinding.FragmentAboutBinding
import org.schabi.newpipe.util.Localization
import org.schabi.newpipe.util.ShareUtils
import org.schabi.newpipe.util.ThemeHelper
import org.schabi.newpipe.util.external_communication.ShareUtils
class AboutActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {

View File

@@ -1,10 +1,7 @@
package org.schabi.newpipe.about
import android.os.Bundle
import android.view.ContextMenu
import android.view.ContextMenu.ContextMenuInfo
import android.view.LayoutInflater
import android.view.MenuItem
import android.view.View
import android.view.ViewGroup
import androidx.core.os.bundleOf
@@ -14,31 +11,21 @@ import org.schabi.newpipe.R
import org.schabi.newpipe.about.LicenseFragmentHelper.showLicense
import org.schabi.newpipe.databinding.FragmentLicensesBinding
import org.schabi.newpipe.databinding.ItemSoftwareComponentBinding
import org.schabi.newpipe.util.ShareUtils
import java.util.Arrays
import java.util.Objects
/**
* Fragment containing the software licenses.
*/
class LicenseFragment : Fragment() {
private lateinit var softwareComponents: Array<SoftwareComponent>
private var componentForContextMenu: SoftwareComponent? = null
private var activeLicense: License? = null
private val compositeDisposable = CompositeDisposable()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
softwareComponents =
arguments?.getParcelableArray(ARG_COMPONENTS) as Array<SoftwareComponent>
if (savedInstanceState != null) {
val license = savedInstanceState.getSerializable(LICENSE_KEY)
if (license != null) {
activeLicense = license as License?
}
}
softwareComponents = arguments?.getParcelableArray(ARG_COMPONENTS) as Array<SoftwareComponent>
activeLicense = savedInstanceState?.getSerializable(LICENSE_KEY) as? License
// Sort components by name
Arrays.sort(softwareComponents, Comparator.comparing(SoftwareComponent::name))
softwareComponents.sortBy { it.name }
}
override fun onDestroy() {
@@ -73,49 +60,19 @@ class LicenseFragment : Fragment() {
root.setOnClickListener {
activeLicense = component.license
compositeDisposable.add(
showLicense(activity, component.license)
showLicense(activity, component)
)
}
binding.licensesSoftwareComponents.addView(root)
registerForContextMenu(root)
}
if (activeLicense != null) {
compositeDisposable.add(
showLicense(activity, activeLicense!!)
)
}
activeLicense?.let { compositeDisposable.add(showLicense(activity, it)) }
return binding.root
}
override fun onCreateContextMenu(menu: ContextMenu, v: View, menuInfo: ContextMenuInfo?) {
val inflater = requireActivity().menuInflater
val component = v.tag as SoftwareComponent
menu.setHeaderTitle(component.name)
inflater.inflate(R.menu.software_component, menu)
super.onCreateContextMenu(menu, v, menuInfo)
componentForContextMenu = component
}
override fun onContextItemSelected(item: MenuItem): Boolean {
// item.getMenuInfo() is null so we use the tag of the view
val component = componentForContextMenu ?: return false
when (item.itemId) {
R.id.menu_software_website -> {
ShareUtils.openUrlInBrowser(activity, component.link)
return true
}
R.id.menu_software_show_license -> compositeDisposable.add(
showLicense(activity, component.license)
)
}
return false
}
override fun onSaveInstanceState(savedInstanceState: Bundle) {
super.onSaveInstanceState(savedInstanceState)
if (activeLicense != null) {
savedInstanceState.putSerializable(LICENSE_KEY, activeLicense)
}
activeLicense?.let { savedInstanceState.putSerializable(LICENSE_KEY, it) }
}
companion object {
@@ -123,8 +80,7 @@ class LicenseFragment : Fragment() {
private const val LICENSE_KEY = "ACTIVE_LICENSE"
fun newInstance(softwareComponents: Array<SoftwareComponent>): LicenseFragment {
val fragment = LicenseFragment()
fragment.arguments =
bundleOf(ARG_COMPONENTS to Objects.requireNonNull(softwareComponents))
fragment.arguments = bundleOf(ARG_COMPONENTS to softwareComponents)
return fragment
}
}

View File

@@ -11,6 +11,7 @@ import io.reactivex.rxjava3.schedulers.Schedulers
import org.schabi.newpipe.R
import org.schabi.newpipe.util.Localization
import org.schabi.newpipe.util.ThemeHelper
import org.schabi.newpipe.util.external_communication.ShareUtils
import java.io.BufferedReader
import java.io.IOException
import java.io.InputStreamReader
@@ -113,4 +114,34 @@ object LicenseFragmentHelper {
}
}
}
@JvmStatic
fun showLicense(context: Context?, component: SoftwareComponent): Disposable {
return if (context == null) {
Disposable.empty()
} else {
Observable.fromCallable { getFormattedLicense(context, component.license) }
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe { formattedLicense: String ->
val webViewData = Base64.encodeToString(
formattedLicense
.toByteArray(StandardCharsets.UTF_8),
Base64.NO_PADDING
)
val webView = WebView(context)
webView.loadData(webViewData, "text/html; charset=UTF-8", "base64")
val alert = AlertDialog.Builder(context)
alert.setTitle(component.license.name)
alert.setView(webView)
Localization.assureCorrectAppLanguage(context)
alert.setPositiveButton(
R.string.dismiss
) { dialog, _ -> dialog.dismiss() }
alert.setNeutralButton(R.string.open_website_license) { _, _ ->
ShareUtils.openUrlInBrowser(context, component.link)
}
alert.show()
}
}
}
}

View File

@@ -1,64 +0,0 @@
package org.schabi.newpipe.database;
import androidx.room.TypeConverter;
import org.schabi.newpipe.extractor.stream.StreamType;
import org.schabi.newpipe.local.subscription.FeedGroupIcon;
import java.time.Instant;
import java.time.OffsetDateTime;
import java.time.ZoneOffset;
public final class Converters {
private Converters() { }
/**
* Convert a long value to a {@link OffsetDateTime}.
*
* @param value the long value
* @return the {@code OffsetDateTime}
*/
@TypeConverter
public static OffsetDateTime offsetDateTimeFromTimestamp(final Long value) {
return value == null ? null : OffsetDateTime.ofInstant(Instant.ofEpochMilli(value),
ZoneOffset.UTC);
}
/**
* Convert a {@link OffsetDateTime} to a long value.
*
* @param offsetDateTime the {@code OffsetDateTime}
* @return the long value
*/
@TypeConverter
public static Long offsetDateTimeToTimestamp(final OffsetDateTime offsetDateTime) {
return offsetDateTime == null ? null : offsetDateTime.withOffsetSameInstant(ZoneOffset.UTC)
.toInstant().toEpochMilli();
}
@TypeConverter
public static StreamType streamTypeOf(final String value) {
return StreamType.valueOf(value);
}
@TypeConverter
public static String stringOf(final StreamType streamType) {
return streamType.name();
}
@TypeConverter
public static Integer integerOf(final FeedGroupIcon feedGroupIcon) {
return feedGroupIcon.getId();
}
@TypeConverter
public static FeedGroupIcon feedGroupIconOf(final Integer id) {
for (final FeedGroupIcon icon : FeedGroupIcon.values()) {
if (icon.getId() == id) {
return icon;
}
}
throw new IllegalArgumentException("There's no feed group icon with the id \"" + id + "\"");
}
}

View File

@@ -0,0 +1,52 @@
package org.schabi.newpipe.database
import androidx.room.TypeConverter
import org.schabi.newpipe.extractor.stream.StreamType
import org.schabi.newpipe.local.subscription.FeedGroupIcon
import java.time.Instant
import java.time.OffsetDateTime
import java.time.ZoneOffset
object Converters {
/**
* Convert a long value to a [OffsetDateTime].
*
* @param value the long value
* @return the `OffsetDateTime`
*/
@TypeConverter
fun offsetDateTimeFromTimestamp(value: Long?): OffsetDateTime? {
return value?.let { OffsetDateTime.ofInstant(Instant.ofEpochMilli(it), ZoneOffset.UTC) }
}
/**
* Convert a [OffsetDateTime] to a long value.
*
* @param offsetDateTime the `OffsetDateTime`
* @return the long value
*/
@TypeConverter
fun offsetDateTimeToTimestamp(offsetDateTime: OffsetDateTime?): Long? {
return offsetDateTime?.withOffsetSameInstant(ZoneOffset.UTC)?.toInstant()?.toEpochMilli()
}
@TypeConverter
fun streamTypeOf(value: String): StreamType {
return StreamType.valueOf(value)
}
@TypeConverter
fun stringOf(streamType: StreamType): String {
return streamType.name
}
@TypeConverter
fun integerOf(feedGroupIcon: FeedGroupIcon): Int {
return feedGroupIcon.id
}
@TypeConverter
fun feedGroupIconOf(id: Int): FeedGroupIcon {
return FeedGroupIcon.values().first { it.id == id }
}
}

View File

@@ -9,7 +9,8 @@ import androidx.room.Update
import io.reactivex.rxjava3.core.Flowable
import org.schabi.newpipe.database.feed.model.FeedEntity
import org.schabi.newpipe.database.feed.model.FeedLastUpdatedEntity
import org.schabi.newpipe.database.stream.model.StreamEntity
import org.schabi.newpipe.database.stream.StreamWithState
import org.schabi.newpipe.database.stream.model.StreamStateEntity
import org.schabi.newpipe.database.subscription.SubscriptionEntity
import java.time.OffsetDateTime
@@ -20,21 +21,34 @@ abstract class FeedDAO {
@Query(
"""
SELECT s.* FROM streams s
SELECT s.*, sst.progress_time
FROM streams s
LEFT JOIN stream_state sst
ON s.uid = sst.stream_id
LEFT JOIN stream_history sh
ON s.uid = sh.stream_id
INNER JOIN feed f
ON s.uid = f.stream_id
ORDER BY s.upload_date IS NULL DESC, s.upload_date DESC, s.uploader ASC
LIMIT 500
"""
)
abstract fun getAllStreams(): Flowable<List<StreamEntity>>
abstract fun getAllStreams(): Flowable<List<StreamWithState>>
@Query(
"""
SELECT s.* FROM streams s
SELECT s.*, sst.progress_time
FROM streams s
LEFT JOIN stream_state sst
ON s.uid = sst.stream_id
LEFT JOIN stream_history sh
ON s.uid = sh.stream_id
INNER JOIN feed f
ON s.uid = f.stream_id
@@ -42,16 +56,88 @@ abstract class FeedDAO {
INNER JOIN feed_group_subscription_join fgs
ON fgs.subscription_id = f.subscription_id
INNER JOIN feed_group fg
ON fg.uid = fgs.group_id
WHERE fgs.group_id = :groupId
ORDER BY s.upload_date IS NULL DESC, s.upload_date DESC, s.uploader ASC
LIMIT 500
"""
)
abstract fun getAllStreamsFromGroup(groupId: Long): Flowable<List<StreamEntity>>
abstract fun getAllStreamsForGroup(groupId: Long): Flowable<List<StreamWithState>>
/**
* @see StreamStateEntity.isFinished()
* @see StreamStateEntity.PLAYBACK_FINISHED_END_MILLISECONDS
* @return all of the non-live, never-played and non-finished streams in the feed
* (all of the cited conditions must hold for a stream to be in the returned list)
*/
@Query(
"""
SELECT s.*, sst.progress_time
FROM streams s
LEFT JOIN stream_state sst
ON s.uid = sst.stream_id
LEFT JOIN stream_history sh
ON s.uid = sh.stream_id
INNER JOIN feed f
ON s.uid = f.stream_id
WHERE (
sh.stream_id IS NULL
OR sst.stream_id IS NULL
OR sst.progress_time < s.duration * 1000 - ${StreamStateEntity.PLAYBACK_FINISHED_END_MILLISECONDS}
OR sst.progress_time < s.duration * 1000 * 3 / 4
OR s.stream_type = 'LIVE_STREAM'
OR s.stream_type = 'AUDIO_LIVE_STREAM'
)
ORDER BY s.upload_date IS NULL DESC, s.upload_date DESC, s.uploader ASC
LIMIT 500
"""
)
abstract fun getLiveOrNotPlayedStreams(): Flowable<List<StreamWithState>>
/**
* @see StreamStateEntity.isFinished()
* @see StreamStateEntity.PLAYBACK_FINISHED_END_MILLISECONDS
* @param groupId the group id to get streams of
* @return all of the non-live, never-played and non-finished streams for the given feed group
* (all of the cited conditions must hold for a stream to be in the returned list)
*/
@Query(
"""
SELECT s.*, sst.progress_time
FROM streams s
LEFT JOIN stream_state sst
ON s.uid = sst.stream_id
LEFT JOIN stream_history sh
ON s.uid = sh.stream_id
INNER JOIN feed f
ON s.uid = f.stream_id
INNER JOIN feed_group_subscription_join fgs
ON fgs.subscription_id = f.subscription_id
WHERE fgs.group_id = :groupId
AND (
sh.stream_id IS NULL
OR sst.stream_id IS NULL
OR sst.progress_time < s.duration * 1000 - ${StreamStateEntity.PLAYBACK_FINISHED_END_MILLISECONDS}
OR sst.progress_time < s.duration * 1000 * 3 / 4
OR s.stream_type = 'LIVE_STREAM'
OR s.stream_type = 'AUDIO_LIVE_STREAM'
)
ORDER BY s.upload_date IS NULL DESC, s.upload_date DESC, s.uploader ASC
LIMIT 500
"""
)
abstract fun getLiveOrNotPlayedStreamsForGroup(groupId: Long): Flowable<List<StreamWithState>>
@Query(
"""

View File

@@ -21,7 +21,7 @@ import static org.schabi.newpipe.database.stream.StreamStatisticsEntry.STREAM_WA
import static org.schabi.newpipe.database.stream.model.StreamEntity.STREAM_ID;
import static org.schabi.newpipe.database.stream.model.StreamEntity.STREAM_TABLE;
import static org.schabi.newpipe.database.stream.model.StreamStateEntity.JOIN_STREAM_ID_ALIAS;
import static org.schabi.newpipe.database.stream.model.StreamStateEntity.STREAM_PROGRESS_TIME;
import static org.schabi.newpipe.database.stream.model.StreamStateEntity.STREAM_PROGRESS_MILLIS;
import static org.schabi.newpipe.database.stream.model.StreamStateEntity.STREAM_STATE_TABLE;
@Dao
@@ -80,7 +80,7 @@ public abstract class StreamHistoryDAO implements HistoryDAO<StreamHistoryEntity
+ " LEFT JOIN "
+ "(SELECT " + JOIN_STREAM_ID + " AS " + JOIN_STREAM_ID_ALIAS + ", "
+ STREAM_PROGRESS_TIME
+ STREAM_PROGRESS_MILLIS
+ " FROM " + STREAM_STATE_TABLE + " )"
+ " ON " + STREAM_ID + " = " + JOIN_STREAM_ID_ALIAS)
public abstract Flowable<List<StreamStatisticsEntry>> getStatistics();

View File

@@ -12,8 +12,8 @@ data class PlaylistStreamEntry(
@Embedded
val streamEntity: StreamEntity,
@ColumnInfo(name = StreamStateEntity.STREAM_PROGRESS_TIME, defaultValue = "0")
val progressTime: Long,
@ColumnInfo(name = StreamStateEntity.STREAM_PROGRESS_MILLIS, defaultValue = "0")
val progressMillis: Long,
@ColumnInfo(name = PlaylistStreamEntity.JOIN_STREAM_ID)
val streamId: Long,

View File

@@ -14,26 +14,26 @@ import static org.schabi.newpipe.database.playlist.model.PlaylistEntity.PLAYLIST
import static org.schabi.newpipe.database.playlist.model.PlaylistEntity.PLAYLIST_TABLE;
@Dao
public abstract class PlaylistDAO implements BasicDAO<PlaylistEntity> {
public interface PlaylistDAO extends BasicDAO<PlaylistEntity> {
@Override
@Query("SELECT * FROM " + PLAYLIST_TABLE)
public abstract Flowable<List<PlaylistEntity>> getAll();
Flowable<List<PlaylistEntity>> getAll();
@Override
@Query("DELETE FROM " + PLAYLIST_TABLE)
public abstract int deleteAll();
int deleteAll();
@Override
public Flowable<List<PlaylistEntity>> listByService(final int serviceId) {
default Flowable<List<PlaylistEntity>> listByService(final int serviceId) {
throw new UnsupportedOperationException();
}
@Query("SELECT * FROM " + PLAYLIST_TABLE + " WHERE " + PLAYLIST_ID + " = :playlistId")
public abstract Flowable<List<PlaylistEntity>> getPlaylist(long playlistId);
Flowable<List<PlaylistEntity>> getPlaylist(long playlistId);
@Query("DELETE FROM " + PLAYLIST_TABLE + " WHERE " + PLAYLIST_ID + " = :playlistId")
public abstract int deletePlaylist(long playlistId);
int deletePlaylist(long playlistId);
@Query("SELECT COUNT(*) FROM " + PLAYLIST_TABLE)
public abstract Flowable<Long> getCount();
Flowable<Long> getCount();
}

View File

@@ -17,31 +17,31 @@ import static org.schabi.newpipe.database.playlist.model.PlaylistRemoteEntity.RE
import static org.schabi.newpipe.database.playlist.model.PlaylistRemoteEntity.REMOTE_PLAYLIST_URL;
@Dao
public abstract class PlaylistRemoteDAO implements BasicDAO<PlaylistRemoteEntity> {
public interface PlaylistRemoteDAO extends BasicDAO<PlaylistRemoteEntity> {
@Override
@Query("SELECT * FROM " + REMOTE_PLAYLIST_TABLE)
public abstract Flowable<List<PlaylistRemoteEntity>> getAll();
Flowable<List<PlaylistRemoteEntity>> getAll();
@Override
@Query("DELETE FROM " + REMOTE_PLAYLIST_TABLE)
public abstract int deleteAll();
int deleteAll();
@Override
@Query("SELECT * FROM " + REMOTE_PLAYLIST_TABLE
+ " WHERE " + REMOTE_PLAYLIST_SERVICE_ID + " = :serviceId")
public abstract Flowable<List<PlaylistRemoteEntity>> listByService(int serviceId);
Flowable<List<PlaylistRemoteEntity>> listByService(int serviceId);
@Query("SELECT * FROM " + REMOTE_PLAYLIST_TABLE + " WHERE "
+ REMOTE_PLAYLIST_URL + " = :url AND " + REMOTE_PLAYLIST_SERVICE_ID + " = :serviceId")
public abstract Flowable<List<PlaylistRemoteEntity>> getPlaylist(long serviceId, String url);
Flowable<List<PlaylistRemoteEntity>> getPlaylist(long serviceId, String url);
@Query("SELECT " + REMOTE_PLAYLIST_ID + " FROM " + REMOTE_PLAYLIST_TABLE
+ " WHERE " + REMOTE_PLAYLIST_URL + " = :url "
+ "AND " + REMOTE_PLAYLIST_SERVICE_ID + " = :serviceId")
abstract Long getPlaylistIdInternal(long serviceId, String url);
Long getPlaylistIdInternal(long serviceId, String url);
@Transaction
public long upsert(final PlaylistRemoteEntity playlist) {
default long upsert(final PlaylistRemoteEntity playlist) {
final Long playlistId = getPlaylistIdInternal(playlist.getServiceId(), playlist.getUrl());
if (playlistId == null) {
@@ -55,5 +55,5 @@ public abstract class PlaylistRemoteDAO implements BasicDAO<PlaylistRemoteEntity
@Query("DELETE FROM " + REMOTE_PLAYLIST_TABLE
+ " WHERE " + REMOTE_PLAYLIST_ID + " = :playlistId")
public abstract int deletePlaylist(long playlistId);
int deletePlaylist(long playlistId);
}

View File

@@ -25,32 +25,32 @@ import static org.schabi.newpipe.database.playlist.model.PlaylistStreamEntity.PL
import static org.schabi.newpipe.database.stream.model.StreamEntity.STREAM_ID;
import static org.schabi.newpipe.database.stream.model.StreamEntity.STREAM_TABLE;
import static org.schabi.newpipe.database.stream.model.StreamStateEntity.JOIN_STREAM_ID_ALIAS;
import static org.schabi.newpipe.database.stream.model.StreamStateEntity.STREAM_PROGRESS_TIME;
import static org.schabi.newpipe.database.stream.model.StreamStateEntity.STREAM_PROGRESS_MILLIS;
import static org.schabi.newpipe.database.stream.model.StreamStateEntity.STREAM_STATE_TABLE;
@Dao
public abstract class PlaylistStreamDAO implements BasicDAO<PlaylistStreamEntity> {
public interface PlaylistStreamDAO extends BasicDAO<PlaylistStreamEntity> {
@Override
@Query("SELECT * FROM " + PLAYLIST_STREAM_JOIN_TABLE)
public abstract Flowable<List<PlaylistStreamEntity>> getAll();
Flowable<List<PlaylistStreamEntity>> getAll();
@Override
@Query("DELETE FROM " + PLAYLIST_STREAM_JOIN_TABLE)
public abstract int deleteAll();
int deleteAll();
@Override
public Flowable<List<PlaylistStreamEntity>> listByService(final int serviceId) {
default Flowable<List<PlaylistStreamEntity>> listByService(final int serviceId) {
throw new UnsupportedOperationException();
}
@Query("DELETE FROM " + PLAYLIST_STREAM_JOIN_TABLE
+ " WHERE " + JOIN_PLAYLIST_ID + " = :playlistId")
public abstract void deleteBatch(long playlistId);
void deleteBatch(long playlistId);
@Query("SELECT COALESCE(MAX(" + JOIN_INDEX + "), -1)"
+ " FROM " + PLAYLIST_STREAM_JOIN_TABLE
+ " WHERE " + JOIN_PLAYLIST_ID + " = :playlistId")
public abstract Flowable<Integer> getMaximumIndexOf(long playlistId);
Flowable<Integer> getMaximumIndexOf(long playlistId);
@Transaction
@Query("SELECT * FROM " + STREAM_TABLE + " INNER JOIN "
@@ -64,12 +64,12 @@ public abstract class PlaylistStreamDAO implements BasicDAO<PlaylistStreamEntity
+ " LEFT JOIN "
+ "(SELECT " + JOIN_STREAM_ID + " AS " + JOIN_STREAM_ID_ALIAS + ", "
+ STREAM_PROGRESS_TIME
+ STREAM_PROGRESS_MILLIS
+ " FROM " + STREAM_STATE_TABLE + " )"
+ " ON " + STREAM_ID + " = " + JOIN_STREAM_ID_ALIAS
+ " ORDER BY " + JOIN_INDEX + " ASC")
public abstract Flowable<List<PlaylistStreamEntry>> getOrderedStreamsOf(long playlistId);
Flowable<List<PlaylistStreamEntry>> getOrderedStreamsOf(long playlistId);
@Transaction
@Query("SELECT " + PLAYLIST_ID + ", " + PLAYLIST_NAME + ", " + PLAYLIST_THUMBNAIL_URL + ", "
@@ -80,5 +80,5 @@ public abstract class PlaylistStreamDAO implements BasicDAO<PlaylistStreamEntity
+ " ON " + PLAYLIST_ID + " = " + JOIN_PLAYLIST_ID
+ " GROUP BY " + JOIN_PLAYLIST_ID
+ " ORDER BY " + PLAYLIST_NAME + " COLLATE NOCASE ASC")
public abstract Flowable<List<PlaylistMetadataEntry>> getPlaylistMetadata();
Flowable<List<PlaylistMetadataEntry>> getPlaylistMetadata();
}

View File

@@ -5,7 +5,7 @@ import androidx.room.Embedded
import org.schabi.newpipe.database.LocalItem
import org.schabi.newpipe.database.history.model.StreamHistoryEntity
import org.schabi.newpipe.database.stream.model.StreamEntity
import org.schabi.newpipe.database.stream.model.StreamStateEntity.STREAM_PROGRESS_TIME
import org.schabi.newpipe.database.stream.model.StreamStateEntity.STREAM_PROGRESS_MILLIS
import org.schabi.newpipe.extractor.stream.StreamInfoItem
import java.time.OffsetDateTime
@@ -13,8 +13,8 @@ class StreamStatisticsEntry(
@Embedded
val streamEntity: StreamEntity,
@ColumnInfo(name = STREAM_PROGRESS_TIME, defaultValue = "0")
val progressTime: Long,
@ColumnInfo(name = STREAM_PROGRESS_MILLIS, defaultValue = "0")
val progressMillis: Long,
@ColumnInfo(name = StreamHistoryEntity.JOIN_STREAM_ID)
val streamId: Long,

View File

@@ -0,0 +1,14 @@
package org.schabi.newpipe.database.stream
import androidx.room.ColumnInfo
import androidx.room.Embedded
import org.schabi.newpipe.database.stream.model.StreamEntity
import org.schabi.newpipe.database.stream.model.StreamStateEntity
data class StreamWithState(
@Embedded
val stream: StreamEntity,
@ColumnInfo(name = StreamStateEntity.STREAM_PROGRESS_MILLIS)
val stateProgressMillis: Long?
)

View File

@@ -17,31 +17,31 @@ import static org.schabi.newpipe.database.stream.model.StreamStateEntity.JOIN_ST
import static org.schabi.newpipe.database.stream.model.StreamStateEntity.STREAM_STATE_TABLE;
@Dao
public abstract class StreamStateDAO implements BasicDAO<StreamStateEntity> {
public interface StreamStateDAO extends BasicDAO<StreamStateEntity> {
@Override
@Query("SELECT * FROM " + STREAM_STATE_TABLE)
public abstract Flowable<List<StreamStateEntity>> getAll();
Flowable<List<StreamStateEntity>> getAll();
@Override
@Query("DELETE FROM " + STREAM_STATE_TABLE)
public abstract int deleteAll();
int deleteAll();
@Override
public Flowable<List<StreamStateEntity>> listByService(final int serviceId) {
default Flowable<List<StreamStateEntity>> listByService(final int serviceId) {
throw new UnsupportedOperationException();
}
@Query("SELECT * FROM " + STREAM_STATE_TABLE + " WHERE " + JOIN_STREAM_ID + " = :streamId")
public abstract Flowable<List<StreamStateEntity>> getState(long streamId);
Flowable<List<StreamStateEntity>> getState(long streamId);
@Query("DELETE FROM " + STREAM_STATE_TABLE + " WHERE " + JOIN_STREAM_ID + " = :streamId")
public abstract int deleteState(long streamId);
int deleteState(long streamId);
@Insert(onConflict = OnConflictStrategy.IGNORE)
abstract void silentInsertInternal(StreamStateEntity streamState);
void silentInsertInternal(StreamStateEntity streamState);
@Transaction
public long upsert(final StreamStateEntity stream) {
default long upsert(final StreamStateEntity stream) {
silentInsertInternal(stream);
return update(stream);
}

View File

@@ -5,7 +5,7 @@ import androidx.room.ColumnInfo;
import androidx.room.Entity;
import androidx.room.ForeignKey;
import java.util.concurrent.TimeUnit;
import java.util.Objects;
import static androidx.room.ForeignKey.CASCADE;
import static org.schabi.newpipe.database.stream.model.StreamStateEntity.JOIN_STREAM_ID;
@@ -25,26 +25,31 @@ public class StreamStateEntity {
// This additional field is required for the SQL query because 'stream_id' is used
// for some other joins already
public static final String JOIN_STREAM_ID_ALIAS = "stream_id_alias";
public static final String STREAM_PROGRESS_TIME = "progress_time";
public static final String STREAM_PROGRESS_MILLIS = "progress_time";
/**
* Playback state will not be saved, if playback time is less than this threshold.
* Playback state will not be saved, if playback time is less than this threshold (5000ms = 5s).
*/
private static final int PLAYBACK_SAVE_THRESHOLD_START_SECONDS = 5;
private static final long PLAYBACK_SAVE_THRESHOLD_START_MILLISECONDS = 5000;
/**
* Playback state will not be saved, if time left is less than this threshold.
* Stream will be considered finished if the playback time left exceeds this threshold
* (60000ms = 60s).
* @see #isFinished(long)
* @see org.schabi.newpipe.database.feed.dao.FeedDAO#getLiveOrNotPlayedStreams()
* @see org.schabi.newpipe.database.feed.dao.FeedDAO#getLiveOrNotPlayedStreamsForGroup(long)
*/
private static final int PLAYBACK_SAVE_THRESHOLD_END_SECONDS = 10;
public static final long PLAYBACK_FINISHED_END_MILLISECONDS = 60000;
@ColumnInfo(name = JOIN_STREAM_ID)
private long streamUid;
@ColumnInfo(name = STREAM_PROGRESS_TIME)
private long progressTime;
@ColumnInfo(name = STREAM_PROGRESS_MILLIS)
private long progressMillis;
public StreamStateEntity(final long streamUid, final long progressTime) {
public StreamStateEntity(final long streamUid, final long progressMillis) {
this.streamUid = streamUid;
this.progressTime = progressTime;
this.progressMillis = progressMillis;
}
public long getStreamUid() {
@@ -55,27 +60,53 @@ public class StreamStateEntity {
this.streamUid = streamUid;
}
public long getProgressTime() {
return progressTime;
public long getProgressMillis() {
return progressMillis;
}
public void setProgressTime(final long progressTime) {
this.progressTime = progressTime;
public void setProgressMillis(final long progressMillis) {
this.progressMillis = progressMillis;
}
public boolean isValid(final int durationInSeconds) {
final int seconds = (int) TimeUnit.MILLISECONDS.toSeconds(progressTime);
return seconds > PLAYBACK_SAVE_THRESHOLD_START_SECONDS
&& seconds < durationInSeconds - PLAYBACK_SAVE_THRESHOLD_END_SECONDS;
/**
* The state will be considered valid, and thus be saved, if the progress is more than {@link
* #PLAYBACK_SAVE_THRESHOLD_START_MILLISECONDS} or at least 1/4 of the video length.
* @param durationInSeconds the duration of the stream connected with this state, in seconds
* @return whether this stream state entity should be saved or not
*/
public boolean isValid(final long durationInSeconds) {
return progressMillis > PLAYBACK_SAVE_THRESHOLD_START_MILLISECONDS
|| progressMillis > durationInSeconds * 1000 / 4;
}
/**
* The video will be considered as finished, if the time left is less than {@link
* #PLAYBACK_FINISHED_END_MILLISECONDS} and the progress is at least 3/4 of the video length.
* The state will be saved anyway, so that it can be shown under stream info items, but the
* player will not resume if a state is considered as finished. Finished streams are also the
* ones that can be filtered out in the feed fragment.
* @see org.schabi.newpipe.database.feed.dao.FeedDAO#getLiveOrNotPlayedStreams()
* @see org.schabi.newpipe.database.feed.dao.FeedDAO#getLiveOrNotPlayedStreamsForGroup(long)
* @param durationInSeconds the duration of the stream connected with this state, in seconds
* @return whether the stream is finished or not
*/
public boolean isFinished(final long durationInSeconds) {
return progressMillis >= durationInSeconds * 1000 - PLAYBACK_FINISHED_END_MILLISECONDS
&& progressMillis >= durationInSeconds * 1000 * 3 / 4;
}
@Override
public boolean equals(@Nullable final Object obj) {
if (obj instanceof StreamStateEntity) {
return ((StreamStateEntity) obj).streamUid == streamUid
&& ((StreamStateEntity) obj).progressTime == progressTime;
&& ((StreamStateEntity) obj).progressMillis == progressMillis;
} else {
return false;
}
}
@Override
public int hashCode() {
return Objects.hash(streamUid, progressMillis);
}
}

View File

@@ -3,6 +3,8 @@ package org.schabi.newpipe.download;
import android.app.Activity;
import android.content.ComponentName;
import android.content.Context;
import android.content.DialogInterface;
import android.content.DialogInterface.OnDismissListener;
import android.content.Intent;
import android.content.ServiceConnection;
import android.content.SharedPreferences;
@@ -20,6 +22,9 @@ import android.widget.RadioGroup;
import android.widget.SeekBar;
import android.widget.Toast;
import androidx.activity.result.ActivityResult;
import androidx.activity.result.ActivityResultLauncher;
import androidx.activity.result.contract.ActivityResultContracts.StartActivityForResult;
import androidx.annotation.IdRes;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
@@ -35,7 +40,6 @@ import com.nononsenseapps.filepicker.Utils;
import org.schabi.newpipe.MainActivity;
import org.schabi.newpipe.R;
import org.schabi.newpipe.RouterActivity;
import org.schabi.newpipe.databinding.DownloadDialogBinding;
import org.schabi.newpipe.error.ErrorActivity;
import org.schabi.newpipe.error.ErrorInfo;
@@ -49,6 +53,8 @@ import org.schabi.newpipe.extractor.stream.StreamInfo;
import org.schabi.newpipe.extractor.stream.SubtitlesStream;
import org.schabi.newpipe.extractor.stream.VideoStream;
import org.schabi.newpipe.settings.NewPipeSettings;
import org.schabi.newpipe.streams.io.StoredDirectoryHelper;
import org.schabi.newpipe.streams.io.StoredFileHelper;
import org.schabi.newpipe.util.FilePickerActivityHelper;
import org.schabi.newpipe.util.FilenameUtils;
import org.schabi.newpipe.util.ListHelper;
@@ -68,8 +74,6 @@ import icepick.Icepick;
import icepick.State;
import io.reactivex.rxjava3.disposables.CompositeDisposable;
import us.shandian.giga.get.MissionRecoveryInfo;
import us.shandian.giga.io.StoredDirectoryHelper;
import us.shandian.giga.io.StoredFileHelper;
import us.shandian.giga.postprocessing.Postprocessing;
import us.shandian.giga.service.DownloadManager;
import us.shandian.giga.service.DownloadManagerService;
@@ -82,7 +86,6 @@ public class DownloadDialog extends DialogFragment
implements RadioGroup.OnCheckedChangeListener, AdapterView.OnItemSelectedListener {
private static final String TAG = "DialogFragment";
private static final boolean DEBUG = MainActivity.DEBUG;
private static final int REQUEST_DOWNLOAD_SAVE_AS = 0x1230;
@State
StreamInfo currentInfo;
@@ -99,6 +102,9 @@ public class DownloadDialog extends DialogFragment
@State
int selectedSubtitleIndex = 0;
@Nullable
private OnDismissListener onDismissListener = null;
private StoredDirectoryHelper mainStorageAudio = null;
private StoredDirectoryHelper mainStorageVideo = null;
private DownloadManager downloadManager = null;
@@ -116,6 +122,25 @@ public class DownloadDialog extends DialogFragment
private SharedPreferences prefs;
// Variables for file name and MIME type when picking new folder because it's not set yet
private String filenameTmp;
private String mimeTmp;
private final ActivityResultLauncher<Intent> requestDownloadSaveAsLauncher =
registerForActivityResult(
new StartActivityForResult(), this::requestDownloadSaveAsResult);
private final ActivityResultLauncher<Intent> requestDownloadPickAudioFolderLauncher =
registerForActivityResult(
new StartActivityForResult(), this::requestDownloadPickAudioFolderResult);
private final ActivityResultLauncher<Intent> requestDownloadPickVideoFolderLauncher =
registerForActivityResult(
new StartActivityForResult(), this::requestDownloadPickVideoFolderResult);
/*//////////////////////////////////////////////////////////////////////////
// Instance creation
//////////////////////////////////////////////////////////////////////////*/
public static DownloadDialog newInstance(final StreamInfo info) {
final DownloadDialog dialog = new DownloadDialog();
dialog.setInfo(info);
@@ -137,6 +162,11 @@ public class DownloadDialog extends DialogFragment
return instance;
}
/*//////////////////////////////////////////////////////////////////////////
// Setters
//////////////////////////////////////////////////////////////////////////*/
private void setInfo(final StreamInfo info) {
this.currentInfo = info;
}
@@ -153,10 +183,6 @@ public class DownloadDialog extends DialogFragment
setVideoStreams(new StreamSizeWrapper<>(videoStreams, getContext()));
}
/*//////////////////////////////////////////////////////////////////////////
// LifeCycle
//////////////////////////////////////////////////////////////////////////*/
public void setVideoStreams(final StreamSizeWrapper<VideoStream> wvs) {
this.wrappedVideoStreams = wvs;
}
@@ -182,6 +208,14 @@ public class DownloadDialog extends DialogFragment
this.selectedSubtitleIndex = ssi;
}
public void setOnDismissListener(@Nullable final OnDismissListener onDismissListener) {
this.onDismissListener = onDismissListener;
}
/*//////////////////////////////////////////////////////////////////////////
// Android lifecycle
//////////////////////////////////////////////////////////////////////////*/
@Override
public void onCreate(@Nullable final Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
@@ -192,7 +226,7 @@ public class DownloadDialog extends DialogFragment
if (!PermissionHelper.checkStoragePermissions(getActivity(),
PermissionHelper.DOWNLOAD_DIALOG_REQUEST_CODE)) {
getDialog().dismiss();
dismiss();
return;
}
@@ -251,10 +285,6 @@ public class DownloadDialog extends DialogFragment
}, Context.BIND_AUTO_CREATE);
}
/*//////////////////////////////////////////////////////////////////////////
// Inits
//////////////////////////////////////////////////////////////////////////*/
@Override
public View onCreateView(@NonNull final LayoutInflater inflater, final ViewGroup container,
final Bundle savedInstanceState) {
@@ -310,6 +340,60 @@ public class DownloadDialog extends DialogFragment
fetchStreamsSize();
}
private void initToolbar(final Toolbar toolbar) {
if (DEBUG) {
Log.d(TAG, "initToolbar() called with: toolbar = [" + toolbar + "]");
}
toolbar.setTitle(R.string.download_dialog_title);
toolbar.setNavigationIcon(R.drawable.ic_arrow_back);
toolbar.inflateMenu(R.menu.dialog_url);
toolbar.setNavigationOnClickListener(v -> dismiss());
toolbar.setNavigationContentDescription(R.string.cancel);
okButton = toolbar.findViewById(R.id.okay);
okButton.setEnabled(false); // disable until the download service connection is done
toolbar.setOnMenuItemClickListener(item -> {
if (item.getItemId() == R.id.okay) {
prepareSelectedDownload();
return true;
}
return false;
});
}
@Override
public void onDismiss(@NonNull final DialogInterface dialog) {
super.onDismiss(dialog);
if (onDismissListener != null) {
onDismissListener.onDismiss(dialog);
}
}
@Override
public void onDestroy() {
super.onDestroy();
disposables.clear();
}
@Override
public void onDestroyView() {
dialogBinding = null;
super.onDestroyView();
}
@Override
public void onSaveInstanceState(@NonNull final Bundle outState) {
super.onSaveInstanceState(outState);
Icepick.saveInstanceState(this, outState);
}
/*//////////////////////////////////////////////////////////////////////////
// Video, audio and subtitle spinners
//////////////////////////////////////////////////////////////////////////*/
private void fetchStreamsSize() {
disposables.clear();
disposables.add(StreamSizeWrapper.fetchSizeForWrapper(wrappedVideoStreams)
@@ -344,91 +428,6 @@ public class DownloadDialog extends DialogFragment
currentInfo.getServiceId()))));
}
@Override
public void onDestroy() {
super.onDestroy();
disposables.clear();
}
@Override
public void onDestroyView() {
dialogBinding = null;
super.onDestroyView();
}
/*//////////////////////////////////////////////////////////////////////////
// Radio group Video&Audio options - Listener
//////////////////////////////////////////////////////////////////////////*/
@Override
public void onSaveInstanceState(@NonNull final Bundle outState) {
super.onSaveInstanceState(outState);
Icepick.saveInstanceState(this, outState);
}
/*//////////////////////////////////////////////////////////////////////////
// Streams Spinner Listener
//////////////////////////////////////////////////////////////////////////*/
@Override
public void onActivityResult(final int requestCode, final int resultCode, final Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if (requestCode == REQUEST_DOWNLOAD_SAVE_AS && resultCode == Activity.RESULT_OK) {
if (data.getData() == null) {
showFailedDialog(R.string.general_error);
return;
}
if (FilePickerActivityHelper.isOwnFileUri(context, data.getData())) {
final File file = Utils.getFileForUri(data.getData());
checkSelectedDownload(null, Uri.fromFile(file), file.getName(),
StoredFileHelper.DEFAULT_MIME);
return;
}
final DocumentFile docFile = DocumentFile.fromSingleUri(context, data.getData());
if (docFile == null) {
showFailedDialog(R.string.general_error);
return;
}
// check if the selected file was previously used
checkSelectedDownload(null, data.getData(), docFile.getName(),
docFile.getType());
}
}
private void initToolbar(final Toolbar toolbar) {
if (DEBUG) {
Log.d(TAG, "initToolbar() called with: toolbar = [" + toolbar + "]");
}
toolbar.setTitle(R.string.download_dialog_title);
toolbar.setNavigationIcon(R.drawable.ic_arrow_back);
toolbar.inflateMenu(R.menu.dialog_url);
toolbar.setNavigationOnClickListener(v -> requireDialog().dismiss());
toolbar.setNavigationContentDescription(R.string.cancel);
okButton = toolbar.findViewById(R.id.okay);
okButton.setEnabled(false); // disable until the download service connection is done
toolbar.setOnMenuItemClickListener(item -> {
if (item.getItemId() == R.id.okay) {
prepareSelectedDownload();
if (getActivity() instanceof RouterActivity) {
getActivity().finish();
}
return true;
}
return false;
});
}
/*//////////////////////////////////////////////////////////////////////////
// Utils
//////////////////////////////////////////////////////////////////////////*/
private void setupAudioSpinner() {
if (getContext() == null) {
return;
@@ -459,6 +458,88 @@ public class DownloadDialog extends DialogFragment
setRadioButtonsState(true);
}
/*//////////////////////////////////////////////////////////////////////////
// Activity results
//////////////////////////////////////////////////////////////////////////*/
private void requestDownloadPickAudioFolderResult(final ActivityResult result) {
requestDownloadPickFolderResult(
result, getString(R.string.download_path_audio_key), DownloadManager.TAG_AUDIO);
}
private void requestDownloadPickVideoFolderResult(final ActivityResult result) {
requestDownloadPickFolderResult(
result, getString(R.string.download_path_video_key), DownloadManager.TAG_VIDEO);
}
private void requestDownloadSaveAsResult(final ActivityResult result) {
if (result.getResultCode() != Activity.RESULT_OK) {
return;
}
if (result.getData() == null || result.getData().getData() == null) {
showFailedDialog(R.string.general_error);
return;
}
if (FilePickerActivityHelper.isOwnFileUri(context, result.getData().getData())) {
final File file = Utils.getFileForUri(result.getData().getData());
checkSelectedDownload(null, Uri.fromFile(file), file.getName(),
StoredFileHelper.DEFAULT_MIME);
return;
}
final DocumentFile docFile
= DocumentFile.fromSingleUri(context, result.getData().getData());
if (docFile == null) {
showFailedDialog(R.string.general_error);
return;
}
// check if the selected file was previously used
checkSelectedDownload(null, result.getData().getData(), docFile.getName(),
docFile.getType());
}
private void requestDownloadPickFolderResult(final ActivityResult result,
final String key,
final String tag) {
if (result.getResultCode() != Activity.RESULT_OK) {
return;
}
if (result.getData() == null || result.getData().getData() == null) {
showFailedDialog(R.string.general_error);
return;
}
Uri uri = result.getData().getData();
if (FilePickerActivityHelper.isOwnFileUri(context, uri)) {
uri = Uri.fromFile(Utils.getFileForUri(uri));
} else {
context.grantUriPermission(context.getPackageName(), uri,
StoredDirectoryHelper.PERMISSION_FLAGS);
}
PreferenceManager.getDefaultSharedPreferences(context).edit()
.putString(key, uri.toString()).apply();
try {
final StoredDirectoryHelper mainStorage
= new StoredDirectoryHelper(context, uri, tag);
checkSelectedDownload(mainStorage, mainStorage.findFile(filenameTmp),
filenameTmp, mimeTmp);
} catch (final IOException e) {
showFailedDialog(R.string.general_error);
}
}
/*//////////////////////////////////////////////////////////////////////////
// Listeners
//////////////////////////////////////////////////////////////////////////*/
@Override
public void onCheckedChanged(final RadioGroup group, @IdRes final int checkedId) {
if (DEBUG) {
@@ -508,6 +589,11 @@ public class DownloadDialog extends DialogFragment
public void onNothingSelected(final AdapterView<?> parent) {
}
/*//////////////////////////////////////////////////////////////////////////
// Download
//////////////////////////////////////////////////////////////////////////*/
protected void setupDownloadOptions() {
setRadioButtonsState(false);
@@ -520,7 +606,7 @@ public class DownloadDialog extends DialogFragment
dialogBinding.subtitleButton.setVisibility(isSubtitleStreamsAvailable
? View.VISIBLE : View.GONE);
prefs = PreferenceManager.getDefaultSharedPreferences(getContext());
prefs = PreferenceManager.getDefaultSharedPreferences(requireContext());
final String defaultMedia = prefs.getString(getString(R.string.last_used_download_type),
getString(R.string.last_download_type_video_key));
@@ -548,7 +634,7 @@ public class DownloadDialog extends DialogFragment
} else {
Toast.makeText(getContext(), R.string.no_streams_available_download,
Toast.LENGTH_SHORT).show();
getDialog().dismiss();
dismiss();
}
}
@@ -600,87 +686,97 @@ public class DownloadDialog extends DialogFragment
.show();
}
private void launchDirectoryPicker(final ActivityResultLauncher<Intent> launcher) {
launcher.launch(StoredDirectoryHelper.getPicker(context));
}
private void prepareSelectedDownload() {
final StoredDirectoryHelper mainStorage;
final MediaFormat format;
final String mime;
final String selectedMediaType;
// first, build the filename and get the output folder (if possible)
// later, run a very very very large file checking logic
String filename = getNameEditText().concat(".");
filenameTmp = getNameEditText().concat(".");
switch (dialogBinding.videoAudioGroup.getCheckedRadioButtonId()) {
case R.id.audio_button:
selectedMediaType = getString(R.string.last_download_type_audio_key);
mainStorage = mainStorageAudio;
format = audioStreamsAdapter.getItem(selectedAudioIndex).getFormat();
switch (format) {
case WEBMA_OPUS:
mime = "audio/ogg";
filename += "opus";
break;
default:
mime = format.mimeType;
filename += format.suffix;
break;
if (format == MediaFormat.WEBMA_OPUS) {
mimeTmp = "audio/ogg";
filenameTmp += "opus";
} else {
mimeTmp = format.mimeType;
filenameTmp += format.suffix;
}
break;
case R.id.video_button:
selectedMediaType = getString(R.string.last_download_type_video_key);
mainStorage = mainStorageVideo;
format = videoStreamsAdapter.getItem(selectedVideoIndex).getFormat();
mime = format.mimeType;
filename += format.suffix;
mimeTmp = format.mimeType;
filenameTmp += format.suffix;
break;
case R.id.subtitle_button:
selectedMediaType = getString(R.string.last_download_type_subtitle_key);
mainStorage = mainStorageVideo; // subtitle & video files go together
format = subtitleStreamsAdapter.getItem(selectedSubtitleIndex).getFormat();
mime = format.mimeType;
filename += (format == MediaFormat.TTML ? MediaFormat.SRT : format).suffix;
mimeTmp = format.mimeType;
filenameTmp += (format == MediaFormat.TTML ? MediaFormat.SRT : format).suffix;
break;
default:
throw new RuntimeException("No stream selected");
}
if (mainStorage == null || askForSavePath) {
// This part is called if with SAF preferred:
// * older android version running
// * save path not defined (via download settings)
// * the user checked the "ask where to download" option
if (!askForSavePath
&& (mainStorage == null
|| mainStorage.isDirect() == NewPipeSettings.useStorageAccessFramework(context)
|| mainStorage.isInvalidSafStorage())) {
// Pick new download folder if one of:
// - Download folder is not set
// - Download folder uses SAF while SAF is disabled
// - Download folder doesn't use SAF while SAF is enabled
// - Download folder uses SAF but the user manually revoked access to it
Toast.makeText(context, getString(R.string.no_dir_yet),
Toast.LENGTH_LONG).show();
if (!askForSavePath) {
Toast.makeText(context, getString(R.string.no_available_dir),
Toast.LENGTH_LONG).show();
}
if (NewPipeSettings.useStorageAccessFramework(context)) {
StoredFileHelper.requestSafWithFileCreation(this, REQUEST_DOWNLOAD_SAVE_AS,
filename, mime);
if (dialogBinding.videoAudioGroup.getCheckedRadioButtonId() == R.id.audio_button) {
launchDirectoryPicker(requestDownloadPickAudioFolderLauncher);
} else {
File initialSavePath;
if (dialogBinding.videoAudioGroup.getCheckedRadioButtonId() == R.id.audio_button) {
initialSavePath = NewPipeSettings.getDir(Environment.DIRECTORY_MUSIC);
} else {
initialSavePath = NewPipeSettings.getDir(Environment.DIRECTORY_MOVIES);
}
initialSavePath = new File(initialSavePath, filename);
startActivityForResult(FilePickerActivityHelper.chooseFileToSave(context,
initialSavePath.getAbsolutePath()), REQUEST_DOWNLOAD_SAVE_AS);
launchDirectoryPicker(requestDownloadPickVideoFolderLauncher);
}
return;
}
if (askForSavePath) {
final Uri initialPath;
if (NewPipeSettings.useStorageAccessFramework(context)) {
initialPath = null;
} else {
final File initialSavePath;
if (dialogBinding.videoAudioGroup.getCheckedRadioButtonId() == R.id.audio_button) {
initialSavePath = NewPipeSettings.getDir(Environment.DIRECTORY_MUSIC);
} else {
initialSavePath = NewPipeSettings.getDir(Environment.DIRECTORY_MOVIES);
}
initialPath = Uri.parse(initialSavePath.getAbsolutePath());
}
requestDownloadSaveAsLauncher.launch(StoredFileHelper.getNewPicker(context,
filenameTmp, mimeTmp, initialPath));
return;
}
// check for existing file with the same name
checkSelectedDownload(mainStorage, mainStorage.findFile(filename), filename, mime);
checkSelectedDownload(mainStorage, mainStorage.findFile(filenameTmp), filenameTmp, mimeTmp);
// remember the last media type downloaded by the user
prefs.edit()
.putString(getString(R.string.last_used_download_type), selectedMediaType)
prefs.edit().putString(getString(R.string.last_used_download_type), selectedMediaType)
.apply();
}
@@ -708,15 +804,14 @@ public class DownloadDialog extends DialogFragment
return;
}
// check if is our file
// get state of potential mission referring to the same file
final MissionState state = downloadManager.checkForExistingMission(storage);
@StringRes
final int msgBtn;
@StringRes
final int msgBody;
@StringRes final int msgBtn;
@StringRes final int msgBody;
// this switch checks if there is already a mission referring to the same file
switch (state) {
case Finished:
case Finished: // there is already a finished mission
msgBtn = R.string.overwrite;
msgBody = R.string.overwrite_finished_warning;
break;
@@ -728,7 +823,7 @@ public class DownloadDialog extends DialogFragment
msgBtn = R.string.generate_unique_name;
msgBody = R.string.download_already_running;
break;
case None:
case None: // there is no mission referring to the same file
if (mainStorage == null) {
// This part is called if:
// * using SAF on older android version
@@ -763,7 +858,7 @@ public class DownloadDialog extends DialogFragment
msgBody = R.string.overwrite_unrelated_warning;
break;
default:
return;
return; // unreachable
}
final AlertDialog.Builder askDialog = new AlertDialog.Builder(context)

View File

@@ -27,7 +27,7 @@ import org.schabi.newpipe.MainActivity;
import org.schabi.newpipe.R;
import org.schabi.newpipe.databinding.ActivityErrorBinding;
import org.schabi.newpipe.util.Localization;
import org.schabi.newpipe.util.ShareUtils;
import org.schabi.newpipe.util.external_communication.ShareUtils;
import org.schabi.newpipe.util.ThemeHelper;
import java.time.LocalDateTime;
@@ -195,7 +195,8 @@ public class ErrorActivity extends AppCompatActivity {
onBackPressed();
return true;
case R.id.menu_item_share_error:
ShareUtils.shareText(this, getString(R.string.error_report_title), buildJson());
ShareUtils.shareText(getApplicationContext(),
getString(R.string.error_report_title), buildJson());
return true;
default:
return false;
@@ -220,13 +221,10 @@ public class ErrorActivity extends AppCompatActivity {
+ getString(R.string.app_name) + " "
+ BuildConfig.VERSION_NAME)
.putExtra(Intent.EXTRA_TEXT, buildJson());
if (i.resolveActivity(getPackageManager()) != null) {
ShareUtils.openIntentInApp(context, i);
}
ShareUtils.openIntentInApp(context, i, true);
} else if (action.equals("GITHUB")) { // open the NewPipe issue page on GitHub
ShareUtils.openUrlInBrowser(this, ERROR_GITHUB_ISSUE_URL, false);
}
})
.setNegativeButton(R.string.decline, (dialog, which) -> {
// do nothing

View File

@@ -6,6 +6,7 @@ import kotlinx.android.parcel.Parcelize
import org.schabi.newpipe.R
import org.schabi.newpipe.extractor.Info
import org.schabi.newpipe.extractor.NewPipe
import org.schabi.newpipe.extractor.exceptions.AccountTerminatedException
import org.schabi.newpipe.extractor.exceptions.ContentNotAvailableException
import org.schabi.newpipe.extractor.exceptions.ContentNotSupportedException
import org.schabi.newpipe.extractor.exceptions.ExtractionException
@@ -95,6 +96,7 @@ class ErrorInfo(
action: UserAction
): Int {
return when {
throwable is AccountTerminatedException -> R.string.account_terminated
throwable is ContentNotAvailableException -> R.string.content_not_available
throwable != null && throwable.isNetworkRelated -> R.string.network_error
throwable is ContentNotSupportedException -> R.string.content_not_supported

View File

@@ -13,6 +13,8 @@ import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
import io.reactivex.rxjava3.disposables.Disposable
import org.schabi.newpipe.MainActivity
import org.schabi.newpipe.R
import org.schabi.newpipe.extractor.NewPipe
import org.schabi.newpipe.extractor.exceptions.AccountTerminatedException
import org.schabi.newpipe.extractor.exceptions.AgeRestrictedContentException
import org.schabi.newpipe.extractor.exceptions.ContentNotAvailableException
import org.schabi.newpipe.extractor.exceptions.ContentNotSupportedException
@@ -22,9 +24,11 @@ import org.schabi.newpipe.extractor.exceptions.PrivateContentException
import org.schabi.newpipe.extractor.exceptions.ReCaptchaException
import org.schabi.newpipe.extractor.exceptions.SoundCloudGoPlusContentException
import org.schabi.newpipe.extractor.exceptions.YoutubeMusicPremiumContentException
import org.schabi.newpipe.extractor.utils.Utils.isNullOrEmpty
import org.schabi.newpipe.ktx.animate
import org.schabi.newpipe.ktx.isInterruptedCaused
import org.schabi.newpipe.ktx.isNetworkRelated
import org.schabi.newpipe.util.ServiceHelper
import java.util.concurrent.TimeUnit
class ErrorPanelHelper(
@@ -35,6 +39,8 @@ class ErrorPanelHelper(
private val context: Context = rootView.context!!
private val errorPanelRoot: View = rootView.findViewById(R.id.error_panel)
private val errorTextView: TextView = errorPanelRoot.findViewById(R.id.error_message_view)
private val errorServiceInfoTextView: TextView = errorPanelRoot.findViewById(R.id.error_message_service_info_view)
private val errorServiceExplenationTextView: TextView = errorPanelRoot.findViewById(R.id.error_message_service_explenation_view)
private val errorButtonAction: Button = errorPanelRoot.findViewById(R.id.error_button_action)
private val errorButtonRetry: Button = errorPanelRoot.findViewById(R.id.error_button_retry)
@@ -70,13 +76,40 @@ class ErrorPanelHelper(
errorButtonAction.setOnClickListener(null)
}
errorTextView.setText(R.string.recaptcha_request_toast)
// additional info is only provided by AccountTerminatedException
errorServiceInfoTextView.isVisible = false
errorServiceExplenationTextView.isVisible = false
errorButtonRetry.isVisible = true
} else if (errorInfo.throwable is AccountTerminatedException) {
errorButtonRetry.isVisible = false
errorButtonAction.isVisible = false
errorTextView.setText(R.string.account_terminated)
if (!isNullOrEmpty((errorInfo.throwable as AccountTerminatedException).message)) {
errorServiceInfoTextView.setText(
context.resources.getString(
R.string.service_provides_reason,
NewPipe.getNameOfService(ServiceHelper.getSelectedServiceId(context))
)
)
errorServiceExplenationTextView.setText(
(errorInfo.throwable as AccountTerminatedException).message
)
errorServiceInfoTextView.isVisible = true
errorServiceExplenationTextView.isVisible = true
} else {
errorServiceInfoTextView.isVisible = false
errorServiceExplenationTextView.isVisible = false
}
} else {
errorButtonAction.setText(R.string.error_snackbar_action)
errorButtonAction.setOnClickListener {
ErrorActivity.reportError(context, errorInfo)
}
// additional info is only provided by AccountTerminatedException
errorServiceInfoTextView.isVisible = false
errorServiceExplenationTextView.isVisible = false
// hide retry button by default, then show only if not unavailable/unsupported content
errorButtonRetry.isVisible = false
errorTextView.setText(

View File

@@ -162,6 +162,9 @@ public class ReCaptchaActivity extends AppCompatActivity {
setResult(RESULT_OK);
}
// Navigate to blank page (unloads youtube to prevent background playback)
recaptchaBinding.reCaptchaWebView.loadUrl("about:blank");
final Intent intent = new Intent(this, org.schabi.newpipe.MainActivity.class);
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
NavUtils.navigateUpTo(this, intent);

View File

@@ -130,7 +130,7 @@ public class MainFragment extends BaseFragment implements TabLayout.OnTabSelecte
Log.d(TAG, "onCreateOptionsMenu() called with: "
+ "menu = [" + menu + "], inflater = [" + inflater + "]");
}
inflater.inflate(R.menu.main_fragment_menu, menu);
inflater.inflate(R.menu.menu_main_fragment, menu);
final ActionBar supportActionBar = activity.getSupportActionBar();
if (supportActionBar != null) {

View File

@@ -23,15 +23,15 @@ import org.schabi.newpipe.extractor.stream.Description;
import org.schabi.newpipe.extractor.stream.StreamInfo;
import org.schabi.newpipe.util.Localization;
import org.schabi.newpipe.util.NavigationHelper;
import org.schabi.newpipe.util.ShareUtils;
import org.schabi.newpipe.util.TextLinkifier;
import org.schabi.newpipe.util.external_communication.ShareUtils;
import org.schabi.newpipe.util.external_communication.TextLinkifier;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import icepick.State;
import io.reactivex.rxjava3.disposables.Disposable;
import io.reactivex.rxjava3.disposables.CompositeDisposable;
import static android.text.TextUtils.isEmpty;
import static org.schabi.newpipe.extractor.stream.StreamExtractor.NO_AGE_LIMIT;
@@ -41,8 +41,7 @@ public class DescriptionFragment extends BaseFragment {
@State
StreamInfo streamInfo = null;
@Nullable
Disposable descriptionDisposable = null;
final CompositeDisposable descriptionDisposables = new CompositeDisposable();
FragmentDescriptionBinding binding;
public DescriptionFragment() {
@@ -67,10 +66,8 @@ public class DescriptionFragment extends BaseFragment {
@Override
public void onDestroy() {
descriptionDisposables.clear();
super.onDestroy();
if (descriptionDisposable != null) {
descriptionDisposable.dispose();
}
}
@@ -133,17 +130,17 @@ public class DescriptionFragment extends BaseFragment {
final Description description = streamInfo.getDescription();
switch (description.getType()) {
case Description.HTML:
descriptionDisposable = TextLinkifier.createLinksFromHtmlBlock(requireContext(),
description.getContent(), binding.detailDescriptionView,
HtmlCompat.FROM_HTML_MODE_LEGACY);
TextLinkifier.createLinksFromHtmlBlock(binding.detailDescriptionView,
description.getContent(), HtmlCompat.FROM_HTML_MODE_LEGACY, streamInfo,
descriptionDisposables);
break;
case Description.MARKDOWN:
descriptionDisposable = TextLinkifier.createLinksFromMarkdownText(requireContext(),
description.getContent(), binding.detailDescriptionView);
TextLinkifier.createLinksFromMarkdownText(binding.detailDescriptionView,
description.getContent(), streamInfo, descriptionDisposables);
break;
case Description.PLAIN_TEXT: default:
descriptionDisposable = TextLinkifier.createLinksFromPlainText(requireContext(),
description.getContent(), binding.detailDescriptionView);
TextLinkifier.createLinksFromPlainText(binding.detailDescriptionView,
description.getContent(), streamInfo, descriptionDisposables);
break;
}
}
@@ -154,8 +151,6 @@ public class DescriptionFragment extends BaseFragment {
addMetadataItem(inflater, layout, false,
R.string.metadata_category, streamInfo.getCategory());
addTagsMetadataItem(inflater, layout);
addMetadataItem(inflater, layout, false,
R.string.metadata_licence, streamInfo.getLicence());
@@ -177,6 +172,8 @@ public class DescriptionFragment extends BaseFragment {
R.string.metadata_host, streamInfo.getHost());
addMetadataItem(inflater, layout, true,
R.string.metadata_thumbnail_url, streamInfo.getThumbnailUrl());
addTagsMetadataItem(inflater, layout);
}
private void addMetadataItem(final LayoutInflater inflater,
@@ -198,8 +195,8 @@ public class DescriptionFragment extends BaseFragment {
});
if (linkifyContent) {
TextLinkifier.createLinksFromPlainText(requireContext(),
content, itemBinding.metadataContentView);
TextLinkifier.createLinksFromPlainText(itemBinding.metadataContentView, content, null,
descriptionDisposables);
} else {
itemBinding.metadataContentView.setText(content);
}

View File

@@ -91,12 +91,12 @@ import org.schabi.newpipe.util.Constants;
import org.schabi.newpipe.util.DeviceUtils;
import org.schabi.newpipe.util.ExtractorHelper;
import org.schabi.newpipe.util.ImageDisplayConstants;
import org.schabi.newpipe.util.KoreUtil;
import org.schabi.newpipe.util.external_communication.KoreUtils;
import org.schabi.newpipe.util.ListHelper;
import org.schabi.newpipe.util.Localization;
import org.schabi.newpipe.util.NavigationHelper;
import org.schabi.newpipe.util.PermissionHelper;
import org.schabi.newpipe.util.ShareUtils;
import org.schabi.newpipe.util.external_communication.ShareUtils;
import org.schabi.newpipe.util.ThemeHelper;
import java.util.ArrayList;
@@ -201,6 +201,7 @@ public final class VideoDetailFragment
@Nullable
private MainPlayer playerService;
private Player player;
private PlayerHolder playerHolder = PlayerHolder.getInstance();
/*//////////////////////////////////////////////////////////////////////////
// Service management
@@ -304,7 +305,8 @@ public final class VideoDetailFragment
@Override
public View onCreateView(@NonNull final LayoutInflater inflater, final ViewGroup container,
final Bundle savedInstanceState) {
return inflater.inflate(R.layout.fragment_video_detail, container, false);
binding = FragmentVideoDetailBinding.inflate(inflater, container, false);
return binding.getRoot();
}
@Override
@@ -355,14 +357,13 @@ public final class VideoDetailFragment
@Override
public void onDestroy() {
super.onDestroy();
binding = null;
// Stop the service when user leaves the app with double back press
// if video player is selected. Otherwise unbind
if (activity.isFinishing() && player != null && player.videoPlayerSelected()) {
PlayerHolder.stopService(App.getApp());
if (activity.isFinishing() && isPlayerAvailable() && player.videoPlayerSelected()) {
playerHolder.stopService();
} else {
PlayerHolder.removeListener();
playerHolder.setListener(null);
}
PreferenceManager.getDefaultSharedPreferences(activity)
@@ -388,6 +389,12 @@ public final class VideoDetailFragment
}
}
@Override
public void onDestroyView() {
super.onDestroyView();
binding = null;
}
@Override
public void onActivityResult(final int requestCode, final int resultCode, final Intent data) {
super.onActivityResult(requestCode, resultCode, data);
@@ -454,8 +461,8 @@ public final class VideoDetailFragment
break;
case R.id.detail_controls_share:
if (currentInfo != null) {
ShareUtils.shareText(requireContext(),
currentInfo.getName(), currentInfo.getUrl());
ShareUtils.shareText(requireContext(), currentInfo.getName(),
currentInfo.getUrl(), currentInfo.getThumbnailUrl());
}
break;
case R.id.detail_controls_open_in_browser:
@@ -472,7 +479,7 @@ public final class VideoDetailFragment
if (DEBUG) {
Log.i(TAG, "Failed to start kore", e);
}
KoreUtil.showInstallKoreDialog(requireContext());
KoreUtils.showInstallKoreDialog(requireContext());
}
}
break;
@@ -512,7 +519,7 @@ public final class VideoDetailFragment
openVideoPlayer();
}
setOverlayPlayPauseImage(player != null && player.isPlaying());
setOverlayPlayPauseImage(isPlayerAvailable() && player.isPlaying());
break;
case R.id.overlay_close_button:
bottomSheetBehavior.setState(BottomSheetBehavior.STATE_HIDDEN);
@@ -586,10 +593,9 @@ public final class VideoDetailFragment
// Init
//////////////////////////////////////////////////////////////////////////*/
@Override
@Override // called from onViewCreated in {@link BaseFragment#onViewCreated}
protected void initViews(final View rootView, final Bundle savedInstanceState) {
super.initViews(rootView, savedInstanceState);
binding = FragmentVideoDetailBinding.bind(rootView);
pageAdapter = new TabAdapter(getChildFragmentManager());
binding.viewPager.setAdapter(pageAdapter);
@@ -631,7 +637,7 @@ public final class VideoDetailFragment
binding.detailControlsShare.setOnClickListener(this);
binding.detailControlsOpenInBrowser.setOnClickListener(this);
binding.detailControlsPlayWithKodi.setOnClickListener(this);
binding.detailControlsPlayWithKodi.setVisibility(KoreUtil.shouldShowPlayWithKodi(
binding.detailControlsPlayWithKodi.setVisibility(KoreUtils.shouldShowPlayWithKodi(
requireContext(), serviceId) ? View.VISIBLE : View.GONE);
binding.overlayThumbnail.setOnClickListener(this);
@@ -655,10 +661,10 @@ public final class VideoDetailFragment
});
setupBottomPlayer();
if (!PlayerHolder.bound) {
if (!playerHolder.bound) {
setHeightThumbnail();
} else {
PlayerHolder.startService(App.getApp(), false, this);
playerHolder.startService(false, this);
}
}
@@ -721,7 +727,7 @@ public final class VideoDetailFragment
@Override
public boolean onKeyDown(final int keyCode) {
return player != null && player.onKeyDown(keyCode);
return isPlayerAvailable() && player.onKeyDown(keyCode);
}
@Override
@@ -731,7 +737,7 @@ public final class VideoDetailFragment
}
// If we are in fullscreen mode just exit from it via first back press
if (player != null && player.isFullscreen()) {
if (isPlayerAvailable() && player.isFullscreen()) {
if (!DeviceUtils.isTablet(activity)) {
player.pause();
}
@@ -741,7 +747,7 @@ public final class VideoDetailFragment
}
// If we have something in history of played items we replay it here
if (player != null
if (isPlayerAvailable()
&& player.getPlayQueue() != null
&& player.videoPlayerSelected()
&& player.getPlayQueue().previous()) {
@@ -778,7 +784,7 @@ public final class VideoDetailFragment
final PlayQueueItem playQueueItem = item.getPlayQueue().getItem();
// Update title, url, uploader from the last item in the stack (it's current now)
final boolean isPlayerStopped = player == null || player.isStopped();
final boolean isPlayerStopped = !isPlayerAvailable() || player.isStopped();
if (playQueueItem != null && isPlayerStopped) {
updateOverlayData(playQueueItem.getTitle(),
playQueueItem.getUploader(), playQueueItem.getThumbnailUrl());
@@ -806,8 +812,8 @@ public final class VideoDetailFragment
@Nullable final String newUrl,
@NonNull final String newTitle,
@Nullable final PlayQueue newQueue) {
if (player != null && newQueue != null && playQueue != null
&& !Objects.equals(newQueue.getItem(), playQueue.getItem())) {
if (isPlayerAvailable() && newQueue != null && playQueue != null
&& playQueue.getItem() != null && !playQueue.getItem().getUrl().equals(newUrl)) {
// Preloading can be disabled since playback is surely being replaced.
player.disablePreloadingOfCurrentTrack();
}
@@ -982,7 +988,7 @@ public final class VideoDetailFragment
.replace(R.id.relatedItemsLayout, RelatedItemsFragment.getInstance(info))
.commitAllowingStateLoss();
binding.relatedItemsLayout.setVisibility(
player != null && player.isFullscreen() ? View.GONE : View.VISIBLE);
isPlayerAvailable() && player.isFullscreen() ? View.GONE : View.VISIBLE);
}
}
@@ -1059,6 +1065,14 @@ public final class VideoDetailFragment
// Play Utils
//////////////////////////////////////////////////////////////////////////*/
private void toggleFullscreenIfInFullscreenMode() {
// If a user watched video inside fullscreen mode and than chose another player
// return to non-fullscreen mode
if (isPlayerAvailable() && player.isFullscreen()) {
player.toggleFullscreen();
}
}
private void openBackgroundPlayer(final boolean append) {
final AudioStream audioStream = currentInfo.getAudioStreams()
.get(ListHelper.getDefaultAudioFormat(activity, currentInfo.getAudioStreams()));
@@ -1067,11 +1081,7 @@ public final class VideoDetailFragment
.getDefaultSharedPreferences(activity)
.getBoolean(activity.getString(R.string.use_external_audio_player_key), false);
// If a user watched video inside fullscreen mode and than chose another player
// return to non-fullscreen mode
if (player != null && player.isFullscreen()) {
player.toggleFullscreen();
}
toggleFullscreenIfInFullscreenMode();
if (!useExternalAudioPlayer) {
openNormalBackgroundPlayer(append);
@@ -1087,15 +1097,11 @@ public final class VideoDetailFragment
}
// See UI changes while remote playQueue changes
if (player == null) {
PlayerHolder.startService(App.getApp(), false, this);
if (!isPlayerAvailable()) {
playerHolder.startService(false, this);
}
// If a user watched video inside fullscreen mode and than chose another player
// return to non-fullscreen mode
if (player != null && player.isFullscreen()) {
player.toggleFullscreen();
}
toggleFullscreenIfInFullscreenMode();
final PlayQueue queue = setupPlayQueueForIntent(append);
if (append) {
@@ -1117,8 +1123,8 @@ public final class VideoDetailFragment
private void openNormalBackgroundPlayer(final boolean append) {
// See UI changes while remote playQueue changes
if (player == null) {
PlayerHolder.startService(App.getApp(), false, this);
if (!isPlayerAvailable()) {
playerHolder.startService(false, this);
}
final PlayQueue queue = setupPlayQueueForIntent(append);
@@ -1131,8 +1137,8 @@ public final class VideoDetailFragment
}
private void openMainPlayer() {
if (playerService == null) {
PlayerHolder.startService(App.getApp(), autoPlayEnabled, this);
if (!isPlayerServiceAvailable()) {
playerHolder.startService(autoPlayEnabled, this);
return;
}
if (currentInfo == null) {
@@ -1150,11 +1156,11 @@ public final class VideoDetailFragment
final Intent playerIntent = NavigationHelper
.getPlayerIntent(requireContext(), MainPlayer.class, queue, true, autoPlayEnabled);
activity.startService(playerIntent);
ContextCompat.startForegroundService(activity, playerIntent);
}
private void hideMainPlayer() {
if (playerService == null
if (!isPlayerServiceAvailable()
|| playerService.getView() == null
|| !player.videoPlayerSelected()) {
return;
@@ -1211,13 +1217,13 @@ public final class VideoDetailFragment
private boolean isAutoplayEnabled() {
return autoPlayEnabled
&& !isExternalPlayerEnabled()
&& (player == null || player.videoPlayerSelected())
&& (!isPlayerAvailable() || player.videoPlayerSelected())
&& bottomSheetState != BottomSheetBehavior.STATE_HIDDEN
&& PlayerHelper.isAutoplayAllowedByUser(requireContext());
}
private void addVideoPlayerView() {
if (player == null || getView() == null) {
if (!isPlayerAvailable() || getView() == null) {
return;
}
@@ -1277,7 +1283,7 @@ public final class VideoDetailFragment
final boolean isPortrait = metrics.heightPixels > metrics.widthPixels;
requireView().getViewTreeObserver().removeOnPreDrawListener(preDrawListener);
if (player != null && player.isFullscreen()) {
if (isPlayerAvailable() && player.isFullscreen()) {
final int height = (isInMultiWindow()
? requireView()
: activity.getWindow().getDecorView()).getHeight();
@@ -1300,7 +1306,7 @@ public final class VideoDetailFragment
new FrameLayout.LayoutParams(
RelativeLayout.LayoutParams.MATCH_PARENT, newHeight));
binding.detailThumbnailImageView.setMinimumHeight(newHeight);
if (player != null) {
if (isPlayerAvailable()) {
final int maxHeight = (int) (metrics.heightPixels * MAX_PLAYER_HEIGHT);
player.getSurfaceView()
.setHeights(newHeight, player.isFullscreen() ? newHeight : maxHeight);
@@ -1368,9 +1374,9 @@ public final class VideoDetailFragment
bottomSheetBehavior.setState(BottomSheetBehavior.STATE_COLLAPSED);
}
// Rebound to the service if it was closed via notification or mini player
if (!PlayerHolder.bound) {
PlayerHolder.startService(
App.getApp(), false, VideoDetailFragment.this);
if (!playerHolder.bound) {
playerHolder.startService(
false, VideoDetailFragment.this);
}
break;
}
@@ -1389,13 +1395,12 @@ public final class VideoDetailFragment
//////////////////////////////////////////////////////////////////////////*/
private void restoreDefaultOrientation() {
if (player == null || !player.videoPlayerSelected() || activity == null) {
if (!isPlayerAvailable() || !player.videoPlayerSelected() || activity == null) {
return;
}
if (player != null && player.isFullscreen()) {
player.toggleFullscreen();
}
toggleFullscreenIfInFullscreenMode();
// This will show systemUI and pause the player.
// User can tap on Play button and video will be in fullscreen mode again
// Note for tablet: trying to avoid orientation changes since it's not easy
@@ -1435,7 +1440,7 @@ public final class VideoDetailFragment
if (binding.relatedItemsLayout != null) {
if (showRelatedItems) {
binding.relatedItemsLayout.setVisibility(
player != null && player.isFullscreen() ? View.GONE : View.INVISIBLE);
isPlayerAvailable() && player.isFullscreen() ? View.GONE : View.INVISIBLE);
} else {
binding.relatedItemsLayout.setVisibility(View.GONE);
}
@@ -1546,10 +1551,10 @@ public final class VideoDetailFragment
.getDefaultResolutionIndex(activity, sortedVideoStreams);
updateProgressInfo(info);
initThumbnailViews(info);
disposables.add(showMetaInfoInTextView(info.getMetaInfo(), binding.detailMetaInfoTextView,
binding.detailMetaInfoSeparator));
showMetaInfoInTextView(info.getMetaInfo(), binding.detailMetaInfoTextView,
binding.detailMetaInfoSeparator, disposables);
if (player == null || player.isStopped()) {
if (!isPlayerAvailable() || player.isStopped()) {
updateOverlayData(info.getName(), info.getUploaderName(), info.getThumbnailUrl());
}
@@ -1669,7 +1674,7 @@ public final class VideoDetailFragment
.onErrorComplete()
.observeOn(AndroidSchedulers.mainThread())
.subscribe(state -> {
showPlaybackProgress(state.getProgressTime(), info.getDuration() * 1000);
showPlaybackProgress(state.getProgressMillis(), info.getDuration() * 1000);
animate(binding.positionView, true, 500);
animate(binding.detailPositionView, true, 500);
}, e -> {
@@ -1812,9 +1817,7 @@ public final class VideoDetailFragment
if (error.type == ExoPlaybackException.TYPE_SOURCE
|| error.type == ExoPlaybackException.TYPE_UNEXPECTED) {
// Properly exit from fullscreen
if (playerService != null && player.isFullscreen()) {
player.toggleFullscreen();
}
toggleFullscreenIfInFullscreenMode();
hideMainPlayer();
}
}
@@ -1832,7 +1835,9 @@ public final class VideoDetailFragment
@Override
public void onFullscreenStateChanged(final boolean fullscreen) {
setupBrightness();
if (playerService.getView() == null || player.getParentActivity() == null) {
if (!isPlayerAndPlayerServiceAvailable()
|| playerService.getView() == null
|| player.getParentActivity() == null) {
return;
}
@@ -1955,7 +1960,7 @@ public final class VideoDetailFragment
activity.getWindow().getDecorView().setSystemUiVisibility(visibility);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP
&& (isInMultiWindow() || (player != null && player.isFullscreen()))) {
&& (isInMultiWindow() || (isPlayerAvailable() && player.isFullscreen()))) {
activity.getWindow().setStatusBarColor(Color.TRANSPARENT);
activity.getWindow().setNavigationBarColor(Color.TRANSPARENT);
}
@@ -1964,7 +1969,7 @@ public final class VideoDetailFragment
// Listener implementation
public void hideSystemUiIfNeeded() {
if (player != null
if (isPlayerAvailable()
&& player.isFullscreen()
&& bottomSheetBehavior.getState() == BottomSheetBehavior.STATE_EXPANDED) {
hideSystemUi();
@@ -1972,7 +1977,7 @@ public final class VideoDetailFragment
}
private boolean playerIsNotStopped() {
return player != null && !player.isStopped();
return isPlayerAvailable() && !player.isStopped();
}
private void restoreDefaultBrightness() {
@@ -1993,7 +1998,7 @@ public final class VideoDetailFragment
}
final WindowManager.LayoutParams lp = activity.getWindow().getAttributes();
if (player == null
if (!isPlayerAvailable()
|| !player.videoPlayerSelected()
|| !player.isFullscreen()
|| bottomSheetState != BottomSheetBehavior.STATE_EXPANDED) {
@@ -2059,7 +2064,7 @@ public final class VideoDetailFragment
}
private void replaceQueueIfUserConfirms(final Runnable onAllow) {
@Nullable final PlayQueue activeQueue = player == null ? null : player.getPlayQueue();
@Nullable final PlayQueue activeQueue = isPlayerAvailable() ? player.getPlayQueue() : null;
// Player will have STATE_IDLE when a user pressed back button
if (isClearingQueueConfirmationRequired(activity)
@@ -2115,7 +2120,7 @@ public final class VideoDetailFragment
if (currentWorker != null) {
currentWorker.dispose();
}
PlayerHolder.stopService(App.getApp());
playerHolder.stopService();
setInitialData(0, null, "", null);
currentInfo = null;
updateOverlayData(null, null, null);
@@ -2219,7 +2224,7 @@ public final class VideoDetailFragment
hideSystemUiIfNeeded();
// Conditions when the player should be expanded to fullscreen
if (isLandscape()
&& player != null
&& isPlayerAvailable()
&& player.isPlaying()
&& !player.isFullscreen()
&& !DeviceUtils.isTablet(activity)
@@ -2236,17 +2241,17 @@ public final class VideoDetailFragment
// Re-enable clicks
setOverlayElementsClickable(true);
if (player != null) {
if (isPlayerAvailable()) {
player.closeItemsList();
}
setOverlayLook(binding.appBarLayout, behavior, 0);
break;
case BottomSheetBehavior.STATE_DRAGGING:
case BottomSheetBehavior.STATE_SETTLING:
if (player != null && player.isFullscreen()) {
if (isPlayerAvailable() && player.isFullscreen()) {
showSystemUi();
}
if (player != null && player.isControlsVisible()) {
if (isPlayerAvailable() && player.isControlsVisible()) {
player.hideControls(0, 0);
}
break;
@@ -2310,4 +2315,17 @@ public final class VideoDetailFragment
binding.overlayPlayPauseButton.setClickable(enable);
binding.overlayCloseButton.setClickable(enable);
}
// helpers to check the state of player and playerService
boolean isPlayerAvailable() {
return (player != null);
}
boolean isPlayerServiceAvailable() {
return (playerService != null);
}
boolean isPlayerAndPlayerServiceAvailable() {
return (player != null && playerService != null);
}
}

View File

@@ -33,7 +33,7 @@ import org.schabi.newpipe.fragments.OnScrollBelowItemsListener;
import org.schabi.newpipe.info_list.InfoItemDialog;
import org.schabi.newpipe.info_list.InfoListAdapter;
import org.schabi.newpipe.player.helper.PlayerHolder;
import org.schabi.newpipe.util.KoreUtil;
import org.schabi.newpipe.util.external_communication.KoreUtils;
import org.schabi.newpipe.util.NavigationHelper;
import org.schabi.newpipe.util.OnClickGesture;
import org.schabi.newpipe.util.StateSaver;
@@ -353,7 +353,7 @@ public abstract class BaseListFragment<I, N> extends BaseStateFragment<I>
final List<StreamDialogEntry> entries = new ArrayList<>();
if (PlayerHolder.getType() != null) {
if (PlayerHolder.getInstance().getType() != null) {
entries.add(StreamDialogEntry.enqueue);
}
if (item.getStreamType() == StreamType.AUDIO_STREAM) {
@@ -371,7 +371,7 @@ public abstract class BaseListFragment<I, N> extends BaseStateFragment<I>
));
}
entries.add(StreamDialogEntry.open_in_browser);
if (KoreUtil.shouldShowPlayWithKodi(context, item.getServiceId())) {
if (KoreUtils.shouldShowPlayWithKodi(context, item.getServiceId())) {
entries.add(StreamDialogEntry.play_with_kodi);
}
if (!isNullOrEmpty(item.getUploaderUrl())) {
@@ -389,7 +389,8 @@ public abstract class BaseListFragment<I, N> extends BaseStateFragment<I>
//////////////////////////////////////////////////////////////////////////*/
@Override
public void onCreateOptionsMenu(final Menu menu, final MenuInflater inflater) {
public void onCreateOptionsMenu(@NonNull final Menu menu,
@NonNull final MenuInflater inflater) {
if (DEBUG) {
Log.d(TAG, "onCreateOptionsMenu() called with: "
+ "menu = [" + menu + "], inflater = [" + inflater + "]");

View File

@@ -43,7 +43,7 @@ import org.schabi.newpipe.util.ExtractorHelper;
import org.schabi.newpipe.util.ImageDisplayConstants;
import org.schabi.newpipe.util.Localization;
import org.schabi.newpipe.util.NavigationHelper;
import org.schabi.newpipe.util.ShareUtils;
import org.schabi.newpipe.util.external_communication.ShareUtils;
import org.schabi.newpipe.util.ThemeHelper;
import java.util.ArrayList;
@@ -164,7 +164,8 @@ public class ChannelFragment extends BaseListInfoFragment<ChannelInfo>
//////////////////////////////////////////////////////////////////////////*/
@Override
public void onCreateOptionsMenu(final Menu menu, final MenuInflater inflater) {
public void onCreateOptionsMenu(@NonNull final Menu menu,
@NonNull final MenuInflater inflater) {
super.onCreateOptionsMenu(menu, inflater);
final ActionBar supportActionBar = activity.getSupportActionBar();
if (useAsFrontPage && supportActionBar != null) {
@@ -203,7 +204,8 @@ public class ChannelFragment extends BaseListInfoFragment<ChannelInfo>
break;
case R.id.menu_item_share:
if (currentInfo != null) {
ShareUtils.shareText(requireContext(), name, currentInfo.getOriginalUrl());
ShareUtils.shareText(requireContext(), name, currentInfo.getOriginalUrl(),
currentInfo.getAvatarUrl());
}
break;
default:

View File

@@ -6,6 +6,7 @@ import android.view.Menu;
import android.view.MenuInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
@@ -24,6 +25,8 @@ import io.reactivex.rxjava3.disposables.CompositeDisposable;
public class CommentsFragment extends BaseListInfoFragment<CommentsInfo> {
private final CompositeDisposable disposables = new CompositeDisposable();
private TextView emptyStateDesc;
public static CommentsFragment getInstance(final int serviceId, final String url,
final String name) {
final CommentsFragment instance = new CommentsFragment();
@@ -35,6 +38,13 @@ public class CommentsFragment extends BaseListInfoFragment<CommentsInfo> {
super(UserAction.REQUESTED_COMMENTS);
}
@Override
protected void initViews(final View rootView, final Bundle savedInstanceState) {
super.initViews(rootView, savedInstanceState);
emptyStateDesc = rootView.findViewById(R.id.empty_state_desc);
}
/*//////////////////////////////////////////////////////////////////////////
// LifeCycle
//////////////////////////////////////////////////////////////////////////*/
@@ -73,6 +83,12 @@ public class CommentsFragment extends BaseListInfoFragment<CommentsInfo> {
@Override
public void handleResult(@NonNull final CommentsInfo result) {
super.handleResult(result);
emptyStateDesc.setText(
result.isCommentsDisabled()
? R.string.comments_are_disabled
: R.string.no_comments);
ViewUtils.slideUp(requireView(), 120, 150, 0.06f);
disposables.clear();
}
@@ -85,7 +101,8 @@ public class CommentsFragment extends BaseListInfoFragment<CommentsInfo> {
public void setTitle(final String title) { }
@Override
public void onCreateOptionsMenu(final Menu menu, final MenuInflater inflater) { }
public void onCreateOptionsMenu(@NonNull final Menu menu,
@NonNull final MenuInflater inflater) { }
@Override
protected boolean isGridLayout() {

View File

@@ -131,7 +131,8 @@ public class KioskFragment extends BaseListInfoFragment<KioskInfo> {
//////////////////////////////////////////////////////////////////////////*/
@Override
public void onCreateOptionsMenu(final Menu menu, final MenuInflater inflater) {
public void onCreateOptionsMenu(@NonNull final Menu menu,
@NonNull final MenuInflater inflater) {
super.onCreateOptionsMenu(menu, inflater);
final ActionBar supportActionBar = activity.getSupportActionBar();
if (supportActionBar != null && useAsFrontPage) {

View File

@@ -42,10 +42,10 @@ import org.schabi.newpipe.player.playqueue.PlayQueue;
import org.schabi.newpipe.player.playqueue.PlaylistPlayQueue;
import org.schabi.newpipe.util.ExtractorHelper;
import org.schabi.newpipe.util.ImageDisplayConstants;
import org.schabi.newpipe.util.KoreUtil;
import org.schabi.newpipe.util.external_communication.KoreUtils;
import org.schabi.newpipe.util.Localization;
import org.schabi.newpipe.util.NavigationHelper;
import org.schabi.newpipe.util.ShareUtils;
import org.schabi.newpipe.util.external_communication.ShareUtils;
import org.schabi.newpipe.util.StreamDialogEntry;
import java.util.ArrayList;
@@ -144,7 +144,7 @@ public class PlaylistFragment extends BaseListInfoFragment<PlaylistInfo> {
final ArrayList<StreamDialogEntry> entries = new ArrayList<>();
if (PlayerHolder.getType() != null) {
if (PlayerHolder.getInstance().getType() != null) {
entries.add(StreamDialogEntry.enqueue);
}
if (item.getStreamType() == StreamType.AUDIO_STREAM) {
@@ -162,7 +162,7 @@ public class PlaylistFragment extends BaseListInfoFragment<PlaylistInfo> {
));
}
entries.add(StreamDialogEntry.open_in_browser);
if (KoreUtil.shouldShowPlayWithKodi(context, item.getServiceId())) {
if (KoreUtils.shouldShowPlayWithKodi(context, item.getServiceId())) {
entries.add(StreamDialogEntry.play_with_kodi);
}
@@ -181,7 +181,8 @@ public class PlaylistFragment extends BaseListInfoFragment<PlaylistInfo> {
}
@Override
public void onCreateOptionsMenu(final Menu menu, final MenuInflater inflater) {
public void onCreateOptionsMenu(@NonNull final Menu menu,
@NonNull final MenuInflater inflater) {
if (DEBUG) {
Log.d(TAG, "onCreateOptionsMenu() called with: "
+ "menu = [" + menu + "], inflater = [" + inflater + "]");
@@ -251,7 +252,7 @@ public class PlaylistFragment extends BaseListInfoFragment<PlaylistInfo> {
ShareUtils.openUrlInBrowser(requireContext(), url);
break;
case R.id.menu_item_share:
ShareUtils.shareText(requireContext(), name, url);
ShareUtils.shareText(requireContext(), name, url, currentInfo.getThumbnailUrl());
break;
case R.id.menu_item_bookmark:
onBookmarkClicked();

View File

@@ -227,6 +227,25 @@ public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.I
initSearchListeners();
}
private void updateService() {
try {
service = NewPipe.getService(serviceId);
} catch (final Exception e) {
ErrorActivity.reportUiErrorInSnackbar(this,
"Getting service for id " + serviceId, e);
}
}
@Override
public void onStart() {
if (DEBUG) {
Log.d(TAG, "onStart() called");
}
super.onStart();
updateService();
}
@Override
public void onPause() {
super.onPause();
@@ -250,13 +269,6 @@ public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.I
}
super.onResume();
try {
service = NewPipe.getService(serviceId);
} catch (final Exception e) {
ErrorActivity.reportUiErrorInSnackbar(this,
"Getting service for id " + serviceId, e);
}
if (suggestionDisposable == null || suggestionDisposable.isDisposed()) {
initSuggestionObserver();
}
@@ -278,8 +290,9 @@ public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.I
handleSearchSuggestion();
disposables.add(showMetaInfoInTextView(metaInfo == null ? null : Arrays.asList(metaInfo),
searchBinding.searchMetaInfoTextView, searchBinding.searchMetaInfoSeparator));
showMetaInfoInTextView(metaInfo == null ? null : Arrays.asList(metaInfo),
searchBinding.searchMetaInfoTextView, searchBinding.searchMetaInfoSeparator,
disposables);
if (TextUtils.isEmpty(searchString) || wasSearchFocused) {
showKeyboardSearch();
@@ -412,7 +425,8 @@ public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.I
//////////////////////////////////////////////////////////////////////////*/
@Override
public void onCreateOptionsMenu(final Menu menu, final MenuInflater inflater) {
public void onCreateOptionsMenu(@NonNull final Menu menu,
@NonNull final MenuInflater inflater) {
super.onCreateOptionsMenu(menu, inflater);
final ActionBar supportActionBar = activity.getSupportActionBar();
@@ -426,6 +440,12 @@ public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.I
int itemId = 0;
boolean isFirstItem = true;
final Context c = getContext();
if (service == null) {
Log.w(TAG, "onCreateOptionsMenu() called with null service");
updateService();
}
for (final String filter : service.getSearchQHFactory().getAvailableContentFilter()) {
if (filter.equals(YoutubeSearchQueryHandlerFactory.MUSIC_SONGS)) {
final MenuItem musicItem = menu.add(2,
@@ -841,7 +861,7 @@ public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.I
infoListAdapter.clearStreamItemList();
hideSuggestionsPanel();
showMetaInfoInTextView(null, searchBinding.searchMetaInfoTextView,
searchBinding.searchMetaInfoSeparator);
searchBinding.searchMetaInfoSeparator, disposables);
hideKeyboardSearch();
disposables.add(historyRecordManager.onSearched(serviceId, theSearchString)
@@ -986,8 +1006,8 @@ public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.I
// List<MetaInfo> cannot be bundled without creating some containers
metaInfo = new MetaInfo[result.getMetaInfo().size()];
metaInfo = result.getMetaInfo().toArray(metaInfo);
disposables.add(showMetaInfoInTextView(result.getMetaInfo(),
searchBinding.searchMetaInfoTextView, searchBinding.searchMetaInfoSeparator));
showMetaInfoInTextView(result.getMetaInfo(), searchBinding.searchMetaInfoTextView,
searchBinding.searchMetaInfoSeparator, disposables);
handleSearchSuggestion();

View File

@@ -140,7 +140,8 @@ public class RelatedItemsFragment extends BaseListInfoFragment<RelatedItemInfo>
}
@Override
public void onCreateOptionsMenu(final Menu menu, final MenuInflater inflater) {
public void onCreateOptionsMenu(@NonNull final Menu menu,
@NonNull final MenuInflater inflater) {
}
private void setInitialData(final StreamInfo info) {

View File

@@ -1,6 +1,8 @@
package org.schabi.newpipe.info_list.holder;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageView;
import android.widget.TextView;
import org.schabi.newpipe.R;
@@ -31,11 +33,13 @@ import org.schabi.newpipe.local.history.HistoryRecordManager;
public class CommentsInfoItemHolder extends CommentsMiniInfoItemHolder {
public final TextView itemTitleView;
private final ImageView itemHeartView;
public CommentsInfoItemHolder(final InfoItemBuilder infoItemBuilder, final ViewGroup parent) {
super(infoItemBuilder, R.layout.list_comments_item, parent);
itemTitleView = itemView.findViewById(R.id.itemTitleView);
itemHeartView = itemView.findViewById(R.id.detail_heart_image_view);
}
@Override
@@ -49,5 +53,7 @@ public class CommentsInfoItemHolder extends CommentsMiniInfoItemHolder {
final CommentsInfoItem item = (CommentsInfoItem) infoItem;
itemTitleView.setText(item.getUploaderName());
itemHeartView.setVisibility(item.isHeartedByUploader() ? View.VISIBLE : View.GONE);
}
}

View File

@@ -24,7 +24,7 @@ import org.schabi.newpipe.util.DeviceUtils;
import org.schabi.newpipe.util.ImageDisplayConstants;
import org.schabi.newpipe.util.Localization;
import org.schabi.newpipe.util.NavigationHelper;
import org.schabi.newpipe.util.ShareUtils;
import org.schabi.newpipe.util.external_communication.ShareUtils;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

View File

@@ -66,7 +66,7 @@ public class StreamMiniInfoItemHolder extends InfoItemHolder {
itemProgressView.setVisibility(View.VISIBLE);
itemProgressView.setMax((int) item.getDuration());
itemProgressView.setProgress((int) TimeUnit.MILLISECONDS
.toSeconds(state2.getProgressTime()));
.toSeconds(state2.getProgressMillis()));
} else {
itemProgressView.setVisibility(View.GONE);
}
@@ -121,10 +121,10 @@ public class StreamMiniInfoItemHolder extends InfoItemHolder {
itemProgressView.setMax((int) item.getDuration());
if (itemProgressView.getVisibility() == View.VISIBLE) {
itemProgressView.setProgressAnimated((int) TimeUnit.MILLISECONDS
.toSeconds(state.getProgressTime()));
.toSeconds(state.getProgressMillis()));
} else {
itemProgressView.setProgress((int) TimeUnit.MILLISECONDS
.toSeconds(state.getProgressTime()));
.toSeconds(state.getProgressMillis()));
ViewUtils.animate(itemProgressView, true, 500);
}
} else if (itemProgressView.getVisibility() == View.VISIBLE) {

View File

@@ -1,7 +1,6 @@
package org.schabi.newpipe.local;
import android.content.SharedPreferences;
import android.content.res.Configuration;
import android.content.res.Resources;
import android.os.Bundle;
import android.util.Log;
@@ -9,6 +8,7 @@ import android.view.Menu;
import android.view.MenuInflater;
import android.view.View;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.ActionBar;
import androidx.fragment.app.Fragment;
@@ -25,6 +25,7 @@ import org.schabi.newpipe.fragments.list.ListViewContract;
import static org.schabi.newpipe.ktx.ViewUtils.animate;
import static org.schabi.newpipe.ktx.ViewUtils.animateHideRecyclerViewAllowingScrolling;
import static org.schabi.newpipe.util.ThemeHelper.shouldUseGridLayout;
/**
* This fragment is design to be used with persistent data such as
@@ -76,7 +77,7 @@ public abstract class BaseLocalListFragment<I, N> extends BaseStateFragment<I>
super.onResume();
if (updateFlags != 0) {
if ((updateFlags & LIST_MODE_UPDATE_FLAG) != 0) {
final boolean useGrid = isGridLayout();
final boolean useGrid = shouldUseGridLayout(requireContext());
itemsList.setLayoutManager(
useGrid ? getGridLayoutManager() : getListLayoutManager());
itemListAdapter.setUseGridVariant(useGrid);
@@ -120,7 +121,7 @@ public abstract class BaseLocalListFragment<I, N> extends BaseStateFragment<I>
itemListAdapter = new LocalItemListAdapter(activity);
final boolean useGrid = isGridLayout();
final boolean useGrid = shouldUseGridLayout(requireContext());
itemsList = rootView.findViewById(R.id.items_list);
itemsList.setLayoutManager(useGrid ? getGridLayoutManager() : getListLayoutManager());
@@ -145,7 +146,8 @@ public abstract class BaseLocalListFragment<I, N> extends BaseStateFragment<I>
//////////////////////////////////////////////////////////////////////////*/
@Override
public void onCreateOptionsMenu(final Menu menu, final MenuInflater inflater) {
public void onCreateOptionsMenu(@NonNull final Menu menu,
@NonNull final MenuInflater inflater) {
super.onCreateOptionsMenu(menu, inflater);
if (DEBUG) {
Log.d(TAG, "onCreateOptionsMenu() called with: "
@@ -258,17 +260,4 @@ public abstract class BaseLocalListFragment<I, N> extends BaseStateFragment<I>
updateFlags |= LIST_MODE_UPDATE_FLAG;
}
}
protected boolean isGridLayout() {
final String listMode = PreferenceManager.getDefaultSharedPreferences(activity)
.getString(getString(R.string.list_view_mode_key),
getString(R.string.list_view_mode_value));
if ("auto".equals(listMode)) {
final Configuration configuration = getResources().getConfiguration();
return configuration.orientation == Configuration.ORIENTATION_LANDSCAPE
&& configuration.isLayoutSizeAtLeast(Configuration.SCREENLAYOUT_SIZE_LARGE);
} else {
return "grid".equals(listMode);
}
}
}

View File

@@ -2,11 +2,11 @@ package org.schabi.newpipe.local.bookmark;
import android.os.Bundle;
import android.os.Parcelable;
import android.text.InputType;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.EditText;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
@@ -22,6 +22,7 @@ import org.schabi.newpipe.database.LocalItem;
import org.schabi.newpipe.database.playlist.PlaylistLocalItem;
import org.schabi.newpipe.database.playlist.PlaylistMetadataEntry;
import org.schabi.newpipe.database.playlist.model.PlaylistRemoteEntity;
import org.schabi.newpipe.databinding.DialogEditTextBinding;
import org.schabi.newpipe.error.ErrorInfo;
import org.schabi.newpipe.error.UserAction;
import org.schabi.newpipe.local.BaseLocalListFragment;
@@ -255,14 +256,18 @@ public final class BookmarkFragment extends BaseLocalListFragment<List<PlaylistL
}
private void showLocalDialog(final PlaylistMetadataEntry selectedItem) {
final View dialogView = View.inflate(getContext(), R.layout.dialog_bookmark, null);
final EditText editText = dialogView.findViewById(R.id.playlist_name_edit_text);
editText.setText(selectedItem.name);
final DialogEditTextBinding dialogBinding
= DialogEditTextBinding.inflate(getLayoutInflater());
dialogBinding.dialogEditText.setHint(R.string.name);
dialogBinding.dialogEditText.setInputType(InputType.TYPE_CLASS_TEXT);
dialogBinding.dialogEditText.setText(selectedItem.name);
final AlertDialog.Builder builder = new AlertDialog.Builder(activity);
builder.setView(dialogView)
builder.setView(dialogBinding.getRoot())
.setPositiveButton(R.string.rename_playlist, (dialog, which) ->
changeLocalPlaylistName(selectedItem.uid, editText.getText().toString()))
changeLocalPlaylistName(
selectedItem.uid,
dialogBinding.dialogEditText.getText().toString()))
.setNegativeButton(R.string.cancel, null)
.setNeutralButton(R.string.delete, (dialog, which) -> {
showDeleteDialog(selectedItem.name,

View File

@@ -2,8 +2,7 @@ package org.schabi.newpipe.local.dialog;
import android.app.Dialog;
import android.os.Bundle;
import android.view.View;
import android.widget.EditText;
import android.text.InputType;
import android.widget.Toast;
import androidx.annotation.NonNull;
@@ -13,6 +12,7 @@ import androidx.appcompat.app.AlertDialog;
import org.schabi.newpipe.NewPipeDatabase;
import org.schabi.newpipe.R;
import org.schabi.newpipe.database.stream.model.StreamEntity;
import org.schabi.newpipe.databinding.DialogEditTextBinding;
import org.schabi.newpipe.local.playlist.LocalPlaylistManager;
import java.util.List;
@@ -43,16 +43,18 @@ public final class PlaylistCreationDialog extends PlaylistDialog {
return super.onCreateDialog(savedInstanceState);
}
final View dialogView = View.inflate(getContext(), R.layout.dialog_playlist_name, null);
final EditText nameInput = dialogView.findViewById(R.id.playlist_name);
final DialogEditTextBinding dialogBinding
= DialogEditTextBinding.inflate(getLayoutInflater());
dialogBinding.dialogEditText.setHint(R.string.name);
dialogBinding.dialogEditText.setInputType(InputType.TYPE_CLASS_TEXT);
final AlertDialog.Builder dialogBuilder = new AlertDialog.Builder(requireContext())
.setTitle(R.string.create_playlist)
.setView(dialogView)
.setView(dialogBinding.getRoot())
.setCancelable(true)
.setNegativeButton(R.string.cancel, null)
.setPositiveButton(R.string.create, (dialogInterface, i) -> {
final String name = nameInput.getText().toString();
final String name = dialogBinding.dialogEditText.getText().toString();
final LocalPlaylistManager playlistManager =
new LocalPlaylistManager(NewPipeDatabase.getInstance(requireContext()));
final Toast successToast = Toast.makeText(getActivity(),

View File

@@ -12,6 +12,7 @@ import org.schabi.newpipe.NewPipeDatabase
import org.schabi.newpipe.database.feed.model.FeedEntity
import org.schabi.newpipe.database.feed.model.FeedGroupEntity
import org.schabi.newpipe.database.feed.model.FeedLastUpdatedEntity
import org.schabi.newpipe.database.stream.StreamWithState
import org.schabi.newpipe.database.stream.model.StreamEntity
import org.schabi.newpipe.extractor.stream.StreamInfoItem
import org.schabi.newpipe.extractor.stream.StreamType
@@ -38,16 +39,19 @@ class FeedDatabaseManager(context: Context) {
fun database() = database
fun asStreamItems(groupId: Long = FeedGroupEntity.GROUP_ALL_ID): Flowable<List<StreamInfoItem>> {
val streams = when (groupId) {
FeedGroupEntity.GROUP_ALL_ID -> feedTable.getAllStreams()
else -> feedTable.getAllStreamsFromGroup(groupId)
}
return streams.map {
val items = ArrayList<StreamInfoItem>(it.size)
it.mapTo(items) { stream -> stream.toStreamInfoItem() }
return@map items
fun getStreams(
groupId: Long = FeedGroupEntity.GROUP_ALL_ID,
getPlayedStreams: Boolean = true
): Flowable<List<StreamWithState>> {
return when (groupId) {
FeedGroupEntity.GROUP_ALL_ID -> {
if (getPlayedStreams) feedTable.getAllStreams()
else feedTable.getLiveOrNotPlayedStreams()
}
else -> {
if (getPlayedStreams) feedTable.getAllStreamsForGroup(groupId)
else feedTable.getLiveOrNotPlayedStreamsForGroup(groupId)
}
}
}
@@ -60,8 +64,10 @@ class FeedDatabaseManager(context: Context) {
}
}
fun outdatedSubscriptionsForGroup(groupId: Long = FeedGroupEntity.GROUP_ALL_ID, outdatedThreshold: OffsetDateTime) =
feedTable.getAllOutdatedForGroup(groupId, outdatedThreshold)
fun outdatedSubscriptionsForGroup(
groupId: Long = FeedGroupEntity.GROUP_ALL_ID,
outdatedThreshold: OffsetDateTime
) = feedTable.getAllOutdatedForGroup(groupId, outdatedThreshold)
fun markAsOutdated(subscriptionId: Long) = feedTable
.setLastUpdatedForSubscription(FeedLastUpdatedEntity(subscriptionId, null))
@@ -93,10 +99,7 @@ class FeedDatabaseManager(context: Context) {
}
feedTable.setLastUpdatedForSubscription(
FeedLastUpdatedEntity(
subscriptionId,
OffsetDateTime.now(ZoneOffset.UTC)
)
FeedLastUpdatedEntity(subscriptionId, OffsetDateTime.now(ZoneOffset.UTC))
)
}
@@ -108,7 +111,12 @@ class FeedDatabaseManager(context: Context) {
fun clear() {
feedTable.deleteAll()
val deletedOrphans = streamTable.deleteOrphans()
if (DEBUG) Log.d(this::class.java.simpleName, "clear() → streamTable.deleteOrphans() → $deletedOrphans")
if (DEBUG) {
Log.d(
this::class.java.simpleName,
"clear() → streamTable.deleteOrphans() → $deletedOrphans"
)
}
}
// /////////////////////////////////////////////////////////////////////////
@@ -122,7 +130,8 @@ class FeedDatabaseManager(context: Context) {
}
fun updateSubscriptionsForGroup(groupId: Long, subscriptionIds: List<Long>): Completable {
return Completable.fromCallable { feedGroupTable.updateSubscriptionsForGroup(groupId, subscriptionIds) }
return Completable
.fromCallable { feedGroupTable.updateSubscriptionsForGroup(groupId, subscriptionIds) }
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
}

View File

@@ -19,7 +19,10 @@
package org.schabi.newpipe.local.feed
import android.annotation.SuppressLint
import android.app.Activity
import android.content.Intent
import android.content.SharedPreferences
import android.os.Bundle
import android.os.Parcelable
import android.view.LayoutInflater
@@ -28,41 +31,75 @@ import android.view.MenuInflater
import android.view.MenuItem
import android.view.View
import android.view.ViewGroup
import androidx.annotation.Nullable
import androidx.appcompat.app.AlertDialog
import androidx.appcompat.content.res.AppCompatResources
import androidx.core.content.edit
import androidx.core.os.bundleOf
import androidx.core.view.isVisible
import androidx.lifecycle.ViewModelProvider
import androidx.preference.PreferenceManager
import androidx.recyclerview.widget.GridLayoutManager
import com.xwray.groupie.GroupAdapter
import com.xwray.groupie.GroupieViewHolder
import com.xwray.groupie.Item
import com.xwray.groupie.OnItemClickListener
import com.xwray.groupie.OnItemLongClickListener
import icepick.State
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
import io.reactivex.rxjava3.core.Single
import io.reactivex.rxjava3.disposables.CompositeDisposable
import io.reactivex.rxjava3.schedulers.Schedulers
import org.schabi.newpipe.NewPipeDatabase
import org.schabi.newpipe.R
import org.schabi.newpipe.database.feed.model.FeedGroupEntity
import org.schabi.newpipe.database.subscription.SubscriptionEntity
import org.schabi.newpipe.databinding.FragmentFeedBinding
import org.schabi.newpipe.error.ErrorInfo
import org.schabi.newpipe.error.UserAction
import org.schabi.newpipe.fragments.list.BaseListFragment
import org.schabi.newpipe.extractor.exceptions.AccountTerminatedException
import org.schabi.newpipe.extractor.exceptions.ContentNotAvailableException
import org.schabi.newpipe.extractor.stream.StreamInfoItem
import org.schabi.newpipe.extractor.stream.StreamType
import org.schabi.newpipe.extractor.utils.Utils.isNullOrEmpty
import org.schabi.newpipe.fragments.BaseStateFragment
import org.schabi.newpipe.info_list.InfoItemDialog
import org.schabi.newpipe.ktx.animate
import org.schabi.newpipe.ktx.animateHideRecyclerViewAllowingScrolling
import org.schabi.newpipe.local.feed.item.StreamItem
import org.schabi.newpipe.local.feed.service.FeedLoadService
import org.schabi.newpipe.local.subscription.SubscriptionManager
import org.schabi.newpipe.player.helper.PlayerHolder
import org.schabi.newpipe.util.Localization
import org.schabi.newpipe.util.NavigationHelper
import org.schabi.newpipe.util.StreamDialogEntry
import org.schabi.newpipe.util.ThemeHelper.getGridSpanCountStreams
import org.schabi.newpipe.util.ThemeHelper.shouldUseGridLayout
import java.time.OffsetDateTime
import java.util.ArrayList
class FeedFragment : BaseListFragment<FeedState, Unit>() {
class FeedFragment : BaseStateFragment<FeedState>() {
private var _feedBinding: FragmentFeedBinding? = null
private val feedBinding get() = _feedBinding!!
private val disposables = CompositeDisposable()
private lateinit var viewModel: FeedViewModel
@State
@JvmField
var listState: Parcelable? = null
@State @JvmField var listState: Parcelable? = null
private var groupId = FeedGroupEntity.GROUP_ALL_ID
private var groupName = ""
private var oldestSubscriptionUpdate: OffsetDateTime? = null
private lateinit var groupAdapter: GroupAdapter<GroupieViewHolder>
@State @JvmField var showPlayedItems: Boolean = true
private var onSettingsChangeListener: SharedPreferences.OnSharedPreferenceChangeListener? = null
private var updateListViewModeOnResume = false
private var isRefreshing = false
init {
setHasOptionsMenu(true)
setUseDefaultStateSaving(false)
}
override fun onCreate(savedInstanceState: Bundle?) {
@@ -71,6 +108,14 @@ class FeedFragment : BaseListFragment<FeedState, Unit>() {
groupId = arguments?.getLong(KEY_GROUP_ID, FeedGroupEntity.GROUP_ALL_ID)
?: FeedGroupEntity.GROUP_ALL_ID
groupName = arguments?.getString(KEY_GROUP_NAME) ?: ""
onSettingsChangeListener = SharedPreferences.OnSharedPreferenceChangeListener { _, key ->
if (key.equals(getString(R.string.list_view_mode_key))) {
updateListViewModeOnResume = true
}
}
PreferenceManager.getDefaultSharedPreferences(activity)
.registerOnSharedPreferenceChangeListener(onSettingsChangeListener)
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
@@ -82,8 +127,17 @@ class FeedFragment : BaseListFragment<FeedState, Unit>() {
_feedBinding = FragmentFeedBinding.bind(rootView)
super.onViewCreated(rootView, savedInstanceState)
viewModel = ViewModelProvider(this, FeedViewModel.Factory(requireContext(), groupId)).get(FeedViewModel::class.java)
viewModel.stateLiveData.observe(viewLifecycleOwner) { it?.let(::handleResult) }
val factory = FeedViewModel.Factory(requireContext(), groupId, showPlayedItems)
viewModel = ViewModelProvider(this, factory).get(FeedViewModel::class.java)
viewModel.stateLiveData.observe(viewLifecycleOwner, { it?.let(::handleResult) })
groupAdapter = GroupAdapter<GroupieViewHolder>().apply {
setOnItemClickListener(listenerStreamItem)
setOnItemLongClickListener(listenerStreamItem)
}
feedBinding.itemsList.adapter = groupAdapter
setupListViewMode()
}
override fun onPause() {
@@ -94,13 +148,22 @@ class FeedFragment : BaseListFragment<FeedState, Unit>() {
override fun onResume() {
super.onResume()
updateRelativeTimeViews()
if (updateListViewModeOnResume) {
updateListViewModeOnResume = false
setupListViewMode()
if (viewModel.stateLiveData.value != null) {
handleResult(viewModel.stateLiveData.value!!)
}
}
}
override fun setUserVisibleHint(isVisibleToUser: Boolean) {
super.setUserVisibleHint(isVisibleToUser)
if (!isVisibleToUser && view != null) {
updateRelativeTimeViews()
fun setupListViewMode() {
// does everything needed to setup the layouts for grid or list modes
groupAdapter.spanCount = if (shouldUseGridLayout(context)) getGridSpanCountStreams(context) else 1
feedBinding.itemsList.layoutManager = GridLayoutManager(requireContext(), groupAdapter.spanCount).apply {
spanSizeLookup = groupAdapter.spanSizeLookup
}
}
@@ -116,21 +179,21 @@ class FeedFragment : BaseListFragment<FeedState, Unit>() {
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
super.onCreateOptionsMenu(menu, inflater)
activity.supportActionBar?.setDisplayShowTitleEnabled(true)
activity.supportActionBar?.setTitle(R.string.fragment_feed_title)
activity.supportActionBar?.subtitle = groupName
inflater.inflate(R.menu.menu_feed_fragment, menu)
if (useAsFrontPage) {
menu.findItem(R.id.menu_item_feed_help).setShowAsAction(MenuItem.SHOW_AS_ACTION_NEVER)
}
updateTogglePlayedItemsButton(menu.findItem(R.id.menu_item_feed_toggle_played_items))
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
if (item.itemId == R.id.menu_item_feed_help) {
val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(requireContext())
val usingDedicatedMethod = sharedPreferences.getBoolean(getString(R.string.feed_use_dedicated_fetch_method_key), false)
val usingDedicatedMethod = sharedPreferences
.getBoolean(getString(R.string.feed_use_dedicated_fetch_method_key), false)
val enableDisableButtonText = when {
usingDedicatedMethod -> R.string.feed_use_dedicated_fetch_method_disable_button
else -> R.string.feed_use_dedicated_fetch_method_enable_button
@@ -147,6 +210,10 @@ class FeedFragment : BaseListFragment<FeedState, Unit>() {
.create()
.show()
return true
} else if (item.itemId == R.id.menu_item_feed_toggle_played_items) {
showPlayedItems = !item.isChecked
updateTogglePlayedItemsButton(item)
viewModel.togglePlayedItems(showPlayedItems)
}
return super.onOptionsItemSelected(item)
@@ -158,18 +225,34 @@ class FeedFragment : BaseListFragment<FeedState, Unit>() {
}
override fun onDestroy() {
disposables.dispose()
if (onSettingsChangeListener != null) {
PreferenceManager.getDefaultSharedPreferences(activity)
.unregisterOnSharedPreferenceChangeListener(onSettingsChangeListener)
onSettingsChangeListener = null
}
super.onDestroy()
activity?.supportActionBar?.subtitle = null
}
override fun onDestroyView() {
feedBinding.itemsList.adapter = null
_feedBinding = null
super.onDestroyView()
}
// /////////////////////////////////////////////////////////////////////////
private fun updateTogglePlayedItemsButton(menuItem: MenuItem) {
menuItem.isChecked = showPlayedItems
menuItem.icon = AppCompatResources.getDrawable(
requireContext(),
if (showPlayedItems) R.drawable.ic_visibility_on else R.drawable.ic_visibility_off
)
}
// //////////////////////////////////////////////////////////////////////////
// Handling
// /////////////////////////////////////////////////////////////////////////
// //////////////////////////////////////////////////////////////////////////
override fun showLoading() {
super.showLoading()
@@ -177,13 +260,16 @@ class FeedFragment : BaseListFragment<FeedState, Unit>() {
feedBinding.refreshRootView.animate(false, 0)
feedBinding.loadingProgressText.animate(true, 200)
feedBinding.swipeRefreshLayout.isRefreshing = true
isRefreshing = true
}
override fun hideLoading() {
super.hideLoading()
feedBinding.itemsList.animate(true, 0)
feedBinding.refreshRootView.animate(true, 200)
feedBinding.loadingProgressText.animate(false, 0)
feedBinding.swipeRefreshLayout.isRefreshing = false
isRefreshing = false
}
override fun showEmptyState() {
@@ -206,11 +292,11 @@ class FeedFragment : BaseListFragment<FeedState, Unit>() {
override fun handleError() {
super.handleError()
infoListAdapter.clearStreamItemList()
feedBinding.itemsList.animateHideRecyclerViewAllowingScrolling()
feedBinding.refreshRootView.animate(false, 0)
feedBinding.loadingProgressText.animate(false, 0)
feedBinding.swipeRefreshLayout.isRefreshing = false
isRefreshing = false
}
private fun handleProgressState(progressState: FeedState.ProgressState) {
@@ -234,24 +320,109 @@ class FeedFragment : BaseListFragment<FeedState, Unit>() {
feedBinding.loadingProgressBar.max = progressState.maxProgress
}
private fun showStreamDialog(item: StreamInfoItem) {
val context = context
val activity: Activity? = getActivity()
if (context == null || context.resources == null || activity == null) return
val entries = ArrayList<StreamDialogEntry>()
if (PlayerHolder.getInstance().getType() != null) {
entries.add(StreamDialogEntry.enqueue)
}
if (item.streamType == StreamType.AUDIO_STREAM) {
entries.addAll(
listOf(
StreamDialogEntry.start_here_on_background,
StreamDialogEntry.append_playlist,
StreamDialogEntry.share,
StreamDialogEntry.open_in_browser
)
)
} else {
entries.addAll(
listOf(
StreamDialogEntry.start_here_on_background,
StreamDialogEntry.start_here_on_popup,
StreamDialogEntry.append_playlist,
StreamDialogEntry.share,
StreamDialogEntry.open_in_browser
)
)
}
// show "mark as watched" only when watch history is enabled
val isWatchHistoryEnabled = PreferenceManager
.getDefaultSharedPreferences(context)
.getBoolean(getString(R.string.enable_watch_history_key), false)
if (item.streamType != StreamType.AUDIO_LIVE_STREAM &&
item.streamType != StreamType.LIVE_STREAM &&
isWatchHistoryEnabled
) {
entries.add(
StreamDialogEntry.mark_as_watched
)
}
StreamDialogEntry.setEnabledEntries(entries)
InfoItemDialog(activity, item, StreamDialogEntry.getCommands(context)) { _, which ->
StreamDialogEntry.clickOn(which, this, item)
}.show()
}
private val listenerStreamItem = object : OnItemClickListener, OnItemLongClickListener {
override fun onItemClick(item: Item<*>, view: View) {
if (item is StreamItem && !isRefreshing) {
val stream = item.streamWithState.stream
NavigationHelper.openVideoDetailFragment(
requireContext(), fm,
stream.serviceId, stream.url, stream.title, null, false
)
}
}
override fun onItemLongClick(item: Item<*>, view: View): Boolean {
if (item is StreamItem && !isRefreshing) {
showStreamDialog(item.streamWithState.stream.toStreamInfoItem())
return true
}
return false
}
}
@SuppressLint("StringFormatMatches")
private fun handleLoadedState(loadedState: FeedState.LoadedState) {
infoListAdapter.setInfoItemList(loadedState.items)
val itemVersion = if (shouldUseGridLayout(context)) {
StreamItem.ItemVersion.GRID
} else {
StreamItem.ItemVersion.NORMAL
}
loadedState.items.forEach { it.itemVersion = itemVersion }
groupAdapter.updateAsync(loadedState.items, false, null)
listState?.run {
feedBinding.itemsList.layoutManager?.onRestoreInstanceState(listState)
listState = null
}
oldestSubscriptionUpdate = loadedState.oldestUpdate
val loadedCount = loadedState.notLoadedCount > 0
feedBinding.refreshSubtitleText.isVisible = loadedCount
if (loadedCount) {
val feedsNotLoaded = loadedState.notLoadedCount > 0
feedBinding.refreshSubtitleText.isVisible = feedsNotLoaded
if (feedsNotLoaded) {
feedBinding.refreshSubtitleText.text = getString(
R.string.feed_subscription_not_loaded_count,
loadedState.notLoadedCount
)
}
if (oldestSubscriptionUpdate != loadedState.oldestUpdate ||
(oldestSubscriptionUpdate == null && loadedState.oldestUpdate == null)
) {
// ignore errors if they have already been handled for the current update
handleItemsErrors(loadedState.itemsErrors)
}
oldestSubscriptionUpdate = loadedState.oldestUpdate
if (loadedState.items.isEmpty()) {
showEmptyState()
} else {
@@ -269,9 +440,78 @@ class FeedFragment : BaseListFragment<FeedState, Unit>() {
}
}
private fun handleItemsErrors(errors: List<Throwable>) {
errors.forEachIndexed { i, t ->
if (t is FeedLoadService.RequestException &&
t.cause is ContentNotAvailableException
) {
Single.fromCallable {
NewPipeDatabase.getInstance(requireContext()).subscriptionDAO()
.getSubscription(t.subscriptionId)
}.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
{
subscriptionEntity ->
handleFeedNotAvailable(
subscriptionEntity,
t.cause,
errors.subList(i + 1, errors.size)
)
},
{ throwable -> throwable.printStackTrace() }
)
return // this will be called on the remaining errors by handleFeedNotAvailable()
}
}
}
private fun handleFeedNotAvailable(
subscriptionEntity: SubscriptionEntity,
@Nullable cause: Throwable?,
nextItemsErrors: List<Throwable>
) {
val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(requireContext())
val isFastFeedModeEnabled = sharedPreferences.getBoolean(
getString(R.string.feed_use_dedicated_fetch_method_key), false
)
val builder = AlertDialog.Builder(requireContext())
.setTitle(R.string.feed_load_error)
.setPositiveButton(
R.string.unsubscribe
) { _, _ ->
SubscriptionManager(requireContext()).deleteSubscription(
subscriptionEntity.serviceId, subscriptionEntity.url
).subscribe()
handleItemsErrors(nextItemsErrors)
}
.setNegativeButton(R.string.cancel) { _, _ -> }
var message = getString(R.string.feed_load_error_account_info, subscriptionEntity.name)
if (cause is AccountTerminatedException) {
message += "\n" + getString(R.string.feed_load_error_terminated)
} else if (cause is ContentNotAvailableException) {
if (isFastFeedModeEnabled) {
message += "\n" + getString(R.string.feed_load_error_fast_unknown)
builder.setNeutralButton(R.string.feed_use_dedicated_fetch_method_disable_button) { _, _ ->
sharedPreferences.edit {
putBoolean(getString(R.string.feed_use_dedicated_fetch_method_key), false)
}
}
} else if (!isNullOrEmpty(cause.message)) {
message += "\n" + cause.message
}
}
builder.setMessage(message).create().show()
}
private fun updateRelativeTimeViews() {
updateRefreshViewState()
infoListAdapter.notifyDataSetChanged()
groupAdapter.notifyItemRangeChanged(
0, groupAdapter.itemCount,
StreamItem.UPDATE_RELATIVE_TIME
)
}
private fun updateRefreshViewState() {
@@ -286,8 +526,6 @@ class FeedFragment : BaseListFragment<FeedState, Unit>() {
// /////////////////////////////////////////////////////////////////////////
override fun doInitialLoadLogic() {}
override fun loadMoreItems() {}
override fun hasMoreItems() = false
override fun reloadContent() {
getActivity()?.startService(

View File

@@ -1,7 +1,7 @@
package org.schabi.newpipe.local.feed
import androidx.annotation.StringRes
import org.schabi.newpipe.extractor.stream.StreamInfoItem
import org.schabi.newpipe.local.feed.item.StreamItem
import java.time.OffsetDateTime
sealed class FeedState {
@@ -12,7 +12,7 @@ sealed class FeedState {
) : FeedState()
data class LoadedState(
val items: List<StreamInfoItem>,
val items: List<StreamItem>,
val oldestUpdate: OffsetDateTime? = null,
val notLoadedCount: Long,
val itemsErrors: List<Throwable> = emptyList()

View File

@@ -8,9 +8,11 @@ import androidx.lifecycle.ViewModelProvider
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
import io.reactivex.rxjava3.core.Flowable
import io.reactivex.rxjava3.functions.Function4
import io.reactivex.rxjava3.processors.BehaviorProcessor
import io.reactivex.rxjava3.schedulers.Schedulers
import org.schabi.newpipe.database.feed.model.FeedGroupEntity
import org.schabi.newpipe.extractor.stream.StreamInfoItem
import org.schabi.newpipe.database.stream.StreamWithState
import org.schabi.newpipe.local.feed.item.StreamItem
import org.schabi.newpipe.local.feed.service.FeedEventManager
import org.schabi.newpipe.local.feed.service.FeedEventManager.Event.ErrorResultEvent
import org.schabi.newpipe.local.feed.service.FeedEventManager.Event.IdleEvent
@@ -20,26 +22,33 @@ import org.schabi.newpipe.util.DEFAULT_THROTTLE_TIMEOUT
import java.time.OffsetDateTime
import java.util.concurrent.TimeUnit
class FeedViewModel(applicationContext: Context, val groupId: Long = FeedGroupEntity.GROUP_ALL_ID) : ViewModel() {
class Factory(val context: Context, val groupId: Long = FeedGroupEntity.GROUP_ALL_ID) : ViewModelProvider.Factory {
@Suppress("UNCHECKED_CAST")
override fun <T : ViewModel?> create(modelClass: Class<T>): T {
return FeedViewModel(context.applicationContext, groupId) as T
}
}
class FeedViewModel(
applicationContext: Context,
groupId: Long = FeedGroupEntity.GROUP_ALL_ID,
initialShowPlayedItems: Boolean = true
) : ViewModel() {
private var feedDatabaseManager: FeedDatabaseManager = FeedDatabaseManager(applicationContext)
private val toggleShowPlayedItems = BehaviorProcessor.create<Boolean>()
private val streamItems = toggleShowPlayedItems
.startWithItem(initialShowPlayedItems)
.distinctUntilChanged()
.switchMap { showPlayedItems ->
feedDatabaseManager.getStreams(groupId, showPlayedItems)
}
private val mutableStateLiveData = MutableLiveData<FeedState>()
val stateLiveData: LiveData<FeedState> = mutableStateLiveData
private var combineDisposable = Flowable
.combineLatest(
FeedEventManager.events(),
feedDatabaseManager.asStreamItems(groupId),
streamItems,
feedDatabaseManager.notLoadedCount(groupId),
feedDatabaseManager.oldestSubscriptionUpdate(groupId),
Function4 { t1: FeedEventManager.Event, t2: List<StreamInfoItem>, t3: Long, t4: List<OffsetDateTime> ->
Function4 { t1: FeedEventManager.Event, t2: List<StreamWithState>,
t3: Long, t4: List<OffsetDateTime> ->
return@Function4 CombineResultHolder(t1, t2, t3, t4.firstOrNull())
}
)
@@ -49,9 +58,9 @@ class FeedViewModel(applicationContext: Context, val groupId: Long = FeedGroupEn
.subscribe { (event, listFromDB, notLoadedCount, oldestUpdate) ->
mutableStateLiveData.postValue(
when (event) {
is IdleEvent -> FeedState.LoadedState(listFromDB, oldestUpdate, notLoadedCount)
is IdleEvent -> FeedState.LoadedState(listFromDB.map { e -> StreamItem(e) }, oldestUpdate, notLoadedCount)
is ProgressEvent -> FeedState.ProgressState(event.currentProgress, event.maxProgress, event.progressMessage)
is SuccessResultEvent -> FeedState.LoadedState(listFromDB, oldestUpdate, notLoadedCount, event.itemsErrors)
is SuccessResultEvent -> FeedState.LoadedState(listFromDB.map { e -> StreamItem(e) }, oldestUpdate, notLoadedCount, event.itemsErrors)
is ErrorResultEvent -> FeedState.ErrorState(event.error)
}
)
@@ -66,5 +75,20 @@ class FeedViewModel(applicationContext: Context, val groupId: Long = FeedGroupEn
combineDisposable.dispose()
}
private data class CombineResultHolder(val t1: FeedEventManager.Event, val t2: List<StreamInfoItem>, val t3: Long, val t4: OffsetDateTime?)
private data class CombineResultHolder(val t1: FeedEventManager.Event, val t2: List<StreamWithState>, val t3: Long, val t4: OffsetDateTime?)
fun togglePlayedItems(showPlayedItems: Boolean) {
toggleShowPlayedItems.onNext(showPlayedItems)
}
class Factory(
private val context: Context,
private val groupId: Long = FeedGroupEntity.GROUP_ALL_ID,
private val showPlayedItems: Boolean
) : ViewModelProvider.Factory {
@Suppress("UNCHECKED_CAST")
override fun <T : ViewModel?> create(modelClass: Class<T>): T {
return FeedViewModel(context.applicationContext, groupId, showPlayedItems) as T
}
}
}

View File

@@ -0,0 +1,153 @@
package org.schabi.newpipe.local.feed.item
import android.content.Context
import android.text.TextUtils
import android.view.View
import androidx.core.content.ContextCompat
import androidx.preference.PreferenceManager
import com.nostra13.universalimageloader.core.ImageLoader
import com.xwray.groupie.viewbinding.BindableItem
import org.schabi.newpipe.MainActivity
import org.schabi.newpipe.R
import org.schabi.newpipe.database.stream.StreamWithState
import org.schabi.newpipe.database.stream.model.StreamEntity
import org.schabi.newpipe.databinding.ListStreamItemBinding
import org.schabi.newpipe.extractor.stream.StreamType.AUDIO_LIVE_STREAM
import org.schabi.newpipe.extractor.stream.StreamType.AUDIO_STREAM
import org.schabi.newpipe.extractor.stream.StreamType.LIVE_STREAM
import org.schabi.newpipe.extractor.stream.StreamType.VIDEO_STREAM
import org.schabi.newpipe.util.ImageDisplayConstants
import org.schabi.newpipe.util.Localization
import java.util.concurrent.TimeUnit
data class StreamItem(
val streamWithState: StreamWithState,
var itemVersion: ItemVersion = ItemVersion.NORMAL
) : BindableItem<ListStreamItemBinding>() {
companion object {
const val UPDATE_RELATIVE_TIME = 1
}
private val stream: StreamEntity = streamWithState.stream
private val stateProgressTime: Long? = streamWithState.stateProgressMillis
override fun getId(): Long = stream.uid
enum class ItemVersion { NORMAL, MINI, GRID }
override fun getLayout(): Int = when (itemVersion) {
ItemVersion.NORMAL -> R.layout.list_stream_item
ItemVersion.MINI -> R.layout.list_stream_mini_item
ItemVersion.GRID -> R.layout.list_stream_grid_item
}
override fun initializeViewBinding(view: View) = ListStreamItemBinding.bind(view)
override fun bind(viewBinding: ListStreamItemBinding, position: Int, payloads: MutableList<Any>) {
if (payloads.contains(UPDATE_RELATIVE_TIME)) {
if (itemVersion != ItemVersion.MINI) {
viewBinding.itemAdditionalDetails.text =
getStreamInfoDetailLine(viewBinding.itemAdditionalDetails.context)
}
return
}
super.bind(viewBinding, position, payloads)
}
override fun bind(viewBinding: ListStreamItemBinding, position: Int) {
viewBinding.itemVideoTitleView.text = stream.title
viewBinding.itemUploaderView.text = stream.uploader
val isLiveStream = stream.streamType == LIVE_STREAM || stream.streamType == AUDIO_LIVE_STREAM
if (stream.duration > 0) {
viewBinding.itemDurationView.text = Localization.getDurationString(stream.duration)
viewBinding.itemDurationView.setBackgroundColor(
ContextCompat.getColor(
viewBinding.itemDurationView.context,
R.color.duration_background_color
)
)
viewBinding.itemDurationView.visibility = View.VISIBLE
if (stateProgressTime != null) {
viewBinding.itemProgressView.visibility = View.VISIBLE
viewBinding.itemProgressView.max = stream.duration.toInt()
viewBinding.itemProgressView.progress = TimeUnit.MILLISECONDS.toSeconds(stateProgressTime).toInt()
} else {
viewBinding.itemProgressView.visibility = View.GONE
}
} else if (isLiveStream) {
viewBinding.itemDurationView.setText(R.string.duration_live)
viewBinding.itemDurationView.setBackgroundColor(
ContextCompat.getColor(
viewBinding.itemDurationView.context,
R.color.live_duration_background_color
)
)
viewBinding.itemDurationView.visibility = View.VISIBLE
viewBinding.itemProgressView.visibility = View.GONE
} else {
viewBinding.itemDurationView.visibility = View.GONE
viewBinding.itemProgressView.visibility = View.GONE
}
ImageLoader.getInstance().displayImage(
stream.thumbnailUrl, viewBinding.itemThumbnailView,
ImageDisplayConstants.DISPLAY_THUMBNAIL_OPTIONS
)
if (itemVersion != ItemVersion.MINI) {
viewBinding.itemAdditionalDetails.text =
getStreamInfoDetailLine(viewBinding.itemAdditionalDetails.context)
}
}
override fun isLongClickable() = when (stream.streamType) {
AUDIO_STREAM, VIDEO_STREAM, LIVE_STREAM, AUDIO_LIVE_STREAM -> true
else -> false
}
private fun getStreamInfoDetailLine(context: Context): String {
var viewsAndDate = ""
val viewCount = stream.viewCount
if (viewCount != null && viewCount >= 0) {
viewsAndDate = when (stream.streamType) {
AUDIO_LIVE_STREAM -> Localization.listeningCount(context, viewCount)
LIVE_STREAM -> Localization.shortWatchingCount(context, viewCount)
else -> Localization.shortViewCount(context, viewCount)
}
}
val uploadDate = getFormattedRelativeUploadDate(context)
return when {
!TextUtils.isEmpty(uploadDate) -> when {
viewsAndDate.isEmpty() -> uploadDate!!
else -> Localization.concatenateStrings(viewsAndDate, uploadDate)
}
else -> viewsAndDate
}
}
private fun getFormattedRelativeUploadDate(context: Context): String? {
val uploadDate = stream.uploadDate
return if (uploadDate != null) {
var formattedRelativeTime = Localization.relativeTime(uploadDate)
if (MainActivity.DEBUG) {
val key = context.getString(R.string.show_original_time_ago_key)
if (PreferenceManager.getDefaultSharedPreferences(context).getBoolean(key, false)) {
formattedRelativeTime += " (" + stream.textualUploadDate + ")"
}
}
formattedRelativeTime
} else {
stream.textualUploadDate
}
}
override fun getSpanSize(spanCount: Int, position: Int): Int {
return if (itemVersion == ItemVersion.GRID) 1 else spanCount
}
}

View File

@@ -48,9 +48,7 @@ import org.schabi.newpipe.MainActivity.DEBUG
import org.schabi.newpipe.R
import org.schabi.newpipe.database.feed.model.FeedGroupEntity
import org.schabi.newpipe.extractor.ListInfo
import org.schabi.newpipe.extractor.exceptions.ReCaptchaException
import org.schabi.newpipe.extractor.stream.StreamInfoItem
import org.schabi.newpipe.ktx.isNetworkRelated
import org.schabi.newpipe.local.feed.FeedDatabaseManager
import org.schabi.newpipe.local.feed.service.FeedEventManager.Event.ErrorResultEvent
import org.schabi.newpipe.local.feed.service.FeedEventManager.Event.ProgressEvent
@@ -58,7 +56,6 @@ import org.schabi.newpipe.local.feed.service.FeedEventManager.Event.SuccessResul
import org.schabi.newpipe.local.feed.service.FeedEventManager.postEvent
import org.schabi.newpipe.local.subscription.SubscriptionManager
import org.schabi.newpipe.util.ExtractorHelper
import java.io.IOException
import java.time.OffsetDateTime
import java.time.ZoneOffset
import java.util.concurrent.TimeUnit
@@ -162,7 +159,7 @@ class FeedLoadService : Service() {
// Loading & Handling
// /////////////////////////////////////////////////////////////////////////
private class RequestException(val subscriptionId: Long, message: String, cause: Throwable) : Exception(message, cause) {
class RequestException(val subscriptionId: Long, message: String, cause: Throwable) : Exception(message, cause) {
companion object {
fun wrapList(subscriptionId: Long, info: ListInfo<StreamInfoItem>): List<Throwable> {
val toReturn = ArrayList<Throwable>(info.errors.size)
@@ -209,29 +206,40 @@ class FeedLoadService : Service() {
.filter { !cancelSignal.get() }
.map { subscriptionEntity ->
var error: Throwable? = null
try {
val listInfo = if (useFeedExtractor) {
ExtractorHelper
.getFeedInfoFallbackToChannelInfo(subscriptionEntity.serviceId, subscriptionEntity.url)
.onErrorReturn {
error = it // store error, otherwise wrapped into RuntimeException
throw it
}
.blockingGet()
} else {
ExtractorHelper
.getChannelInfo(subscriptionEntity.serviceId, subscriptionEntity.url, true)
.onErrorReturn {
error = it // store error, otherwise wrapped into RuntimeException
throw it
}
.blockingGet()
} as ListInfo<StreamInfoItem>
return@map Notification.createOnNext(Pair(subscriptionEntity.uid, listInfo))
} catch (e: Throwable) {
if (error == null) {
// do this to prevent blockingGet() from wrapping into RuntimeException
error = e
}
val request = "${subscriptionEntity.serviceId}:${subscriptionEntity.url}"
val wrapper = RequestException(subscriptionEntity.uid, request, e)
val wrapper = RequestException(subscriptionEntity.uid, request, error!!)
return@map Notification.createOnError<Pair<Long, ListInfo<StreamInfoItem>>>(wrapper)
}
}
.sequential()
.observeOn(AndroidSchedulers.mainThread())
.doOnNext(errorHandlingConsumer)
.observeOn(AndroidSchedulers.mainThread())
.doOnNext(notificationsConsumer)
@@ -331,24 +339,6 @@ class FeedLoadService : Service() {
}
}
private val errorHandlingConsumer: Consumer<Notification<Pair<Long, ListInfo<StreamInfoItem>>>>
get() = Consumer {
if (it.isOnError) {
var error = it.error!!
if (error is RequestException) error = error.cause!!
val cause = error.cause
when {
error is ReCaptchaException -> throw error
cause is ReCaptchaException -> throw cause
error is IOException -> throw error
cause is IOException -> throw cause
error.isNetworkRelated -> throw IOException(error)
}
}
}
private val notificationsConsumer: Consumer<Notification<Pair<Long, ListInfo<StreamInfoItem>>>>
get() = Consumer { onItemCompleted(it.value?.second?.name) }

View File

@@ -28,6 +28,7 @@ import org.schabi.newpipe.NewPipeDatabase;
import org.schabi.newpipe.R;
import org.schabi.newpipe.database.AppDatabase;
import org.schabi.newpipe.database.LocalItem;
import org.schabi.newpipe.database.feed.dao.FeedDAO;
import org.schabi.newpipe.database.history.dao.SearchHistoryDAO;
import org.schabi.newpipe.database.history.dao.StreamHistoryDAO;
import org.schabi.newpipe.database.history.model.SearchHistoryEntry;
@@ -42,7 +43,10 @@ import org.schabi.newpipe.database.stream.model.StreamEntity;
import org.schabi.newpipe.database.stream.model.StreamStateEntity;
import org.schabi.newpipe.extractor.InfoItem;
import org.schabi.newpipe.extractor.stream.StreamInfo;
import org.schabi.newpipe.extractor.stream.StreamInfoItem;
import org.schabi.newpipe.local.feed.FeedViewModel;
import org.schabi.newpipe.player.playqueue.PlayQueueItem;
import org.schabi.newpipe.util.ExtractorHelper;
import java.time.OffsetDateTime;
import java.time.ZoneOffset;
@@ -81,6 +85,68 @@ public class HistoryRecordManager {
// Watch History
///////////////////////////////////////////////////////
/**
* Marks a stream item as watched such that it is hidden from the feed if watched videos are
* hidden. Adds a history entry and updates the stream progress to 100%.
*
* @see FeedDAO#getLiveOrNotPlayedStreams
* @see FeedViewModel#togglePlayedItems
* @param info the item to mark as watched
* @return a Maybe containing the ID of the item if successful
*/
public Maybe<Long> markAsWatched(final StreamInfoItem info) {
if (!isStreamHistoryEnabled()) {
return Maybe.empty();
}
final OffsetDateTime currentTime = OffsetDateTime.now(ZoneOffset.UTC);
return Maybe.fromCallable(() -> database.runInTransaction(() -> {
final long streamId;
final long duration;
// Duration will not exist if the item was loaded with fast mode, so fetch it if empty
if (info.getDuration() < 0) {
final StreamInfo completeInfo = ExtractorHelper.getStreamInfo(
info.getServiceId(),
info.getUrl(),
false
)
.subscribeOn(Schedulers.io())
.blockingGet();
duration = completeInfo.getDuration();
streamId = streamTable.upsert(new StreamEntity(completeInfo));
} else {
duration = info.getDuration();
streamId = streamTable.upsert(new StreamEntity(info));
}
// Update the stream progress to the full duration of the video
final List<StreamStateEntity> states = streamStateTable.getState(streamId)
.blockingFirst();
if (!states.isEmpty()) {
final StreamStateEntity entity = states.get(0);
entity.setProgressMillis(duration * 1000);
streamStateTable.update(entity);
} else {
final StreamStateEntity entity = new StreamStateEntity(
streamId,
duration * 1000
);
streamStateTable.insert(entity);
}
// Add a history entry
final StreamHistoryEntity latestEntry = streamHistoryTable.getLatestEntry(streamId);
if (latestEntry != null) {
streamHistoryTable.delete(latestEntry);
latestEntry.setAccessDate(currentTime);
latestEntry.setRepeatCount(latestEntry.getRepeatCount() + 1);
return streamHistoryTable.insert(latestEntry);
} else {
return streamHistoryTable.insert(new StreamHistoryEntity(streamId, currentTime));
}
})).subscribeOn(Schedulers.io());
}
public Maybe<Long> onViewed(final StreamInfo info) {
if (!isStreamHistoryEnabled()) {
return Maybe.empty();
@@ -211,11 +277,11 @@ public class HistoryRecordManager {
public Maybe<StreamStateEntity> loadStreamState(final PlayQueueItem queueItem) {
return queueItem.getStream()
.map((info) -> streamTable.upsert(new StreamEntity(info)))
.map(info -> streamTable.upsert(new StreamEntity(info)))
.flatMapPublisher(streamStateTable::getState)
.firstElement()
.flatMap(list -> list.isEmpty() ? Maybe.empty() : Maybe.just(list.get(0)))
.filter(state -> state.isValid((int) queueItem.getDuration()))
.filter(state -> state.isValid(queueItem.getDuration()))
.subscribeOn(Schedulers.io());
}
@@ -224,18 +290,16 @@ public class HistoryRecordManager {
.flatMapPublisher(streamStateTable::getState)
.firstElement()
.flatMap(list -> list.isEmpty() ? Maybe.empty() : Maybe.just(list.get(0)))
.filter(state -> state.isValid((int) info.getDuration()))
.filter(state -> state.isValid(info.getDuration()))
.subscribeOn(Schedulers.io());
}
public Completable saveStreamState(@NonNull final StreamInfo info, final long progressTime) {
public Completable saveStreamState(@NonNull final StreamInfo info, final long progressMillis) {
return Completable.fromAction(() -> database.runInTransaction(() -> {
final long streamId = streamTable.upsert(new StreamEntity(info));
final StreamStateEntity state = new StreamStateEntity(streamId, progressTime);
if (state.isValid((int) info.getDuration())) {
final StreamStateEntity state = new StreamStateEntity(streamId, progressMillis);
if (state.isValid(info.getDuration())) {
streamStateTable.upsert(state);
} else {
streamStateTable.deleteState(streamId);
}
})).subscribeOn(Schedulers.io());
}

View File

@@ -36,7 +36,7 @@ import org.schabi.newpipe.player.helper.PlayerHolder;
import org.schabi.newpipe.player.playqueue.PlayQueue;
import org.schabi.newpipe.player.playqueue.SinglePlayQueue;
import org.schabi.newpipe.settings.HistorySettingsFragment;
import org.schabi.newpipe.util.KoreUtil;
import org.schabi.newpipe.util.external_communication.KoreUtils;
import org.schabi.newpipe.util.NavigationHelper;
import org.schabi.newpipe.util.OnClickGesture;
import org.schabi.newpipe.util.StreamDialogEntry;
@@ -111,7 +111,8 @@ public class StatisticsPlaylistFragment
}
@Override
public void onCreateOptionsMenu(final Menu menu, final MenuInflater inflater) {
public void onCreateOptionsMenu(@NonNull final Menu menu,
@NonNull final MenuInflater inflater) {
super.onCreateOptionsMenu(menu, inflater);
inflater.inflate(R.menu.menu_history, menu);
}
@@ -339,7 +340,7 @@ public class StatisticsPlaylistFragment
final ArrayList<StreamDialogEntry> entries = new ArrayList<>();
if (PlayerHolder.getType() != null) {
if (PlayerHolder.getInstance().getType() != null) {
entries.add(StreamDialogEntry.enqueue);
}
if (infoItem.getStreamType() == StreamType.AUDIO_STREAM) {
@@ -359,7 +360,7 @@ public class StatisticsPlaylistFragment
));
}
entries.add(StreamDialogEntry.open_in_browser);
if (KoreUtil.shouldShowPlayWithKodi(context, infoItem.getServiceId())) {
if (KoreUtils.shouldShowPlayWithKodi(context, infoItem.getServiceId())) {
entries.add(StreamDialogEntry.play_with_kodi);
}

View File

@@ -68,11 +68,11 @@ public class LocalPlaylistStreamItemHolder extends LocalItemHolder {
R.color.duration_background_color));
itemDurationView.setVisibility(View.VISIBLE);
if (item.getProgressTime() > 0) {
if (item.getProgressMillis() > 0) {
itemProgressView.setVisibility(View.VISIBLE);
itemProgressView.setMax((int) item.getStreamEntity().getDuration());
itemProgressView.setProgress((int) TimeUnit.MILLISECONDS
.toSeconds(item.getProgressTime()));
.toSeconds(item.getProgressMillis()));
} else {
itemProgressView.setVisibility(View.GONE);
}
@@ -109,14 +109,14 @@ public class LocalPlaylistStreamItemHolder extends LocalItemHolder {
}
final PlaylistStreamEntry item = (PlaylistStreamEntry) localItem;
if (item.getProgressTime() > 0 && item.getStreamEntity().getDuration() > 0) {
if (item.getProgressMillis() > 0 && item.getStreamEntity().getDuration() > 0) {
itemProgressView.setMax((int) item.getStreamEntity().getDuration());
if (itemProgressView.getVisibility() == View.VISIBLE) {
itemProgressView.setProgressAnimated((int) TimeUnit.MILLISECONDS
.toSeconds(item.getProgressTime()));
.toSeconds(item.getProgressMillis()));
} else {
itemProgressView.setProgress((int) TimeUnit.MILLISECONDS
.toSeconds(item.getProgressTime()));
.toSeconds(item.getProgressMillis()));
ViewUtils.animate(itemProgressView, true, 500);
}
} else if (itemProgressView.getVisibility() == View.VISIBLE) {

View File

@@ -96,11 +96,11 @@ public class LocalStatisticStreamItemHolder extends LocalItemHolder {
R.color.duration_background_color));
itemDurationView.setVisibility(View.VISIBLE);
if (item.getProgressTime() > 0) {
if (item.getProgressMillis() > 0) {
itemProgressView.setVisibility(View.VISIBLE);
itemProgressView.setMax((int) item.getStreamEntity().getDuration());
itemProgressView.setProgress((int) TimeUnit.MILLISECONDS
.toSeconds(item.getProgressTime()));
.toSeconds(item.getProgressMillis()));
} else {
itemProgressView.setVisibility(View.GONE);
}
@@ -140,14 +140,14 @@ public class LocalStatisticStreamItemHolder extends LocalItemHolder {
}
final StreamStatisticsEntry item = (StreamStatisticsEntry) localItem;
if (item.getProgressTime() > 0 && item.getStreamEntity().getDuration() > 0) {
if (item.getProgressMillis() > 0 && item.getStreamEntity().getDuration() > 0) {
itemProgressView.setMax((int) item.getStreamEntity().getDuration());
if (itemProgressView.getVisibility() == View.VISIBLE) {
itemProgressView.setProgressAnimated((int) TimeUnit.MILLISECONDS
.toSeconds(item.getProgressTime()));
.toSeconds(item.getProgressMillis()));
} else {
itemProgressView.setProgress((int) TimeUnit.MILLISECONDS
.toSeconds(item.getProgressTime()));
.toSeconds(item.getProgressMillis()));
ViewUtils.animate(itemProgressView, true, 500);
}
} else if (itemProgressView.getVisibility() == View.VISIBLE) {

View File

@@ -5,6 +5,7 @@ import android.content.Context;
import android.content.DialogInterface;
import android.os.Bundle;
import android.os.Parcelable;
import android.text.InputType;
import android.text.TextUtils;
import android.util.Log;
import android.view.LayoutInflater;
@@ -13,7 +14,6 @@ import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.widget.EditText;
import android.widget.Toast;
import androidx.annotation.NonNull;
@@ -32,6 +32,7 @@ import org.schabi.newpipe.database.history.model.StreamHistoryEntry;
import org.schabi.newpipe.database.playlist.PlaylistStreamEntry;
import org.schabi.newpipe.database.stream.model.StreamEntity;
import org.schabi.newpipe.database.stream.model.StreamStateEntity;
import org.schabi.newpipe.databinding.DialogEditTextBinding;
import org.schabi.newpipe.databinding.LocalPlaylistHeaderBinding;
import org.schabi.newpipe.databinding.PlaylistControlBinding;
import org.schabi.newpipe.error.ErrorInfo;
@@ -44,7 +45,7 @@ import org.schabi.newpipe.local.history.HistoryRecordManager;
import org.schabi.newpipe.player.helper.PlayerHolder;
import org.schabi.newpipe.player.playqueue.PlayQueue;
import org.schabi.newpipe.player.playqueue.SinglePlayQueue;
import org.schabi.newpipe.util.KoreUtil;
import org.schabi.newpipe.util.external_communication.KoreUtils;
import org.schabi.newpipe.util.Localization;
import org.schabi.newpipe.util.NavigationHelper;
import org.schabi.newpipe.util.OnClickGesture;
@@ -68,6 +69,7 @@ import io.reactivex.rxjava3.subjects.PublishSubject;
import static org.schabi.newpipe.extractor.utils.Utils.isNullOrEmpty;
import static org.schabi.newpipe.ktx.ViewUtils.animate;
import static org.schabi.newpipe.util.ThemeHelper.shouldUseGridLayout;
public class LocalPlaylistFragment extends BaseLocalListFragment<List<PlaylistStreamEntry>, Void> {
// Save the list 10 seconds after the last change occurred
@@ -248,7 +250,8 @@ public class LocalPlaylistFragment extends BaseLocalListFragment<List<PlaylistSt
}
@Override
public void onCreateOptionsMenu(final Menu menu, final MenuInflater inflater) {
public void onCreateOptionsMenu(@NonNull final Menu menu,
@NonNull final MenuInflater inflater) {
if (DEBUG) {
Log.d(TAG, "onCreateOptionsMenu() called with: "
+ "menu = [" + menu + "], inflater = [" + inflater + "]");
@@ -524,18 +527,20 @@ public class LocalPlaylistFragment extends BaseLocalListFragment<List<PlaylistSt
return;
}
final View dialogView = View.inflate(getContext(), R.layout.dialog_playlist_name, null);
final EditText nameEdit = dialogView.findViewById(R.id.playlist_name);
nameEdit.setText(name);
nameEdit.setSelection(nameEdit.getText().length());
final DialogEditTextBinding dialogBinding
= DialogEditTextBinding.inflate(getLayoutInflater());
dialogBinding.dialogEditText.setHint(R.string.name);
dialogBinding.dialogEditText.setInputType(InputType.TYPE_CLASS_TEXT);
dialogBinding.dialogEditText.setSelection(dialogBinding.dialogEditText.getText().length());
dialogBinding.dialogEditText.setText(name);
final AlertDialog.Builder dialogBuilder = new AlertDialog.Builder(getContext())
.setTitle(R.string.rename_playlist)
.setView(dialogView)
.setView(dialogBinding.getRoot())
.setCancelable(true)
.setNegativeButton(R.string.cancel, null)
.setPositiveButton(R.string.rename, (dialogInterface, i) ->
changePlaylistName(nameEdit.getText().toString()));
changePlaylistName(dialogBinding.dialogEditText.getText().toString()));
dialogBuilder.show();
}
@@ -677,7 +682,7 @@ public class LocalPlaylistFragment extends BaseLocalListFragment<List<PlaylistSt
private ItemTouchHelper.SimpleCallback getItemTouchCallback() {
int directions = ItemTouchHelper.UP | ItemTouchHelper.DOWN;
if (isGridLayout()) {
if (shouldUseGridLayout(requireContext())) {
directions |= ItemTouchHelper.LEFT | ItemTouchHelper.RIGHT;
}
return new ItemTouchHelper.SimpleCallback(directions,
@@ -748,7 +753,7 @@ public class LocalPlaylistFragment extends BaseLocalListFragment<List<PlaylistSt
final ArrayList<StreamDialogEntry> entries = new ArrayList<>();
if (PlayerHolder.getType() != null) {
if (PlayerHolder.getInstance().getType() != null) {
entries.add(StreamDialogEntry.enqueue);
}
if (infoItem.getStreamType() == StreamType.AUDIO_STREAM) {
@@ -770,7 +775,7 @@ public class LocalPlaylistFragment extends BaseLocalListFragment<List<PlaylistSt
));
}
entries.add(StreamDialogEntry.open_in_browser);
if (KoreUtil.shouldShowPlayWithKodi(context, infoItem.getServiceId())) {
if (KoreUtils.shouldShowPlayWithKodi(context, infoItem.getServiceId())) {
entries.add(StreamDialogEntry.play_with_kodi);
}

View File

@@ -23,13 +23,9 @@ public class ImportConfirmationDialog extends DialogFragment {
public static void show(@NonNull final Fragment fragment,
@NonNull final Intent resultServiceIntent) {
if (fragment.getFragmentManager() == null) {
return;
}
final ImportConfirmationDialog confirmationDialog = new ImportConfirmationDialog();
confirmationDialog.setResultServiceIntent(resultServiceIntent);
confirmationDialog.show(fragment.getFragmentManager(), null);
confirmationDialog.show(fragment.getParentFragmentManager(), null);
}
public void setResultServiceIntent(final Intent resultServiceIntent) {

View File

@@ -6,9 +6,7 @@ import android.content.Context
import android.content.DialogInterface
import android.content.Intent
import android.content.IntentFilter
import android.content.res.Configuration
import android.os.Bundle
import android.os.Environment
import android.os.Parcelable
import android.view.LayoutInflater
import android.view.Menu
@@ -16,12 +14,12 @@ import android.view.MenuInflater
import android.view.View
import android.view.ViewGroup
import android.widget.Toast
import androidx.activity.result.ActivityResult
import androidx.activity.result.contract.ActivityResultContracts.StartActivityForResult
import androidx.appcompat.app.AlertDialog
import androidx.lifecycle.ViewModelProvider
import androidx.localbroadcastmanager.content.LocalBroadcastManager
import androidx.preference.PreferenceManager
import androidx.recyclerview.widget.GridLayoutManager
import com.nononsenseapps.filepicker.Utils
import com.xwray.groupie.Group
import com.xwray.groupie.GroupAdapter
import com.xwray.groupie.Item
@@ -52,22 +50,20 @@ import org.schabi.newpipe.local.subscription.item.HeaderWithMenuItem
import org.schabi.newpipe.local.subscription.item.HeaderWithMenuItem.Companion.PAYLOAD_UPDATE_VISIBILITY_MENU_ITEM
import org.schabi.newpipe.local.subscription.services.SubscriptionsExportService
import org.schabi.newpipe.local.subscription.services.SubscriptionsExportService.EXPORT_COMPLETE_ACTION
import org.schabi.newpipe.local.subscription.services.SubscriptionsExportService.KEY_FILE_PATH
import org.schabi.newpipe.local.subscription.services.SubscriptionsImportService
import org.schabi.newpipe.local.subscription.services.SubscriptionsImportService.IMPORT_COMPLETE_ACTION
import org.schabi.newpipe.local.subscription.services.SubscriptionsImportService.KEY_MODE
import org.schabi.newpipe.local.subscription.services.SubscriptionsImportService.KEY_VALUE
import org.schabi.newpipe.local.subscription.services.SubscriptionsImportService.PREVIOUS_EXPORT_MODE
import org.schabi.newpipe.util.FilePickerActivityHelper
import org.schabi.newpipe.streams.io.StoredFileHelper
import org.schabi.newpipe.util.NavigationHelper
import org.schabi.newpipe.util.OnClickGesture
import org.schabi.newpipe.util.ShareUtils
import java.io.File
import org.schabi.newpipe.util.ThemeHelper.getGridSpanCountChannels
import org.schabi.newpipe.util.ThemeHelper.shouldUseGridLayout
import org.schabi.newpipe.util.external_communication.ShareUtils
import java.text.SimpleDateFormat
import java.util.Date
import java.util.Locale
import kotlin.math.floor
import kotlin.math.max
class SubscriptionFragment : BaseStateFragment<SubscriptionState>() {
private var _binding: FragmentSubscriptionBinding? = null
@@ -86,6 +82,11 @@ class SubscriptionFragment : BaseStateFragment<SubscriptionState>() {
private lateinit var feedGroupsSortMenuItem: HeaderWithMenuItem
private val subscriptionsSection = Section()
private val requestExportLauncher =
registerForActivityResult(StartActivityForResult(), this::requestExportResult)
private val requestImportLauncher =
registerForActivityResult(StartActivityForResult(), this::requestImportResult)
@State
@JvmField
var itemsListState: Parcelable? = null
@@ -109,13 +110,6 @@ class SubscriptionFragment : BaseStateFragment<SubscriptionState>() {
setupInitialLayout()
}
override fun setUserVisibleHint(isVisibleToUser: Boolean) {
super.setUserVisibleHint(isVisibleToUser)
if (activity != null && isVisibleToUser) {
setTitle(activity.getString(R.string.tab_subscriptions))
}
}
override fun onAttach(context: Context) {
super.onAttach(context)
subscriptionManager = SubscriptionManager(requireContext())
@@ -153,11 +147,8 @@ class SubscriptionFragment : BaseStateFragment<SubscriptionState>() {
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
super.onCreateOptionsMenu(menu, inflater)
val supportActionBar = activity.supportActionBar
if (supportActionBar != null) {
supportActionBar.setDisplayShowTitleEnabled(true)
setTitle(getString(R.string.tab_subscriptions))
}
activity.supportActionBar?.setDisplayShowTitleEnabled(true)
activity.supportActionBar?.setTitle(R.string.tab_subscriptions)
}
private fun setupBroadcastReceiver() {
@@ -188,44 +179,39 @@ class SubscriptionFragment : BaseStateFragment<SubscriptionState>() {
}
private fun onImportPreviousSelected() {
startActivityForResult(FilePickerActivityHelper.chooseSingleFile(activity), REQUEST_IMPORT_CODE)
requestImportLauncher.launch(StoredFileHelper.getPicker(activity))
}
private fun onExportSelected() {
val date = SimpleDateFormat("yyyyMMddHHmm", Locale.ENGLISH).format(Date())
val exportName = "newpipe_subscriptions_$date.json"
val exportFile = File(Environment.getExternalStorageDirectory(), exportName)
startActivityForResult(FilePickerActivityHelper.chooseFileToSave(activity, exportFile.absolutePath), REQUEST_EXPORT_CODE)
requestExportLauncher.launch(
StoredFileHelper.getNewPicker(activity, exportName, "application/json", null)
)
}
private fun openReorderDialog() {
FeedGroupReorderDialog().show(requireFragmentManager(), null)
FeedGroupReorderDialog().show(parentFragmentManager, null)
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
if (data != null && data.data != null && resultCode == Activity.RESULT_OK) {
if (requestCode == REQUEST_EXPORT_CODE) {
val exportFile = Utils.getFileForUri(data.data!!)
val parentFile = exportFile.parentFile!!
if (!parentFile.canWrite() || !parentFile.canRead()) {
Toast.makeText(activity, R.string.invalid_directory, Toast.LENGTH_SHORT).show()
} else {
activity.startService(
Intent(activity, SubscriptionsExportService::class.java)
.putExtra(KEY_FILE_PATH, exportFile.absolutePath)
)
}
} else if (requestCode == REQUEST_IMPORT_CODE) {
val path = Utils.getFileForUri(data.data!!).absolutePath
ImportConfirmationDialog.show(
this,
Intent(activity, SubscriptionsImportService::class.java)
.putExtra(KEY_MODE, PREVIOUS_EXPORT_MODE)
.putExtra(KEY_VALUE, path)
)
}
fun requestExportResult(result: ActivityResult) {
if (result.data != null && result.resultCode == Activity.RESULT_OK) {
activity.startService(
Intent(activity, SubscriptionsExportService::class.java)
.putExtra(SubscriptionsExportService.KEY_FILE_PATH, result.data?.data)
)
}
}
fun requestImportResult(result: ActivityResult) {
if (result.data != null && result.resultCode == Activity.RESULT_OK) {
ImportConfirmationDialog.show(
this,
Intent(activity, SubscriptionsImportService::class.java)
.putExtra(KEY_MODE, PREVIOUS_EXPORT_MODE)
.putExtra(KEY_VALUE, result.data?.data)
)
}
}
@@ -281,8 +267,7 @@ class SubscriptionFragment : BaseStateFragment<SubscriptionState>() {
super.initViews(rootView, savedInstanceState)
_binding = FragmentSubscriptionBinding.bind(rootView)
val shouldUseGridLayout = shouldUseGridLayout()
groupAdapter.spanCount = if (shouldUseGridLayout) getGridSpanCount() else 1
groupAdapter.spanCount = if (shouldUseGridLayout(context)) getGridSpanCountChannels(context) else 1
binding.itemsList.layoutManager = GridLayoutManager(requireContext(), groupAdapter.spanCount).apply {
spanSizeLookup = groupAdapter.spanSizeLookup
}
@@ -295,13 +280,17 @@ class SubscriptionFragment : BaseStateFragment<SubscriptionState>() {
private fun showLongTapDialog(selectedItem: ChannelInfoItem) {
val commands = arrayOf(
getString(R.string.share), getString(R.string.open_in_browser),
getString(R.string.share),
getString(R.string.open_in_browser),
getString(R.string.unsubscribe)
)
val actions = DialogInterface.OnClickListener { _, i ->
when (i) {
0 -> ShareUtils.shareText(requireContext(), selectedItem.name, selectedItem.url)
0 -> ShareUtils.shareText(
requireContext(), selectedItem.name, selectedItem.url,
selectedItem.thumbnailUrl
)
1 -> ShareUtils.openUrlInBrowser(requireContext(), selectedItem.url)
2 -> deleteChannel(selectedItem)
}
@@ -357,7 +346,7 @@ class SubscriptionFragment : BaseStateFragment<SubscriptionState>() {
override fun handleResult(result: SubscriptionState) {
super.handleResult(result)
val shouldUseGridLayout = shouldUseGridLayout()
val shouldUseGridLayout = shouldUseGridLayout(context)
when (result) {
is SubscriptionState.LoadedState -> {
result.subscriptions.forEach {
@@ -418,35 +407,4 @@ class SubscriptionFragment : BaseStateFragment<SubscriptionState>() {
super.hideLoading()
binding.itemsList.animate(true, 200)
}
// /////////////////////////////////////////////////////////////////////////
// Grid Mode
// /////////////////////////////////////////////////////////////////////////
// TODO: Move these out of this class, as it can be reused
private fun shouldUseGridLayout(): Boolean {
val listMode = PreferenceManager.getDefaultSharedPreferences(requireContext())
.getString(getString(R.string.list_view_mode_key), getString(R.string.list_view_mode_value))
return when (listMode) {
getString(R.string.list_view_mode_auto_key) -> {
val configuration = resources.configuration
configuration.orientation == Configuration.ORIENTATION_LANDSCAPE &&
configuration.isLayoutSizeAtLeast(Configuration.SCREENLAYOUT_SIZE_LARGE)
}
getString(R.string.list_view_mode_grid_key) -> true
else -> false
}
}
private fun getGridSpanCount(): Int {
val minWidth = resources.getDimensionPixelSize(R.dimen.channel_item_grid_min_width)
return max(1, floor(resources.displayMetrics.widthPixels / minWidth.toDouble()).toInt())
}
companion object {
private const val REQUEST_EXPORT_CODE = 666
private const val REQUEST_IMPORT_CODE = 667
}
}

View File

@@ -12,14 +12,15 @@ import android.widget.Button;
import android.widget.EditText;
import android.widget.TextView;
import androidx.activity.result.ActivityResult;
import androidx.activity.result.ActivityResultLauncher;
import androidx.activity.result.contract.ActivityResultContracts.StartActivityForResult;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.StringRes;
import androidx.appcompat.app.ActionBar;
import androidx.core.text.util.LinkifyCompat;
import com.nononsenseapps.filepicker.Utils;
import org.schabi.newpipe.BaseFragment;
import org.schabi.newpipe.R;
import org.schabi.newpipe.error.ErrorActivity;
@@ -29,8 +30,8 @@ import org.schabi.newpipe.extractor.NewPipe;
import org.schabi.newpipe.extractor.exceptions.ExtractionException;
import org.schabi.newpipe.extractor.subscription.SubscriptionExtractor;
import org.schabi.newpipe.local.subscription.services.SubscriptionsImportService;
import org.schabi.newpipe.streams.io.StoredFileHelper;
import org.schabi.newpipe.util.Constants;
import org.schabi.newpipe.util.FilePickerActivityHelper;
import org.schabi.newpipe.util.ServiceHelper;
import java.util.Collections;
@@ -45,8 +46,6 @@ import static org.schabi.newpipe.local.subscription.services.SubscriptionsImport
import static org.schabi.newpipe.local.subscription.services.SubscriptionsImportService.KEY_VALUE;
public class SubscriptionsImportFragment extends BaseFragment {
private static final int REQUEST_IMPORT_FILE_CODE = 666;
@State
int currentServiceId = Constants.NO_SERVICE_ID;
@@ -64,6 +63,9 @@ public class SubscriptionsImportFragment extends BaseFragment {
private EditText inputText;
private Button inputButton;
private final ActivityResultLauncher<Intent> requestImportFileLauncher =
registerForActivityResult(new StartActivityForResult(), this::requestImportFileResult);
public static SubscriptionsImportFragment getInstance(final int serviceId) {
final SubscriptionsImportFragment instance = new SubscriptionsImportFragment();
instance.setInitialData(serviceId);
@@ -175,23 +177,19 @@ public class SubscriptionsImportFragment extends BaseFragment {
}
public void onImportFile() {
startActivityForResult(FilePickerActivityHelper.chooseSingleFile(activity),
REQUEST_IMPORT_FILE_CODE);
requestImportFileLauncher.launch(StoredFileHelper.getPicker(activity));
}
@Override
public void onActivityResult(final int requestCode, final int resultCode, final Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if (data == null) {
private void requestImportFileResult(final ActivityResult result) {
if (result.getData() == null) {
return;
}
if (resultCode == Activity.RESULT_OK && requestCode == REQUEST_IMPORT_FILE_CODE
&& data.getData() != null) {
final String path = Utils.getFileForUri(data.getData()).getAbsolutePath();
if (result.getResultCode() == Activity.RESULT_OK && result.getData().getData() != null) {
ImportConfirmationDialog.show(this,
new Intent(activity, SubscriptionsImportService.class)
.putExtra(KEY_MODE, INPUT_STREAM_MODE).putExtra(KEY_VALUE, path)
.putExtra(KEY_MODE, INPUT_STREAM_MODE)
.putExtra(KEY_VALUE, result.getData().getData())
.putExtra(Constants.KEY_SERVICE_ID, currentServiceId));
}
}

View File

@@ -20,7 +20,7 @@
package org.schabi.newpipe.local.subscription.services;
import android.content.Intent;
import android.text.TextUtils;
import android.net.Uri;
import android.util.Log;
import androidx.localbroadcastmanager.content.LocalBroadcastManager;
@@ -31,10 +31,11 @@ import org.schabi.newpipe.App;
import org.schabi.newpipe.R;
import org.schabi.newpipe.database.subscription.SubscriptionEntity;
import org.schabi.newpipe.extractor.subscription.SubscriptionItem;
import org.schabi.newpipe.streams.io.SharpOutputStream;
import org.schabi.newpipe.streams.io.StoredFileHelper;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.List;
@@ -55,8 +56,8 @@ public class SubscriptionsExportService extends BaseImportExportService {
+ ".services.SubscriptionsExportService.EXPORT_COMPLETE";
private Subscription subscription;
private File outFile;
private FileOutputStream outputStream;
private StoredFileHelper outFile;
private OutputStream outputStream;
@Override
public int onStartCommand(final Intent intent, final int flags, final int startId) {
@@ -64,18 +65,18 @@ public class SubscriptionsExportService extends BaseImportExportService {
return START_NOT_STICKY;
}
final String path = intent.getStringExtra(KEY_FILE_PATH);
if (TextUtils.isEmpty(path)) {
final Uri path = intent.getParcelableExtra(KEY_FILE_PATH);
if (path == null) {
stopAndReportError(new IllegalStateException(
"Exporting to a file, but the path is empty or null"),
"Exporting to a file, but the path is null"),
"Exporting subscriptions");
return START_NOT_STICKY;
}
try {
outFile = new File(path);
outputStream = new FileOutputStream(outFile);
} catch (final FileNotFoundException e) {
outFile = new StoredFileHelper(this, path, "application/json");
outputStream = new SharpOutputStream(outFile.getStream());
} catch (final IOException e) {
handleError(e);
return START_NOT_STICKY;
}
@@ -122,8 +123,8 @@ public class SubscriptionsExportService extends BaseImportExportService {
.subscribe(getSubscriber());
}
private Subscriber<File> getSubscriber() {
return new Subscriber<File>() {
private Subscriber<StoredFileHelper> getSubscriber() {
return new Subscriber<StoredFileHelper>() {
@Override
public void onSubscribe(final Subscription s) {
subscription = s;
@@ -131,7 +132,7 @@ public class SubscriptionsExportService extends BaseImportExportService {
}
@Override
public void onNext(final File file) {
public void onNext(final StoredFileHelper file) {
if (DEBUG) {
Log.d(TAG, "startExport() success: file = " + file);
}
@@ -153,7 +154,7 @@ public class SubscriptionsExportService extends BaseImportExportService {
};
}
private Function<List<SubscriptionItem>, File> exportToFile() {
private Function<List<SubscriptionItem>, StoredFileHelper> exportToFile() {
return subscriptionItems -> {
ImportExportJsonHelper.writeTo(subscriptionItems, outputStream, eventListener);
return outFile;

View File

@@ -20,6 +20,7 @@
package org.schabi.newpipe.local.subscription.services;
import android.content.Intent;
import android.net.Uri;
import android.text.TextUtils;
import android.util.Log;
@@ -36,12 +37,11 @@ import org.schabi.newpipe.extractor.NewPipe;
import org.schabi.newpipe.extractor.channel.ChannelInfo;
import org.schabi.newpipe.extractor.subscription.SubscriptionItem;
import org.schabi.newpipe.ktx.ExceptionUtils;
import org.schabi.newpipe.streams.io.SharpInputStream;
import org.schabi.newpipe.streams.io.StoredFileHelper;
import org.schabi.newpipe.util.Constants;
import org.schabi.newpipe.util.ExtractorHelper;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
@@ -55,6 +55,7 @@ import io.reactivex.rxjava3.functions.Function;
import io.reactivex.rxjava3.schedulers.Schedulers;
import static org.schabi.newpipe.MainActivity.DEBUG;
import static org.schabi.newpipe.streams.io.StoredFileHelper.DEFAULT_MIME;
public class SubscriptionsImportService extends BaseImportExportService {
public static final int CHANNEL_URL_MODE = 0;
@@ -101,17 +102,18 @@ public class SubscriptionsImportService extends BaseImportExportService {
if (currentMode == CHANNEL_URL_MODE) {
channelUrl = intent.getStringExtra(KEY_VALUE);
} else {
final String filePath = intent.getStringExtra(KEY_VALUE);
if (TextUtils.isEmpty(filePath)) {
final Uri uri = intent.getParcelableExtra(KEY_VALUE);
if (uri == null) {
stopAndReportError(new IllegalStateException(
"Importing from input stream, but file path is empty or null"),
"Importing from input stream, but file path is null"),
"Importing subscriptions");
return START_NOT_STICKY;
}
try {
inputStream = new FileInputStream(new File(filePath));
} catch (final FileNotFoundException e) {
inputStream = new SharpInputStream(
new StoredFileHelper(this, uri, DEFAULT_MIME).getStream());
} catch (final IOException e) {
handleError(e);
return START_NOT_STICKY;
}

View File

@@ -178,7 +178,10 @@ public final class MainPlayer extends Service {
if (DEBUG) {
Log.d(TAG, "destroy() called");
}
cleanup();
}
private void cleanup() {
if (player != null) {
// Exit from fullscreen when user closes the player via notification
if (player.isFullscreen()) {
@@ -191,9 +194,14 @@ public final class MainPlayer extends Service {
player.stopActivityBinding();
player.removePopupFromView();
player.destroy();
}
player = null;
}
}
public void stopService() {
NotificationUtil.getInstance().cancelNotificationAndStopForeground(this);
cleanup();
stopSelf();
}

View File

@@ -1,5 +1,9 @@
package org.schabi.newpipe.player;
import static org.schabi.newpipe.player.helper.PlayerHelper.formatSpeed;
import static org.schabi.newpipe.util.Localization.assureCorrectAppLanguage;
import static org.schabi.newpipe.util.external_communication.ShareUtils.shareText;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
@@ -40,15 +44,12 @@ import org.schabi.newpipe.player.playqueue.PlayQueueItemTouchCallback;
import org.schabi.newpipe.util.Localization;
import org.schabi.newpipe.util.NavigationHelper;
import org.schabi.newpipe.util.PermissionHelper;
import org.schabi.newpipe.util.ServiceHelper;
import org.schabi.newpipe.util.ThemeHelper;
import java.util.Collections;
import java.util.List;
import static org.schabi.newpipe.player.helper.PlayerHelper.formatSpeed;
import static org.schabi.newpipe.util.Localization.assureCorrectAppLanguage;
import static org.schabi.newpipe.util.ShareUtils.shareText;
public final class PlayQueueActivity extends AppCompatActivity
implements PlayerEventListener, SeekBar.OnSeekBarChangeListener,
View.OnClickListener, PlaybackParameterDialog.Callback {
@@ -83,7 +84,7 @@ public final class PlayQueueActivity extends AppCompatActivity
protected void onCreate(final Bundle savedInstanceState) {
assureCorrectAppLanguage(this);
super.onCreate(savedInstanceState);
ThemeHelper.setTheme(this);
ThemeHelper.setTheme(this, ServiceHelper.getSelectedServiceId(this));
queueControlBinding = ActivityPlayerQueueControlBinding.inflate(getLayoutInflater());
setContentView(queueControlBinding.getRoot());
@@ -313,7 +314,8 @@ public final class PlayQueueActivity extends AppCompatActivity
final MenuItem share = popupMenu.getMenu().add(RECYCLER_ITEM_POPUP_MENU_GROUP_ID, 3,
Menu.NONE, R.string.share);
share.setOnMenuItemClickListener(menuItem -> {
shareText(getApplicationContext(), item.getTitle(), item.getUrl());
shareText(getApplicationContext(), item.getTitle(), item.getUrl(),
item.getThumbnailUrl());
return true;
});

View File

@@ -27,12 +27,13 @@ import android.util.DisplayMetrics;
import android.util.Log;
import android.util.TypedValue;
import android.view.ContextThemeWrapper;
import android.view.GestureDetector;
import android.view.Gravity;
import android.view.KeyEvent;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuItem;
import android.view.MotionEvent;
import android.view.Surface;
import android.view.View;
import android.view.ViewGroup;
import android.view.WindowManager;
@@ -54,8 +55,10 @@ import androidx.appcompat.app.AppCompatActivity;
import androidx.appcompat.content.res.AppCompatResources;
import androidx.appcompat.widget.AppCompatImageButton;
import androidx.core.content.ContextCompat;
import androidx.core.view.DisplayCutoutCompat;
import androidx.core.graphics.Insets;
import androidx.core.view.GestureDetectorCompat;
import androidx.core.view.ViewCompat;
import androidx.core.view.WindowInsetsCompat;
import androidx.preference.PreferenceManager;
import androidx.recyclerview.widget.ItemTouchHelper;
import androidx.recyclerview.widget.RecyclerView;
@@ -112,6 +115,7 @@ import org.schabi.newpipe.player.playback.CustomTrackSelector;
import org.schabi.newpipe.player.playback.MediaSourceManager;
import org.schabi.newpipe.player.playback.PlaybackListener;
import org.schabi.newpipe.player.playback.PlayerMediaSession;
import org.schabi.newpipe.player.playback.SurfaceHolderCallback;
import org.schabi.newpipe.player.playqueue.PlayQueue;
import org.schabi.newpipe.player.playqueue.PlayQueueAdapter;
import org.schabi.newpipe.player.playqueue.PlayQueueItem;
@@ -121,13 +125,15 @@ import org.schabi.newpipe.player.playqueue.PlayQueueItemTouchCallback;
import org.schabi.newpipe.player.resolver.AudioPlaybackResolver;
import org.schabi.newpipe.player.resolver.MediaSourceTag;
import org.schabi.newpipe.player.resolver.VideoPlaybackResolver;
import org.schabi.newpipe.player.seekbarpreview.SeekbarPreviewThumbnailHelper;
import org.schabi.newpipe.player.seekbarpreview.SeekbarPreviewThumbnailHolder;
import org.schabi.newpipe.util.DeviceUtils;
import org.schabi.newpipe.util.ImageDisplayConstants;
import org.schabi.newpipe.util.KoreUtil;
import org.schabi.newpipe.util.ListHelper;
import org.schabi.newpipe.util.NavigationHelper;
import org.schabi.newpipe.util.SerializedCache;
import org.schabi.newpipe.util.ShareUtils;
import org.schabi.newpipe.util.external_communication.KoreUtils;
import org.schabi.newpipe.util.external_communication.ShareUtils;
import org.schabi.newpipe.views.ExpandableSurfaceView;
import java.io.IOException;
@@ -267,6 +273,7 @@ public final class Player implements
private SimpleExoPlayer simpleExoPlayer;
private AudioReactor audioReactor;
private MediaSessionManager mediaSessionManager;
@Nullable private SurfaceHolderCallback surfaceHolderCallback;
@NonNull private final CustomTrackSelector trackSelector;
@NonNull private final LoadController loadController;
@@ -353,7 +360,7 @@ public final class Player implements
private static final float MAX_GESTURE_LENGTH = 0.75f;
private int maxGestureLength; // scaled
private GestureDetector gestureDetector;
private GestureDetectorCompat gestureDetector;
/*//////////////////////////////////////////////////////////////////////////
// Listeners and disposables
@@ -376,6 +383,8 @@ public final class Player implements
@NonNull private final SharedPreferences prefs;
@NonNull private final HistoryRecordManager recordManager;
@NonNull private final SeekbarPreviewThumbnailHolder seekbarPreviewThumbnailHolder =
new SeekbarPreviewThumbnailHolder();
/*//////////////////////////////////////////////////////////////////////////
@@ -489,7 +498,7 @@ public final class Player implements
registerBroadcastReceiver();
// Setup video view
simpleExoPlayer.setVideoSurfaceView(binding.surfaceView);
setupVideoSurface();
simpleExoPlayer.addVideoListener(this);
// Setup subtitle view
@@ -517,7 +526,7 @@ public final class Player implements
binding.playbackLiveSync.setOnClickListener(this);
final PlayerGestureListener listener = new PlayerGestureListener(this, service);
gestureDetector = new GestureDetector(context, listener);
gestureDetector = new GestureDetectorCompat(context, listener);
binding.getRoot().setOnTouchListener(listener);
binding.queueButton.setOnClickListener(this);
@@ -532,6 +541,7 @@ public final class Player implements
binding.moreOptionsButton.setOnClickListener(this);
binding.moreOptionsButton.setOnLongClickListener(this);
binding.share.setOnClickListener(this);
binding.share.setOnLongClickListener(this);
binding.fullScreenButton.setOnClickListener(this);
binding.screenRotationButton.setOnClickListener(this);
binding.playWithKodi.setOnClickListener(this);
@@ -551,10 +561,9 @@ public final class Player implements
binding.getRoot().addOnLayoutChangeListener(this::onLayoutChange);
ViewCompat.setOnApplyWindowInsetsListener(binding.itemsListPanel, (view, windowInsets) -> {
final DisplayCutoutCompat cutout = windowInsets.getDisplayCutout();
if (cutout != null) {
view.setPadding(cutout.getSafeInsetLeft(), cutout.getSafeInsetTop(),
cutout.getSafeInsetRight(), cutout.getSafeInsetBottom());
final Insets cutout = windowInsets.getInsets(WindowInsetsCompat.Type.displayCutout());
if (!cutout.equals(Insets.NONE)) {
view.setPadding(cutout.left, cutout.top, cutout.right, cutout.bottom);
}
return windowInsets;
});
@@ -670,7 +679,11 @@ public final class Player implements
//.doFinally()
.subscribe(
state -> {
newQueue.setRecovery(newQueue.getIndex(), state.getProgressTime());
if (!state.isFinished(newQueue.getItem().getDuration())) {
// resume playback only if the stream was not played to the end
newQueue.setRecovery(newQueue.getIndex(),
state.getProgressMillis());
}
initPlayback(newQueue, repeatMode, playbackSpeed, playbackPitch,
playbackSkipSilence, playWhenReady, isMuted);
},
@@ -767,8 +780,12 @@ public final class Player implements
if (DEBUG) {
Log.d(TAG, "destroyPlayer() called");
}
cleanupVideoSurface();
if (!exoPlayerIsNull()) {
simpleExoPlayer.removeListener(this);
simpleExoPlayer.removeVideoListener(this);
simpleExoPlayer.stop();
simpleExoPlayer.release();
}
@@ -852,7 +869,7 @@ public final class Player implements
Log.d(TAG, "onPlaybackShutdown() called");
}
// destroys the service, which in turn will destroy the player
service.onDestroy();
service.stopService();
}
public void smoothStopPlayer() {
@@ -1032,7 +1049,7 @@ public final class Player implements
// show kodi button if it supports the current service and it is enabled in settings
binding.playWithKodi.setVisibility(videoPlayerSelected()
&& playQueue != null && playQueue.getItem() != null
&& KoreUtil.shouldShowPlayWithKodi(context, playQueue.getItem().getServiceId())
&& KoreUtils.shouldShowPlayWithKodi(context, playQueue.getItem().getServiceId())
? View.VISIBLE : View.GONE);
}
//endregion
@@ -1092,7 +1109,7 @@ public final class Player implements
pause();
break;
case ACTION_CLOSE:
service.onDestroy();
service.stopService();
break;
case ACTION_PLAY_PAUSE:
playPause();
@@ -1493,7 +1510,7 @@ public final class Player implements
Objects.requireNonNull(windowManager)
.removeView(closeOverlayBinding.getRoot());
closeOverlayBinding = null;
service.onDestroy();
service.stopService();
}
}).start();
}
@@ -1664,12 +1681,67 @@ public final class Player implements
@Override // seekbar listener
public void onProgressChanged(final SeekBar seekBar, final int progress,
final boolean fromUser) {
if (DEBUG && fromUser) {
// Currently we don't need method execution when fromUser is false
if (!fromUser) {
return;
}
if (DEBUG) {
Log.d(TAG, "onProgressChanged() called with: "
+ "seekBar = [" + seekBar + "], progress = [" + progress + "]");
}
if (fromUser) {
binding.currentDisplaySeek.setText(getTimeString(progress));
binding.currentDisplaySeek.setText(getTimeString(progress));
// Seekbar Preview Thumbnail
SeekbarPreviewThumbnailHelper
.tryResizeAndSetSeekbarPreviewThumbnail(
getContext(),
seekbarPreviewThumbnailHolder.getBitmapAt(progress),
binding.currentSeekbarPreviewThumbnail,
binding.subtitleView::getWidth);
adjustSeekbarPreviewContainer();
}
private void adjustSeekbarPreviewContainer() {
try {
// Should only be required when an error occurred before
// and the layout was positioned in the center
binding.bottomSeekbarPreviewLayout.setGravity(Gravity.NO_GRAVITY);
// Calculate the current left position of seekbar progress in px
// More info: https://stackoverflow.com/q/20493577
final int currentSeekbarLeft =
binding.playbackSeekBar.getLeft()
+ binding.playbackSeekBar.getPaddingLeft()
+ binding.playbackSeekBar.getThumb().getBounds().left;
// Calculate the (unchecked) left position of the container
final int uncheckedContainerLeft =
currentSeekbarLeft - (binding.seekbarPreviewContainer.getWidth() / 2);
// Fix the position so it's within the boundaries
final int checkedContainerLeft =
Math.max(
Math.min(
uncheckedContainerLeft,
// Max left
binding.playbackWindowRoot.getWidth()
- binding.seekbarPreviewContainer.getWidth()
),
0 // Min left
);
// See also: https://stackoverflow.com/a/23249734
final LinearLayout.LayoutParams params =
new LinearLayout.LayoutParams(
binding.seekbarPreviewContainer.getLayoutParams());
params.setMarginStart(checkedContainerLeft);
binding.seekbarPreviewContainer.setLayoutParams(params);
} catch (final Exception ex) {
Log.e(TAG, "Failed to adjust seekbarPreviewContainer", ex);
// Fallback - position in the middle
binding.bottomSeekbarPreviewLayout.setGravity(Gravity.CENTER);
}
}
@@ -1690,6 +1762,8 @@ public final class Player implements
showControls(0);
animate(binding.currentDisplaySeek, true, DEFAULT_CONTROLS_DURATION,
AnimationType.SCALE_AND_ALPHA);
animate(binding.currentSeekbarPreviewThumbnail, true, DEFAULT_CONTROLS_DURATION,
AnimationType.SCALE_AND_ALPHA);
}
@Override // seekbar listener
@@ -1705,6 +1779,7 @@ public final class Player implements
binding.playbackCurrentTime.setText(getTimeString(seekBar.getProgress()));
animate(binding.currentDisplaySeek, false, 200, AnimationType.SCALE_AND_ALPHA);
animate(binding.currentSeekbarPreviewThumbnail, false, 200, AnimationType.SCALE_AND_ALPHA);
if (currentState == STATE_PAUSED_SEEK) {
changeState(STATE_BUFFERING);
@@ -1934,9 +2009,7 @@ public final class Player implements
break;
case com.google.android.exoplayer2.Player.STATE_ENDED: // 4
changeState(STATE_COMPLETED);
if (currentMetadata != null) {
resetStreamProgressState(currentMetadata.getMetadata());
}
saveStreamProgressStateCompleted();
isPrepared = false;
break;
}
@@ -2157,7 +2230,10 @@ public final class Player implements
private void onCompleted() {
if (DEBUG) {
Log.d(TAG, "onCompleted() called");
Log.d(TAG, "onCompleted() called" + (playQueue == null ? ". playQueue is null" : ""));
}
if (playQueue == null) {
return;
}
animate(binding.playPauseButton, false, 0, AnimationType.SCALE_AND_ALPHA, 0,
@@ -2397,7 +2473,7 @@ public final class Player implements
case DISCONTINUITY_REASON_SEEK_ADJUSTMENT:
case DISCONTINUITY_REASON_INTERNAL:
if (playQueue.getIndex() != newWindowIndex) {
resetStreamProgressState(playQueue.getItem());
saveStreamProgressStateCompleted(); // current stream has ended
playQueue.setIndex(newWindowIndex);
}
break;
@@ -2788,61 +2864,47 @@ public final class Player implements
}
}
private void saveStreamProgressState(final StreamInfo info, final long progress) {
if (info == null) {
private void saveStreamProgressState(final long progressMillis) {
if (currentMetadata == null
|| !prefs.getBoolean(context.getString(R.string.enable_watch_history_key), true)) {
return;
}
if (DEBUG) {
Log.d(TAG, "saveStreamProgressState() called");
Log.d(TAG, "saveStreamProgressState() called with: progressMillis=" + progressMillis
+ ", currentMetadata=[" + currentMetadata.getMetadata().getName() + "]");
}
if (prefs.getBoolean(context.getString(R.string.enable_watch_history_key), true)) {
final Disposable stateSaver = recordManager.saveStreamState(info, progress)
.observeOn(AndroidSchedulers.mainThread())
.doOnError((e) -> {
if (DEBUG) {
e.printStackTrace();
}
})
.onErrorComplete()
.subscribe();
databaseUpdateDisposable.add(stateSaver);
}
}
private void resetStreamProgressState(final PlayQueueItem queueItem) {
if (queueItem == null) {
return;
}
if (prefs.getBoolean(context.getString(R.string.enable_watch_history_key), true)) {
final Disposable stateSaver = queueItem.getStream()
.flatMapCompletable(info -> recordManager.saveStreamState(info, 0))
.observeOn(AndroidSchedulers.mainThread())
.doOnError((e) -> {
if (DEBUG) {
e.printStackTrace();
}
})
.onErrorComplete()
.subscribe();
databaseUpdateDisposable.add(stateSaver);
}
}
private void resetStreamProgressState(final StreamInfo info) {
saveStreamProgressState(info, 0);
databaseUpdateDisposable
.add(recordManager.saveStreamState(currentMetadata.getMetadata(), progressMillis)
.observeOn(AndroidSchedulers.mainThread())
.doOnError((e) -> {
if (DEBUG) {
e.printStackTrace();
}
})
.onErrorComplete()
.subscribe());
}
public void saveStreamProgressState() {
if (exoPlayerIsNull() || currentMetadata == null) {
if (exoPlayerIsNull() || currentMetadata == null || playQueue == null
|| playQueue.getIndex() != simpleExoPlayer.getCurrentWindowIndex()) {
// Make sure play queue and current window index are equal, to prevent saving state for
// the wrong stream on discontinuity (e.g. when the stream just changed but the
// playQueue index and currentMetadata still haven't updated)
return;
}
final StreamInfo currentInfo = currentMetadata.getMetadata();
if (playQueue != null) {
// Save current position. It will help to restore this position once a user
// wants to play prev or next stream from the queue
playQueue.setRecovery(playQueue.getIndex(), simpleExoPlayer.getContentPosition());
// Save current position. It will help to restore this position once a user
// wants to play prev or next stream from the queue
playQueue.setRecovery(playQueue.getIndex(), simpleExoPlayer.getContentPosition());
saveStreamProgressState(simpleExoPlayer.getCurrentPosition());
}
public void saveStreamProgressStateCompleted() {
if (currentMetadata != null) {
// current stream has ended, so the progress is its duration (+1 to overcome rounding)
saveStreamProgressState((currentMetadata.getMetadata().getDuration() + 1) * 1000);
}
saveStreamProgressState(currentInfo, simpleExoPlayer.getCurrentPosition());
}
//endregion
@@ -2867,6 +2929,10 @@ public final class Player implements
binding.titleTextView.setText(tag.getMetadata().getName());
binding.channelTextView.setText(tag.getMetadata().getUploaderName());
this.seekbarPreviewThumbnailHolder.resetFrom(
this.getContext(),
tag.getMetadata().getPreviewFrames());
NotificationUtil.getInstance().createNotificationIfNeededAndUpdate(this, false);
notifyMetadataUpdateToListeners();
@@ -2917,6 +2983,18 @@ public final class Player implements
: currentMetadata.getMetadata().getUrl();
}
@NonNull
private String getVideoUrlAtCurrentTime() {
final int timeSeconds = binding.playbackSeekBar.getProgress() / 1000;
String videoUrl = getVideoUrl();
if (!isLive() && timeSeconds >= 0 && currentMetadata != null
&& currentMetadata.getMetadata().getServiceId() == YouTube.getServiceId()) {
// Timestamp doesn't make sense in a live stream so drop it
videoUrl += ("&t=" + timeSeconds);
}
return videoUrl;
}
@NonNull
public String getVideoTitle() {
return currentMetadata == null
@@ -3580,7 +3658,8 @@ public final class Player implements
} else if (v.getId() == binding.moreOptionsButton.getId()) {
onMoreOptionsClicked();
} else if (v.getId() == binding.share.getId()) {
onShareClicked();
ShareUtils.shareText(context, getVideoTitle(), getVideoUrlAtCurrentTime(),
currentItem.getThumbnailUrl());
} else if (v.getId() == binding.playWithKodi.getId()) {
onPlayWithKodiClicked();
} else if (v.getId() == binding.openInBrowser.getId()) {
@@ -3629,6 +3708,8 @@ public final class Player implements
fragmentListener.onMoreOptionsLongClicked();
hideControls(0, 0);
hideSystemUIIfNeeded();
} else if (v.getId() == binding.share.getId()) {
ShareUtils.copyToClipboard(context, getVideoUrlAtCurrentTime());
}
return true;
}
@@ -3700,19 +3781,6 @@ public final class Player implements
showControls(DEFAULT_CONTROLS_DURATION);
}
private void onShareClicked() {
// share video at the current time (youtube.com/watch?v=ID&t=SECONDS)
// Timestamp doesn't make sense in a live stream so drop it
final int ts = binding.playbackSeekBar.getProgress() / 1000;
String videoUrl = getVideoUrl();
if (!isLive() && ts >= 0 && currentMetadata != null
&& currentMetadata.getMetadata().getServiceId() == YouTube.getServiceId()) {
videoUrl += ("&t=" + ts);
}
ShareUtils.shareText(context, getVideoTitle(), videoUrl);
}
private void onPlayWithKodiClicked() {
if (currentMetadata != null) {
pause();
@@ -3722,7 +3790,7 @@ public final class Player implements
if (DEBUG) {
Log.i(TAG, "Failed to start kore", e);
}
KoreUtil.showInstallKoreDialog(getParentActivity());
KoreUtils.showInstallKoreDialog(getParentActivity());
}
}
}
@@ -4131,7 +4199,7 @@ public final class Player implements
return audioReactor;
}
public GestureDetector getGestureDetector() {
public GestureDetectorCompat getGestureDetector() {
return gestureDetector;
}
@@ -4229,6 +4297,39 @@ public final class Player implements
public PlayQueueAdapter getPlayQueueAdapter() {
return playQueueAdapter;
}
//endregion
/*//////////////////////////////////////////////////////////////////////////
// SurfaceHolderCallback helpers
//////////////////////////////////////////////////////////////////////////*/
//region SurfaceHolderCallback helpers
private void setupVideoSurface() {
// make sure there is nothing left over from previous calls
cleanupVideoSurface();
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { // >=API23
surfaceHolderCallback = new SurfaceHolderCallback(context, simpleExoPlayer);
binding.surfaceView.getHolder().addCallback(surfaceHolderCallback);
final Surface surface = binding.surfaceView.getHolder().getSurface();
// initially set the surface manually otherwise
// onRenderedFirstFrame() will not be called
simpleExoPlayer.setVideoSurface(surface);
} else {
simpleExoPlayer.setVideoSurfaceView(binding.surfaceView);
}
}
private void cleanupVideoSurface() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { // >=API23
if (surfaceHolderCallback != null) {
if (binding != null) {
binding.surfaceView.getHolder().removeCallback(surfaceHolderCallback);
}
surfaceHolderCallback.release();
surfaceHolderCallback = null;
}
}
}
//endregion SurfaceHolderCallback helpers
}

View File

@@ -32,7 +32,8 @@ public class LoadController implements LoadControl {
final DefaultLoadControl.Builder builder = new DefaultLoadControl.Builder();
builder.setBufferDurationsMs(minimumPlaybackBufferMs, optimalPlaybackBufferMs,
initialPlaybackBufferMs, initialPlaybackBufferMs);
initialPlaybackBufferMs,
DefaultLoadControl.DEFAULT_BUFFER_FOR_PLAYBACK_AFTER_REBUFFER_MS);
internalLoadControl = builder.build();
}

View File

@@ -8,6 +8,7 @@ import android.os.IBinder;
import android.util.Log;
import androidx.annotation.Nullable;
import androidx.core.content.ContextCompat;
import com.google.android.exoplayer2.ExoPlaybackException;
import com.google.android.exoplayer2.PlaybackParameters;
@@ -22,18 +23,27 @@ import org.schabi.newpipe.player.event.PlayerServiceExtendedEventListener;
import org.schabi.newpipe.player.playqueue.PlayQueue;
public final class PlayerHolder {
private PlayerHolder() {
}
private static final boolean DEBUG = MainActivity.DEBUG;
private static final String TAG = "PlayerHolder";
private static PlayerHolder instance;
public static synchronized PlayerHolder getInstance() {
if (PlayerHolder.instance == null) {
PlayerHolder.instance = new PlayerHolder();
}
return PlayerHolder.instance;
}
private static PlayerServiceExtendedEventListener listener;
private final boolean DEBUG = MainActivity.DEBUG;
private final String TAG = PlayerHolder.class.getSimpleName();
private static ServiceConnection serviceConnection;
public static boolean bound;
private static MainPlayer playerService;
private static Player player;
private PlayerServiceExtendedEventListener listener;
private final PlayerServiceConnection serviceConnection = new PlayerServiceConnection();
public boolean bound;
private MainPlayer playerService;
private Player player;
/**
* Returns the current {@link MainPlayer.PlayerType} of the {@link MainPlayer} service,
@@ -42,26 +52,31 @@ public final class PlayerHolder {
* @return Current PlayerType
*/
@Nullable
public static MainPlayer.PlayerType getType() {
public MainPlayer.PlayerType getType() {
if (player == null) {
return null;
}
return player.getPlayerType();
}
public static boolean isPlaying() {
public boolean isPlaying() {
if (player == null) {
return false;
}
return player.isPlaying();
}
public static boolean isPlayerOpen() {
public boolean isPlayerOpen() {
return player != null;
}
public static void setListener(final PlayerServiceExtendedEventListener newListener) {
public void setListener(@Nullable final PlayerServiceExtendedEventListener newListener) {
listener = newListener;
if (listener == null) {
return;
}
// Force reload data from service
if (player != null) {
listener.onServiceConnected(player, playerService, false);
@@ -69,14 +84,15 @@ public final class PlayerHolder {
}
}
public static void removeListener() {
listener = null;
// helper to handle context in common place as using the same
// context to bind/unbind a service is crucial
private Context getCommonContext() {
return App.getApp();
}
public static void startService(final Context context,
final boolean playAfterConnect,
final PlayerServiceExtendedEventListener newListener) {
public void startService(final boolean playAfterConnect,
final PlayerServiceExtendedEventListener newListener) {
final Context context = getCommonContext();
setListener(newListener);
if (bound) {
return;
@@ -85,58 +101,65 @@ public final class PlayerHolder {
// and NullPointerExceptions inside the service because the service will be
// bound twice. Prevent it with unbinding first
unbind(context);
context.startService(new Intent(context, MainPlayer.class));
serviceConnection = getServiceConnection(context, playAfterConnect);
ContextCompat.startForegroundService(context, new Intent(context, MainPlayer.class));
serviceConnection.doPlayAfterConnect(playAfterConnect);
bind(context);
}
public static void stopService(final Context context) {
public void stopService() {
final Context context = getCommonContext();
unbind(context);
context.stopService(new Intent(context, MainPlayer.class));
}
private static ServiceConnection getServiceConnection(final Context context,
final boolean playAfterConnect) {
return new ServiceConnection() {
@Override
public void onServiceDisconnected(final ComponentName compName) {
if (DEBUG) {
Log.d(TAG, "Player service is disconnected");
}
class PlayerServiceConnection implements ServiceConnection {
unbind(context);
private boolean playAfterConnect = false;
public void doPlayAfterConnect(final boolean playAfterConnection) {
this.playAfterConnect = playAfterConnection;
}
@Override
public void onServiceDisconnected(final ComponentName compName) {
if (DEBUG) {
Log.d(TAG, "Player service is disconnected");
}
@Override
public void onServiceConnected(final ComponentName compName, final IBinder service) {
if (DEBUG) {
Log.d(TAG, "Player service is connected");
}
final MainPlayer.LocalBinder localBinder = (MainPlayer.LocalBinder) service;
final Context context = getCommonContext();
unbind(context);
}
playerService = localBinder.getService();
player = localBinder.getPlayer();
if (listener != null) {
listener.onServiceConnected(player, playerService, playAfterConnect);
}
startPlayerListener();
@Override
public void onServiceConnected(final ComponentName compName, final IBinder service) {
if (DEBUG) {
Log.d(TAG, "Player service is connected");
}
};
}
final MainPlayer.LocalBinder localBinder = (MainPlayer.LocalBinder) service;
private static void bind(final Context context) {
playerService = localBinder.getService();
player = localBinder.getPlayer();
if (listener != null) {
listener.onServiceConnected(player, playerService, playAfterConnect);
}
startPlayerListener();
}
};
private void bind(final Context context) {
if (DEBUG) {
Log.d(TAG, "bind() called");
}
final Intent serviceIntent = new Intent(context, MainPlayer.class);
bound = context.bindService(serviceIntent, serviceConnection, Context.BIND_AUTO_CREATE);
bound = context.bindService(serviceIntent, serviceConnection,
Context.BIND_AUTO_CREATE);
if (!bound) {
context.unbindService(serviceConnection);
}
}
private static void unbind(final Context context) {
private void unbind(final Context context) {
if (DEBUG) {
Log.d(TAG, "unbind() called");
}
@@ -153,21 +176,19 @@ public final class PlayerHolder {
}
}
private static void startPlayerListener() {
private void startPlayerListener() {
if (player != null) {
player.setFragmentListener(INNER_LISTENER);
player.setFragmentListener(internalListener);
}
}
private static void stopPlayerListener() {
private void stopPlayerListener() {
if (player != null) {
player.removeFragmentListener(INNER_LISTENER);
player.removeFragmentListener(internalListener);
}
}
private static final PlayerServiceEventListener INNER_LISTENER =
private final PlayerServiceEventListener internalListener =
new PlayerServiceEventListener() {
@Override
public void onFullscreenStateChanged(final boolean fullscreen) {
@@ -242,7 +263,7 @@ public final class PlayerHolder {
if (listener != null) {
listener.onServiceStopped();
}
unbind(App.getApp());
unbind(getCommonContext());
}
};
}

View File

@@ -0,0 +1,62 @@
package org.schabi.newpipe.player.playback;
import android.content.Context;
import android.view.SurfaceHolder;
import com.google.android.exoplayer2.SimpleExoPlayer;
import com.google.android.exoplayer2.video.DummySurface;
/**
* Prevent error message: 'Unrecoverable player error occurred'
* In case of rotation some users see this kind of an error which is preventable
* having a Callback that handles the lifecycle of the surface.
* <p>
* How?: In case we are no longer able to write to the surface eg. through rotation/putting in
* background we set set a DummySurface. Although it it works on API >= 23 only.
* Result: we get a little video interruption (audio is still fine) but we won't get the
* 'Unrecoverable player error occurred' error message.
* <p>
* This implementation is based on:
* 'ExoPlayer stuck in buffering after re-adding the surface view a few time #2703'
* <p>
* -> exoplayer fix suggestion link
* https://github.com/google/ExoPlayer/issues/2703#issuecomment-300599981
*/
public final class SurfaceHolderCallback implements SurfaceHolder.Callback {
private final Context context;
private final SimpleExoPlayer player;
private DummySurface dummySurface;
public SurfaceHolderCallback(final Context context, final SimpleExoPlayer player) {
this.context = context;
this.player = player;
}
@Override
public void surfaceCreated(final SurfaceHolder holder) {
player.setVideoSurface(holder.getSurface());
}
@Override
public void surfaceChanged(final SurfaceHolder holder,
final int format,
final int width,
final int height) {
}
@Override
public void surfaceDestroyed(final SurfaceHolder holder) {
if (dummySurface == null) {
dummySurface = DummySurface.newInstanceV17(context, false);
}
player.setVideoSurface(dummySurface);
}
public void release() {
if (dummySurface != null) {
dummySurface.release();
dummySurface = null;
}
}
}

View File

@@ -182,8 +182,10 @@ public class PlayQueueAdapter extends RecyclerView.Adapter<RecyclerView.ViewHold
return ITEM_VIEW_TYPE_ID;
}
@NonNull
@Override
public RecyclerView.ViewHolder onCreateViewHolder(final ViewGroup parent, final int type) {
public RecyclerView.ViewHolder onCreateViewHolder(@NonNull final ViewGroup parent,
final int type) {
switch (type) {
case FOOTER_VIEW_TYPE_ID:
return new HFHolder(footer);
@@ -197,7 +199,8 @@ public class PlayQueueAdapter extends RecyclerView.Adapter<RecyclerView.ViewHold
}
@Override
public void onBindViewHolder(final RecyclerView.ViewHolder holder, final int position) {
public void onBindViewHolder(@NonNull final RecyclerView.ViewHolder holder,
final int position) {
if (holder instanceof PlayQueueItemHolder) {
final PlayQueueItemHolder itemHolder = (PlayQueueItemHolder) holder;
@@ -207,7 +210,6 @@ public class PlayQueueAdapter extends RecyclerView.Adapter<RecyclerView.ViewHold
// Check if the current item should be selected/highlighted
final boolean isSelected = playQueue.getIndex() == position;
itemHolder.itemSelected.setVisibility(isSelected ? View.VISIBLE : View.INVISIBLE);
itemHolder.itemView.setSelected(isSelected);
} else if (holder instanceof HFHolder && position == playQueue.getStreams().size()
&& footer != null && showFooter) {

View File

@@ -64,20 +64,6 @@ public class PlayQueueItem implements Serializable {
this.recoveryPosition = RECOVERY_UNSET;
}
@Override
public boolean equals(final Object o) {
if (o instanceof PlayQueueItem) {
return url.equals(((PlayQueueItem) o).url);
} else {
return false;
}
}
@Override
public int hashCode() {
return url.hashCode();
}
@NonNull
public String getTitle() {
return title;

View File

@@ -37,7 +37,6 @@ public class PlayQueueItemHolder extends RecyclerView.ViewHolder {
public final TextView itemDurationView;
final TextView itemAdditionalDetailsView;
final ImageView itemSelected;
public final ImageView itemThumbnailView;
final ImageView itemHandle;
@@ -49,7 +48,6 @@ public class PlayQueueItemHolder extends RecyclerView.ViewHolder {
itemVideoTitleView = v.findViewById(R.id.itemVideoTitleView);
itemDurationView = v.findViewById(R.id.itemDurationView);
itemAdditionalDetailsView = v.findViewById(R.id.itemAdditionalDetails);
itemSelected = v.findViewById(R.id.itemSelected);
itemThumbnailView = v.findViewById(R.id.itemThumbnailView);
itemHandle = v.findViewById(R.id.itemHandle);
}

View File

@@ -0,0 +1,108 @@
package org.schabi.newpipe.player.seekbarpreview;
import android.content.Context;
import android.graphics.Bitmap;
import android.util.Log;
import android.view.View;
import android.widget.ImageView;
import androidx.annotation.IntDef;
import androidx.annotation.NonNull;
import androidx.preference.PreferenceManager;
import org.schabi.newpipe.R;
import org.schabi.newpipe.util.DeviceUtils;
import java.lang.annotation.Retention;
import java.util.Objects;
import java.util.Optional;
import java.util.function.IntSupplier;
import static java.lang.annotation.RetentionPolicy.SOURCE;
import static org.schabi.newpipe.player.seekbarpreview.SeekbarPreviewThumbnailHelper.SeekbarPreviewThumbnailType.HIGH_QUALITY;
import static org.schabi.newpipe.player.seekbarpreview.SeekbarPreviewThumbnailHelper.SeekbarPreviewThumbnailType.LOW_QUALITY;
import static org.schabi.newpipe.player.seekbarpreview.SeekbarPreviewThumbnailHelper.SeekbarPreviewThumbnailType.NONE;
/**
* Helper for the seekbar preview.
*/
public final class SeekbarPreviewThumbnailHelper {
// This has to be <= 23 chars on devices running Android 7 or lower (API <= 25)
// or it fails with an IllegalArgumentException
// https://stackoverflow.com/a/54744028
public static final String TAG = "SeekbarPrevThumbHelper";
private SeekbarPreviewThumbnailHelper() {
// No impl pls
}
@Retention(SOURCE)
@IntDef({HIGH_QUALITY, LOW_QUALITY,
NONE})
public @interface SeekbarPreviewThumbnailType {
int HIGH_QUALITY = 0;
int LOW_QUALITY = 1;
int NONE = 2;
}
////////////////////////////////////////////////////////////////////////////
// Settings Resolution
///////////////////////////////////////////////////////////////////////////
@SeekbarPreviewThumbnailType
public static int getSeekbarPreviewThumbnailType(@NonNull final Context context) {
final String type = PreferenceManager.getDefaultSharedPreferences(context).getString(
context.getString(R.string.seekbar_preview_thumbnail_key), "");
if (type.equals(context.getString(R.string.seekbar_preview_thumbnail_none))) {
return NONE;
} else if (type.equals(context.getString(R.string.seekbar_preview_thumbnail_low_quality))) {
return LOW_QUALITY;
} else {
return HIGH_QUALITY; // default
}
}
public static void tryResizeAndSetSeekbarPreviewThumbnail(
@NonNull final Context context,
@NonNull final Optional<Bitmap> optPreviewThumbnail,
@NonNull final ImageView currentSeekbarPreviewThumbnail,
@NonNull final IntSupplier baseViewWidthSupplier) {
if (!optPreviewThumbnail.isPresent()) {
currentSeekbarPreviewThumbnail.setVisibility(View.GONE);
return;
}
currentSeekbarPreviewThumbnail.setVisibility(View.VISIBLE);
final Bitmap srcBitmap = optPreviewThumbnail.get();
// Resize original bitmap
try {
Objects.requireNonNull(srcBitmap);
final int srcWidth = srcBitmap.getWidth() > 0 ? srcBitmap.getWidth() : 1;
final int newWidth = Math.max(
Math.min(
// Use 1/4 of the width for the preview
Math.round(baseViewWidthSupplier.getAsInt() / 4f),
// Scaling more than that factor looks really pixelated -> max
Math.round(srcWidth * 2.5f)
),
// Min width = 10dp
DeviceUtils.dpToPx(10, context)
);
final float scaleFactor = (float) newWidth / srcWidth;
final int newHeight = (int) (srcBitmap.getHeight() * scaleFactor);
currentSeekbarPreviewThumbnail.setImageBitmap(
Bitmap.createScaledBitmap(srcBitmap, newWidth, newHeight, true));
} catch (final Exception ex) {
Log.e(TAG, "Failed to resize and set seekbar preview thumbnail", ex);
currentSeekbarPreviewThumbnail.setVisibility(View.GONE);
} finally {
srcBitmap.recycle();
}
}
}

View File

@@ -0,0 +1,252 @@
package org.schabi.newpipe.player.seekbarpreview;
import android.content.Context;
import android.graphics.Bitmap;
import android.util.Log;
import androidx.annotation.NonNull;
import com.google.common.base.Stopwatch;
import com.nostra13.universalimageloader.core.ImageLoader;
import org.schabi.newpipe.extractor.stream.Frameset;
import org.schabi.newpipe.util.ImageDisplayConstants;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.function.Supplier;
import static org.schabi.newpipe.player.seekbarpreview.SeekbarPreviewThumbnailHelper.SeekbarPreviewThumbnailType;
public class SeekbarPreviewThumbnailHolder {
// This has to be <= 23 chars on devices running Android 7 or lower (API <= 25)
// or it fails with an IllegalArgumentException
// https://stackoverflow.com/a/54744028
public static final String TAG = "SeekbarPrevThumbHolder";
// Key = Position of the picture in milliseconds
// Supplier = Supplies the bitmap for that position
private final Map<Integer, Supplier<Bitmap>> seekbarPreviewData = new ConcurrentHashMap<>();
// This ensures that if the reset is still undergoing
// and another reset starts, only the last reset is processed
private UUID currentUpdateRequestIdentifier = UUID.randomUUID();
public synchronized void resetFrom(
@NonNull final Context context,
final List<Frameset> framesets) {
final int seekbarPreviewType =
SeekbarPreviewThumbnailHelper.getSeekbarPreviewThumbnailType(context);
final UUID updateRequestIdentifier = UUID.randomUUID();
this.currentUpdateRequestIdentifier = updateRequestIdentifier;
final ExecutorService executorService = Executors.newSingleThreadExecutor();
executorService.submit(() -> {
try {
resetFromAsync(seekbarPreviewType, framesets, updateRequestIdentifier);
} catch (final Exception ex) {
Log.e(TAG, "Failed to execute async", ex);
}
});
// ensure that the executorService stops/destroys it's threads
// after the task is finished
executorService.shutdown();
}
private void resetFromAsync(
final int seekbarPreviewType,
final List<Frameset> framesets,
final UUID updateRequestIdentifier) {
Log.d(TAG, "Clearing seekbarPreviewData");
seekbarPreviewData.clear();
if (seekbarPreviewType == SeekbarPreviewThumbnailType.NONE) {
Log.d(TAG, "Not processing seekbarPreviewData due to settings");
return;
}
final Frameset frameset = getFrameSetForType(framesets, seekbarPreviewType);
if (frameset == null) {
Log.d(TAG, "No frameset was found to fill seekbarPreviewData");
return;
}
Log.d(TAG, "Frameset quality info: "
+ "[width=" + frameset.getFrameWidth()
+ ", heigh=" + frameset.getFrameHeight() + "]");
// Abort method execution if we are not the latest request
if (!isRequestIdentifierCurrent(updateRequestIdentifier)) {
return;
}
generateDataFrom(frameset, updateRequestIdentifier);
}
private Frameset getFrameSetForType(
final List<Frameset> framesets,
final int seekbarPreviewType) {
if (seekbarPreviewType == SeekbarPreviewThumbnailType.HIGH_QUALITY) {
Log.d(TAG, "Strategy for seekbarPreviewData: high quality");
return framesets.stream()
.max(Comparator.comparingInt(fs -> fs.getFrameHeight() * fs.getFrameWidth()))
.orElse(null);
} else {
Log.d(TAG, "Strategy for seekbarPreviewData: low quality");
return framesets.stream()
.min(Comparator.comparingInt(fs -> fs.getFrameHeight() * fs.getFrameWidth()))
.orElse(null);
}
}
private void generateDataFrom(
final Frameset frameset,
final UUID updateRequestIdentifier) {
Log.d(TAG, "Starting generation of seekbarPreviewData");
final Stopwatch sw = Log.isLoggable(TAG, Log.DEBUG) ? Stopwatch.createStarted() : null;
int currentPosMs = 0;
int pos = 1;
final int frameCountPerUrl = frameset.getFramesPerPageX() * frameset.getFramesPerPageY();
// Process each url in the frameset
for (final String url : frameset.getUrls()) {
// get the bitmap
final Bitmap srcBitMap = getBitMapFrom(url);
// The data is not added directly to "seekbarPreviewData" due to
// concurrency and checks for "updateRequestIdentifier"
final Map<Integer, Supplier<Bitmap>> generatedDataForUrl = new HashMap<>();
// The bitmap consists of several images, which we process here
// foreach frame in the returned bitmap
for (int i = 0; i < frameCountPerUrl; i++) {
// Frames outside the video length are skipped
if (pos > frameset.getTotalCount()) {
break;
}
// Get the bounds where the frame is found
final int[] bounds = frameset.getFrameBoundsAt(currentPosMs);
generatedDataForUrl.put(currentPosMs, () -> {
// It can happen, that the original bitmap could not be downloaded
// In such a case - we don't want a NullPointer - simply return null
if (srcBitMap == null) {
return null;
}
// Cut out the corresponding bitmap form the "srcBitMap"
return Bitmap.createBitmap(srcBitMap, bounds[1], bounds[2],
frameset.getFrameWidth(), frameset.getFrameHeight());
});
currentPosMs += frameset.getDurationPerFrame();
pos++;
}
// Check if we are still the latest request
// If not abort method execution
if (isRequestIdentifierCurrent(updateRequestIdentifier)) {
seekbarPreviewData.putAll(generatedDataForUrl);
} else {
Log.d(TAG, "Aborted of generation of seekbarPreviewData");
break;
}
}
if (sw != null) {
Log.d(TAG, "Generation of seekbarPreviewData took " + sw.stop().toString());
}
}
private Bitmap getBitMapFrom(final String url) {
if (url == null) {
Log.w(TAG, "url is null; This should never happen");
return null;
}
final Stopwatch sw = Log.isLoggable(TAG, Log.DEBUG) ? Stopwatch.createStarted() : null;
try {
final SyncImageLoadingListener syncImageLoadingListener =
new SyncImageLoadingListener();
Log.d(TAG, "Downloading bitmap for seekbarPreview from '" + url + "'");
// Ensure that everything is running
ImageLoader.getInstance().resume();
// Load the image
// Impl-Note:
// Ensure that your are not running on the main-Thread this will otherwise hang
ImageLoader.getInstance().loadImage(
url,
ImageDisplayConstants.DISPLAY_SEEKBAR_PREVIEW_OPTIONS,
syncImageLoadingListener);
// Get the bitmap within the timeout
final Bitmap bitmap =
syncImageLoadingListener.waitForBitmapOrThrow(30, TimeUnit.SECONDS);
if (sw != null) {
Log.d(TAG,
"Download of bitmap for seekbarPreview from '" + url
+ "' took " + sw.stop().toString());
}
return bitmap;
} catch (final Exception ex) {
Log.w(TAG,
"Failed to get bitmap for seekbarPreview from url='" + url
+ "' in time",
ex);
return null;
}
}
private boolean isRequestIdentifierCurrent(final UUID requestIdentifier) {
return this.currentUpdateRequestIdentifier.equals(requestIdentifier);
}
public Optional<Bitmap> getBitmapAt(final int positionInMs) {
// Check if the BitmapData is empty
if (seekbarPreviewData.isEmpty()) {
return Optional.empty();
}
// Get the closest frame to the requested position
final int closestIndexPosition =
seekbarPreviewData.keySet().stream()
.min(Comparator.comparingInt(i -> Math.abs(i - positionInMs)))
.orElse(-1);
// this should never happen, because
// it indicates that "seekbarPreviewData" is empty which was already checked
if (closestIndexPosition == -1) {
return Optional.empty();
}
try {
// Get the bitmap for the position (executes the supplier)
return Optional.ofNullable(seekbarPreviewData.get(closestIndexPosition).get());
} catch (final Exception ex) {
// If there is an error, log it and return Optional.empty
Log.w(TAG, "Unable to get seekbar preview", ex);
return Optional.empty();
}
}
}

View File

@@ -0,0 +1,87 @@
package org.schabi.newpipe.player.seekbarpreview;
import android.graphics.Bitmap;
import android.view.View;
import com.nostra13.universalimageloader.core.assist.FailReason;
import com.nostra13.universalimageloader.core.listener.SimpleImageLoadingListener;
import java.util.concurrent.CancellationException;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
/**
* Listener for synchronously downloading of an image/bitmap.
*/
public class SyncImageLoadingListener extends SimpleImageLoadingListener {
private final CountDownLatch countDownLatch = new CountDownLatch(1);
private Bitmap bitmap;
private boolean cancelled = false;
private FailReason failReason = null;
@SuppressWarnings("checkstyle:HiddenField")
@Override
public void onLoadingFailed(
final String imageUri,
final View view,
final FailReason failReason) {
this.failReason = failReason;
countDownLatch.countDown();
}
@Override
public void onLoadingComplete(
final String imageUri,
final View view,
final Bitmap loadedImage) {
bitmap = loadedImage;
countDownLatch.countDown();
}
@Override
public void onLoadingCancelled(final String imageUri, final View view) {
cancelled = true;
countDownLatch.countDown();
}
public Bitmap waitForBitmapOrThrow(final long timeout, final TimeUnit timeUnit)
throws InterruptedException, TimeoutException {
// Wait for the download to finish
if (!countDownLatch.await(timeout, timeUnit)) {
throw new TimeoutException("Couldn't get the image in time");
}
if (isCancelled()) {
throw new CancellationException("Download of image was cancelled");
}
if (getFailReason() != null) {
throw new RuntimeException("Failed to download image" + getFailReason().getType(),
getFailReason().getCause());
}
if (getBitmap() == null) {
throw new NullPointerException("Bitmap is null");
}
return getBitmap();
}
public Bitmap getBitmap() {
return bitmap;
}
public boolean isCancelled() {
return cancelled;
}
public FailReason getFailReason() {
return failReason;
}
}

View File

@@ -6,12 +6,16 @@ import android.view.View;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.StringRes;
import androidx.preference.Preference;
import androidx.preference.PreferenceFragmentCompat;
import androidx.preference.PreferenceManager;
import org.schabi.newpipe.MainActivity;
import org.schabi.newpipe.util.ThemeHelper;
import java.util.Objects;
public abstract class BasePreferenceFragment extends PreferenceFragmentCompat {
protected final String TAG = getClass().getSimpleName() + "@" + Integer.toHexString(hashCode());
protected final boolean DEBUG = MainActivity.DEBUG;
@@ -37,4 +41,11 @@ public abstract class BasePreferenceFragment extends PreferenceFragmentCompat {
super.onResume();
ThemeHelper.setTitleToAppCompatActivity(getActivity(), getPreferenceScreen().getTitle());
}
@NonNull
public final Preference requirePreference(@StringRes final int resId) {
final Preference preference = findPreference(getString(resId));
Objects.requireNonNull(preference);
return preference;
}
}

View File

@@ -1,21 +1,22 @@
package org.schabi.newpipe.settings;
import android.annotation.SuppressLint;
import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.net.Uri;
import android.os.Bundle;
import android.util.Log;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.activity.result.ActivityResult;
import androidx.activity.result.ActivityResultLauncher;
import androidx.activity.result.contract.ActivityResultContracts.StartActivityForResult;
import androidx.appcompat.app.AlertDialog;
import androidx.core.content.ContextCompat;
import androidx.preference.Preference;
import androidx.preference.PreferenceManager;
import com.nononsenseapps.filepicker.Utils;
import com.nostra13.universalimageloader.core.ImageLoader;
import org.schabi.newpipe.DownloaderImpl;
@@ -26,74 +27,70 @@ import org.schabi.newpipe.error.ReCaptchaActivity;
import org.schabi.newpipe.extractor.NewPipe;
import org.schabi.newpipe.extractor.localization.ContentCountry;
import org.schabi.newpipe.extractor.localization.Localization;
import org.schabi.newpipe.util.FilePathUtils;
import org.schabi.newpipe.util.FilePickerActivityHelper;
import org.schabi.newpipe.streams.io.StoredFileHelper;
import org.schabi.newpipe.util.NavigationHelper;
import org.schabi.newpipe.util.ZipHelper;
import java.io.File;
import java.io.IOException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Locale;
import java.util.Objects;
import static org.schabi.newpipe.extractor.utils.Utils.isBlank;
import static org.schabi.newpipe.util.Localization.assureCorrectAppLanguage;
public class ContentSettingsFragment extends BasePreferenceFragment {
private static final int REQUEST_IMPORT_PATH = 8945;
private static final int REQUEST_EXPORT_PATH = 30945;
private static final String ZIP_MIME_TYPE = "application/zip";
private final SimpleDateFormat exportDateFormat
= new SimpleDateFormat("yyyyMMdd_HHmmss", Locale.US);
private ContentSettingsManager manager;
private String importExportDataPathKey;
private String thumbnailLoadToggleKey;
private String youtubeRestrictedModeEnabledKey;
private Localization initialSelectedLocalization;
private ContentCountry initialSelectedContentCountry;
private String initialLanguage;
private final ActivityResultLauncher<Intent> requestImportPathLauncher =
registerForActivityResult(new StartActivityForResult(), this::requestImportPathResult);
private final ActivityResultLauncher<Intent> requestExportPathLauncher =
registerForActivityResult(new StartActivityForResult(), this::requestExportPathResult);
@Override
public void onCreatePreferences(final Bundle savedInstanceState, final String rootKey) {
final File homeDir = ContextCompat.getDataDir(requireContext());
Objects.requireNonNull(homeDir);
manager = new ContentSettingsManager(new NewPipeFileLocator(homeDir));
manager.deleteSettingsFile();
addPreferencesFromResource(R.xml.content_settings);
importExportDataPathKey = getString(R.string.import_export_data_path);
final Preference importDataPreference = findPreference(getString(R.string.import_data));
importDataPreference.setOnPreferenceClickListener(p -> {
final Intent i = new Intent(getActivity(), FilePickerActivityHelper.class)
.putExtra(FilePickerActivityHelper.EXTRA_ALLOW_MULTIPLE, false)
.putExtra(FilePickerActivityHelper.EXTRA_ALLOW_CREATE_DIR, false)
.putExtra(FilePickerActivityHelper.EXTRA_MODE,
FilePickerActivityHelper.MODE_FILE);
final String path = defaultPreferences.getString(importExportDataPathKey, "");
if (FilePathUtils.isValidDirectoryPath(path)) {
i.putExtra(FilePickerActivityHelper.EXTRA_START_PATH, path);
}
startActivityForResult(i, REQUEST_IMPORT_PATH);
return true;
});
final Preference exportDataPreference = findPreference(getString(R.string.export_data));
exportDataPreference.setOnPreferenceClickListener(p -> {
final Intent i = new Intent(getActivity(), FilePickerActivityHelper.class)
.putExtra(FilePickerActivityHelper.EXTRA_ALLOW_MULTIPLE, false)
.putExtra(FilePickerActivityHelper.EXTRA_ALLOW_CREATE_DIR, true)
.putExtra(FilePickerActivityHelper.EXTRA_MODE,
FilePickerActivityHelper.MODE_DIR);
final String path = defaultPreferences.getString(importExportDataPathKey, "");
if (FilePathUtils.isValidDirectoryPath(path)) {
i.putExtra(FilePickerActivityHelper.EXTRA_START_PATH, path);
}
startActivityForResult(i, REQUEST_EXPORT_PATH);
return true;
});
thumbnailLoadToggleKey = getString(R.string.download_thumbnail_key);
youtubeRestrictedModeEnabledKey = getString(R.string.youtube_restricted_mode_enabled);
addPreferencesFromResource(R.xml.content_settings);
final Preference importDataPreference = requirePreference(R.string.import_data);
importDataPreference.setOnPreferenceClickListener((Preference p) -> {
requestImportPathLauncher.launch(
StoredFileHelper.getPicker(requireContext(), getImportExportDataUri()));
return true;
});
final Preference exportDataPreference = requirePreference(R.string.export_data);
exportDataPreference.setOnPreferenceClickListener((final Preference p) -> {
requestExportPathLauncher.launch(
StoredFileHelper.getNewPicker(requireContext(),
"NewPipeData-" + exportDateFormat.format(new Date()) + ".zip",
ZIP_MIME_TYPE, getImportExportDataUri()));
return true;
});
initialSelectedLocalization = org.schabi.newpipe.util.Localization
.getPreferredLocalization(requireContext());
initialSelectedContentCountry = org.schabi.newpipe.util.Localization
@@ -101,8 +98,7 @@ public class ContentSettingsFragment extends BasePreferenceFragment {
initialLanguage = PreferenceManager
.getDefaultSharedPreferences(requireContext()).getString("app_language_key", "en");
final Preference clearCookiePref = findPreference(getString(R.string.clear_cookie_key));
final Preference clearCookiePref = requirePreference(R.string.clear_cookie_key);
clearCookiePref.setOnPreferenceClickListener(preference -> {
defaultPreferences.edit()
.putString(getString(R.string.recaptcha_cookies_key), "").apply();
@@ -163,62 +159,58 @@ public class ContentSettingsFragment extends BasePreferenceFragment {
}
}
@Override
public void onActivityResult(final int requestCode, final int resultCode,
@NonNull final Intent data) {
private void requestExportPathResult(final ActivityResult result) {
assureCorrectAppLanguage(getContext());
super.onActivityResult(requestCode, resultCode, data);
if (DEBUG) {
Log.d(TAG, "onActivityResult() called with: "
+ "requestCode = [" + requestCode + "], "
+ "resultCode = [" + resultCode + "], "
+ "data = [" + data + "]");
}
if (result.getResultCode() == Activity.RESULT_OK && result.getData() != null) {
// will be saved only on success
final Uri lastExportDataUri = result.getData().getData();
if ((requestCode == REQUEST_IMPORT_PATH || requestCode == REQUEST_EXPORT_PATH)
&& resultCode == Activity.RESULT_OK && data.getData() != null) {
final File file = Utils.getFileForUri(data.getData());
final StoredFileHelper file
= new StoredFileHelper(getContext(), result.getData().getData(), ZIP_MIME_TYPE);
if (requestCode == REQUEST_EXPORT_PATH) {
exportDatabase(file);
} else {
final AlertDialog.Builder builder = new AlertDialog.Builder(requireActivity());
builder.setMessage(R.string.override_current_data)
.setPositiveButton(getString(R.string.finish),
(d, id) -> importDatabase(file))
.setNegativeButton(android.R.string.cancel,
(d, id) -> d.cancel());
builder.create().show();
}
exportDatabase(file, lastExportDataUri);
}
}
private void exportDatabase(@NonNull final File folder) {
try {
final SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMdd_HHmmss", Locale.US);
final String path = folder.getAbsolutePath() + "/NewPipeData-"
+ sdf.format(new Date()) + ".zip";
private void requestImportPathResult(final ActivityResult result) {
assureCorrectAppLanguage(getContext());
if (result.getResultCode() == Activity.RESULT_OK && result.getData() != null) {
// will be saved only on success
final Uri lastImportDataUri = result.getData().getData();
final StoredFileHelper file
= new StoredFileHelper(getContext(), result.getData().getData(), ZIP_MIME_TYPE);
new AlertDialog.Builder(requireActivity())
.setMessage(R.string.override_current_data)
.setPositiveButton(R.string.finish, (d, id) ->
importDatabase(file, lastImportDataUri))
.setNegativeButton(R.string.cancel, (d, id) ->
d.cancel())
.create()
.show();
}
}
private void exportDatabase(final StoredFileHelper file, final Uri exportDataUri) {
try {
//checkpoint before export
NewPipeDatabase.checkpoint();
final SharedPreferences preferences = PreferenceManager
.getDefaultSharedPreferences(requireContext());
manager.exportDatabase(preferences, path);
setImportExportDataPath(folder, false);
manager.exportDatabase(preferences, file);
saveLastImportExportDataUri(exportDataUri); // save export path only on success
Toast.makeText(getContext(), R.string.export_complete_toast, Toast.LENGTH_SHORT).show();
} catch (final Exception e) {
ErrorActivity.reportUiErrorInSnackbar(this, "Exporting database", e);
}
}
private void importDatabase(@NonNull final File file) {
final String filePath = file.getAbsolutePath();
private void importDatabase(final StoredFileHelper file, final Uri importDataUri) {
// check if file is supported
if (!ZipHelper.isValidZipFile(filePath)) {
if (!ZipHelper.isValidZipFile(file)) {
Toast.makeText(getContext(), R.string.no_valid_zip_file, Toast.LENGTH_SHORT)
.show();
return;
@@ -226,32 +218,32 @@ public class ContentSettingsFragment extends BasePreferenceFragment {
try {
if (!manager.ensureDbDirectoryExists()) {
throw new Exception("Could not create databases dir");
throw new IOException("Could not create databases dir");
}
if (!manager.extractDb(filePath)) {
if (!manager.extractDb(file)) {
Toast.makeText(getContext(), R.string.could_not_import_all_files, Toast.LENGTH_LONG)
.show();
.show();
}
//If settings file exist, ask if it should be imported.
if (manager.extractSettings(filePath)) {
// if settings file exist, ask if it should be imported.
if (manager.extractSettings(file)) {
final AlertDialog.Builder alert = new AlertDialog.Builder(requireContext());
alert.setTitle(R.string.import_settings);
alert.setNegativeButton(android.R.string.no, (dialog, which) -> {
dialog.dismiss();
finishImport(file);
finishImport(importDataUri);
});
alert.setPositiveButton(getString(R.string.finish), (dialog, which) -> {
dialog.dismiss();
manager.loadSharedPreferences(PreferenceManager
.getDefaultSharedPreferences(requireContext()));
finishImport(file);
finishImport(importDataUri);
});
alert.show();
} else {
finishImport(file);
finishImport(importDataUri);
}
} catch (final Exception e) {
ErrorActivity.reportUiErrorInSnackbar(this, "Importing database", e);
@@ -261,38 +253,23 @@ public class ContentSettingsFragment extends BasePreferenceFragment {
/**
* Save import path and restart system.
*
* @param file The file of the created backup
* @param importDataUri The import path to save
*/
private void finishImport(@NonNull final File file) {
if (file.getParentFile() != null) {
//immediately because app is about to exit
setImportExportDataPath(file.getParentFile(), true);
}
private void finishImport(final Uri importDataUri) {
// save import path only on success
saveLastImportExportDataUri(importDataUri);
// restart app to properly load db
System.exit(0);
NavigationHelper.restartApp(requireActivity());
}
@SuppressLint("ApplySharedPref")
private void setImportExportDataPath(@NonNull final File file, final boolean immediately) {
final String directoryPath;
if (file.isDirectory()) {
directoryPath = file.getAbsolutePath();
} else {
final File parentFile = file.getParentFile();
if (parentFile != null) {
directoryPath = parentFile.getAbsolutePath();
} else {
directoryPath = "";
}
}
final SharedPreferences.Editor editor = defaultPreferences
.edit()
.putString(importExportDataPathKey, directoryPath);
if (immediately) {
editor.commit();
} else {
editor.apply();
}
private Uri getImportExportDataUri() {
final String path = defaultPreferences.getString(importExportDataPathKey, null);
return isBlank(path) ? null : Uri.parse(path);
}
private void saveLastImportExportDataUri(final Uri importExportDataUri) {
final SharedPreferences.Editor editor = defaultPreferences.edit()
.putString(importExportDataPathKey, importExportDataUri.toString());
editor.apply();
}
}

View File

@@ -1,6 +1,8 @@
package org.schabi.newpipe.settings
import android.content.SharedPreferences
import org.schabi.newpipe.streams.io.SharpOutputStream
import org.schabi.newpipe.streams.io.StoredFileHelper
import org.schabi.newpipe.util.ZipHelper
import java.io.BufferedOutputStream
import java.io.FileInputStream
@@ -17,8 +19,9 @@ class ContentSettingsManager(private val fileLocator: NewPipeFileLocator) {
* It also creates the file.
*/
@Throws(Exception::class)
fun exportDatabase(preferences: SharedPreferences, outputPath: String) {
ZipOutputStream(BufferedOutputStream(FileOutputStream(outputPath)))
fun exportDatabase(preferences: SharedPreferences, file: StoredFileHelper) {
file.create()
ZipOutputStream(BufferedOutputStream(SharpOutputStream(file.stream)))
.use { outZip ->
ZipHelper.addFileToZip(outZip, fileLocator.db.path, "newpipe.db")
@@ -48,8 +51,8 @@ class ContentSettingsManager(private val fileLocator: NewPipeFileLocator) {
return fileLocator.dbDir.exists() || fileLocator.dbDir.mkdir()
}
fun extractDb(filePath: String): Boolean {
val success = ZipHelper.extractFileFromZip(filePath, fileLocator.db.path, "newpipe.db")
fun extractDb(file: StoredFileHelper): Boolean {
val success = ZipHelper.extractFileFromZip(file, fileLocator.db.path, "newpipe.db")
if (success) {
fileLocator.dbJournal.delete()
fileLocator.dbWal.delete()
@@ -59,9 +62,8 @@ class ContentSettingsManager(private val fileLocator: NewPipeFileLocator) {
return success
}
fun extractSettings(filePath: String): Boolean {
return ZipHelper
.extractFileFromZip(filePath, fileLocator.settings.path, "newpipe.settings")
fun extractSettings(file: StoredFileHelper): Boolean {
return ZipHelper.extractFileFromZip(file, fileLocator.settings.path, "newpipe.settings")
}
fun loadSharedPreferences(preferences: SharedPreferences) {

View File

@@ -8,15 +8,20 @@ import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.util.Log;
import android.widget.Toast;
import androidx.activity.result.ActivityResult;
import androidx.activity.result.ActivityResultLauncher;
import androidx.activity.result.contract.ActivityResultContracts.StartActivityForResult;
import androidx.annotation.NonNull;
import androidx.annotation.StringRes;
import androidx.appcompat.app.AlertDialog;
import androidx.preference.Preference;
import androidx.preference.SwitchPreferenceCompat;
import com.nononsenseapps.filepicker.Utils;
import org.schabi.newpipe.R;
import org.schabi.newpipe.streams.io.StoredDirectoryHelper;
import org.schabi.newpipe.util.FilePickerActivityHelper;
import java.io.File;
@@ -26,14 +31,10 @@ import java.net.URI;
import java.net.URLDecoder;
import java.nio.charset.StandardCharsets;
import us.shandian.giga.io.StoredDirectoryHelper;
import static org.schabi.newpipe.util.Localization.assureCorrectAppLanguage;
public class DownloadSettingsFragment extends BasePreferenceFragment {
public static final boolean IGNORE_RELEASE_ON_OLD_PATH = true;
private static final int REQUEST_DOWNLOAD_VIDEO_PATH = 0x1235;
private static final int REQUEST_DOWNLOAD_AUDIO_PATH = 0x1236;
private String downloadPathVideoPreference;
private String downloadPathAudioPreference;
private String storageUseSafPreference;
@@ -43,6 +44,12 @@ public class DownloadSettingsFragment extends BasePreferenceFragment {
private Preference prefStorageAsk;
private Context ctx;
private final ActivityResultLauncher<Intent> requestDownloadVideoPathLauncher =
registerForActivityResult(
new StartActivityForResult(), this::requestDownloadVideoPathResult);
private final ActivityResultLauncher<Intent> requestDownloadAudioPathLauncher =
registerForActivityResult(
new StartActivityForResult(), this::requestDownloadAudioPathResult);
@Override
public void onCreatePreferences(final Bundle savedInstanceState, final String rootKey) {
@@ -57,13 +64,23 @@ public class DownloadSettingsFragment extends BasePreferenceFragment {
prefPathAudio = findPreference(downloadPathAudioPreference);
prefStorageAsk = findPreference(downloadStorageAsk);
final SwitchPreferenceCompat prefUseSaf = findPreference(storageUseSafPreference);
prefUseSaf.setDefaultValue(Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP);
prefUseSaf.setChecked(NewPipeSettings.useStorageAccessFramework(ctx));
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q
|| Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
prefUseSaf.setEnabled(false);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
prefUseSaf.setSummary(R.string.downloads_storage_use_saf_summary_api_29);
} else {
prefUseSaf.setSummary(R.string.downloads_storage_use_saf_summary_api_19);
}
prefStorageAsk.setSummary(R.string.downloads_storage_ask_summary_no_saf_notice);
}
updatePreferencesSummary();
updatePathPickers(!defaultPreferences.getBoolean(downloadStorageAsk, false));
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
prefStorageAsk.setSummary(R.string.downloads_storage_ask_summary);
}
if (hasInvalidPath(downloadPathVideoPreference)
|| hasInvalidPath(downloadPathAudioPreference)) {
updatePreferencesSummary();
@@ -76,7 +93,7 @@ public class DownloadSettingsFragment extends BasePreferenceFragment {
}
@Override
public void onAttach(final Context context) {
public void onAttach(@NonNull final Context context) {
super.onAttach(context);
ctx = context;
}
@@ -174,65 +191,51 @@ public class DownloadSettingsFragment extends BasePreferenceFragment {
}
final String key = preference.getKey();
final int request;
if (key.equals(storageUseSafPreference)) {
Toast.makeText(getContext(), R.string.download_choose_new_path,
Toast.LENGTH_LONG).show();
if (!NewPipeSettings.useStorageAccessFramework(ctx)) {
NewPipeSettings.saveDefaultVideoDownloadDirectory(ctx);
NewPipeSettings.saveDefaultAudioDownloadDirectory(ctx);
} else {
defaultPreferences.edit().putString(downloadPathVideoPreference, null)
.putString(downloadPathAudioPreference, null).apply();
}
updatePreferencesSummary();
return true;
} else if (key.equals(downloadPathVideoPreference)) {
request = REQUEST_DOWNLOAD_VIDEO_PATH;
launchDirectoryPicker(requestDownloadVideoPathLauncher);
} else if (key.equals(downloadPathAudioPreference)) {
request = REQUEST_DOWNLOAD_AUDIO_PATH;
launchDirectoryPicker(requestDownloadAudioPathLauncher);
} else {
return super.onPreferenceTreeClick(preference);
}
final Intent i;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP
&& NewPipeSettings.useStorageAccessFramework(ctx)) {
i = new Intent(Intent.ACTION_OPEN_DOCUMENT_TREE)
.putExtra("android.content.extra.SHOW_ADVANCED", true)
.addFlags(Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION
| StoredDirectoryHelper.PERMISSION_FLAGS);
} else {
i = new Intent(getActivity(), FilePickerActivityHelper.class)
.putExtra(FilePickerActivityHelper.EXTRA_ALLOW_MULTIPLE, false)
.putExtra(FilePickerActivityHelper.EXTRA_ALLOW_CREATE_DIR, true)
.putExtra(FilePickerActivityHelper.EXTRA_MODE,
FilePickerActivityHelper.MODE_DIR);
}
startActivityForResult(i, request);
return true;
}
@Override
public void onActivityResult(final int requestCode, final int resultCode, final Intent data) {
private void launchDirectoryPicker(final ActivityResultLauncher<Intent> launcher) {
launcher.launch(StoredDirectoryHelper.getPicker(ctx));
}
private void requestDownloadVideoPathResult(final ActivityResult result) {
requestDownloadPathResult(result, downloadPathVideoPreference);
}
private void requestDownloadAudioPathResult(final ActivityResult result) {
requestDownloadPathResult(result, downloadPathAudioPreference);
}
private void requestDownloadPathResult(final ActivityResult result, final String key) {
assureCorrectAppLanguage(getContext());
super.onActivityResult(requestCode, resultCode, data);
if (DEBUG) {
Log.d(TAG, "onActivityResult() called with: "
+ "requestCode = [" + requestCode + "], "
+ "resultCode = [" + resultCode + "], data = [" + data + "]"
);
}
if (resultCode != Activity.RESULT_OK) {
if (result.getResultCode() != Activity.RESULT_OK) {
return;
}
final String key;
if (requestCode == REQUEST_DOWNLOAD_VIDEO_PATH) {
key = downloadPathVideoPreference;
} else if (requestCode == REQUEST_DOWNLOAD_AUDIO_PATH) {
key = downloadPathAudioPreference;
} else {
return;
Uri uri = null;
if (result.getData() != null) {
uri = result.getData().getData();
}
Uri uri = data.getData();
if (uri == null) {
showMessageDialog(R.string.general_error, R.string.invalid_directory);
return;

View File

@@ -2,16 +2,20 @@ package org.schabi.newpipe.settings;
import android.content.Context;
import android.content.SharedPreferences;
import android.os.Build;
import android.os.Environment;
import androidx.annotation.NonNull;
import androidx.preference.PreferenceManager;
import org.schabi.newpipe.R;
import org.schabi.newpipe.util.DeviceUtils;
import java.io.File;
import java.util.Set;
import static org.schabi.newpipe.extractor.utils.Utils.isNullOrEmpty;
/*
* Created by k3b on 07.01.2016.
*
@@ -65,32 +69,36 @@ public final class NewPipeSettings {
PreferenceManager.setDefaultValues(context, R.xml.update_settings, true);
PreferenceManager.setDefaultValues(context, R.xml.debug_settings, true);
getVideoDownloadFolder(context);
getAudioDownloadFolder(context);
saveDefaultVideoDownloadDirectory(context);
saveDefaultAudioDownloadDirectory(context);
SettingMigrations.initMigrations(context, isFirstRun);
}
private static void getVideoDownloadFolder(final Context context) {
getDir(context, R.string.download_path_video_key, Environment.DIRECTORY_MOVIES);
static void saveDefaultVideoDownloadDirectory(final Context context) {
saveDefaultDirectory(context, R.string.download_path_video_key,
Environment.DIRECTORY_MOVIES);
}
private static void getAudioDownloadFolder(final Context context) {
getDir(context, R.string.download_path_audio_key, Environment.DIRECTORY_MUSIC);
static void saveDefaultAudioDownloadDirectory(final Context context) {
saveDefaultDirectory(context, R.string.download_path_audio_key,
Environment.DIRECTORY_MUSIC);
}
private static void getDir(final Context context, final int keyID,
final String defaultDirectoryName) {
final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
final String key = context.getString(keyID);
final String downloadPath = prefs.getString(key, null);
if ((downloadPath != null) && (!downloadPath.isEmpty())) {
return;
private static void saveDefaultDirectory(final Context context, final int keyID,
final String defaultDirectoryName) {
if (!useStorageAccessFramework(context)) {
final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
final String key = context.getString(keyID);
final String downloadPath = prefs.getString(key, null);
if (!isNullOrEmpty(downloadPath)) {
return;
}
final SharedPreferences.Editor spEditor = prefs.edit();
spEditor.putString(key, getNewPipeChildFolderPathForDir(getDir(defaultDirectoryName)));
spEditor.apply();
}
final SharedPreferences.Editor spEditor = prefs.edit();
spEditor.putString(key, getNewPipeChildFolderPathForDir(getDir(defaultDirectoryName)));
spEditor.apply();
}
@NonNull
@@ -103,10 +111,17 @@ public final class NewPipeSettings {
}
public static boolean useStorageAccessFramework(final Context context) {
// There's a FireOS bug which prevents SAF open/close dialogs from being confirmed with a
// remote (see #6455).
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP || DeviceUtils.isFireTv()) {
return false;
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
return true;
}
final String key = context.getString(R.string.storage_use_saf);
final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
return prefs.getBoolean(key, false);
return prefs.getBoolean(key, true);
}
}

View File

@@ -12,7 +12,6 @@ import android.view.MenuItem;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import android.widget.EditText;
import android.widget.ImageView;
import android.widget.ProgressBar;
import android.widget.RadioButton;
@@ -35,6 +34,7 @@ import com.grack.nanojson.JsonStringWriter;
import com.grack.nanojson.JsonWriter;
import org.schabi.newpipe.R;
import org.schabi.newpipe.databinding.DialogEditTextBinding;
import org.schabi.newpipe.extractor.services.peertube.PeertubeInstance;
import org.schabi.newpipe.util.Constants;
import org.schabi.newpipe.util.PeertubeHelper;
@@ -139,7 +139,8 @@ public class PeertubeInstanceListFragment extends Fragment {
//////////////////////////////////////////////////////////////////////////*/
@Override
public void onCreateOptionsMenu(final Menu menu, final MenuInflater inflater) {
public void onCreateOptionsMenu(@NonNull final Menu menu,
@NonNull final MenuInflater inflater) {
super.onCreateOptionsMenu(menu, inflater);
final MenuItem restoreItem = menu
@@ -206,20 +207,22 @@ public class PeertubeInstanceListFragment extends Fragment {
}
private void showAddItemDialog(final Context c) {
final EditText urlET = new EditText(c);
urlET.setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_URI);
urlET.setHint(R.string.peertube_instance_add_help);
final AlertDialog dialog = new AlertDialog.Builder(c)
final DialogEditTextBinding dialogBinding
= DialogEditTextBinding.inflate(getLayoutInflater());
dialogBinding.dialogEditText.setInputType(
InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_URI);
dialogBinding.dialogEditText.setHint(R.string.peertube_instance_add_help);
new AlertDialog.Builder(c)
.setTitle(R.string.peertube_instance_add_title)
.setIcon(R.drawable.place_holder_peertube)
.setView(dialogBinding.getRoot())
.setNegativeButton(R.string.cancel, null)
.setPositiveButton(R.string.finish, (dialog1, which) -> {
final String url = urlET.getText().toString();
final String url = dialogBinding.dialogEditText.getText().toString();
addInstance(url);
})
.create();
dialog.setView(urlET, 50, 0, 50, 0);
dialog.show();
.show();
}
private void addInstance(final String url) {
@@ -279,7 +282,7 @@ public class PeertubeInstanceListFragment extends Fragment {
return new ItemTouchHelper.SimpleCallback(ItemTouchHelper.UP | ItemTouchHelper.DOWN,
ItemTouchHelper.START | ItemTouchHelper.END) {
@Override
public int interpolateOutOfBoundsScroll(final RecyclerView recyclerView,
public int interpolateOutOfBoundsScroll(@NonNull final RecyclerView recyclerView,
final int viewSize,
final int viewSizeOutOfBounds,
final int totalSize,
@@ -292,9 +295,9 @@ public class PeertubeInstanceListFragment extends Fragment {
}
@Override
public boolean onMove(final RecyclerView recyclerView,
final RecyclerView.ViewHolder source,
final RecyclerView.ViewHolder target) {
public boolean onMove(@NonNull final RecyclerView recyclerView,
@NonNull final RecyclerView.ViewHolder source,
@NonNull final RecyclerView.ViewHolder target) {
if (source.getItemViewType() != target.getItemViewType()
|| instanceListAdapter == null) {
return false;
@@ -317,7 +320,8 @@ public class PeertubeInstanceListFragment extends Fragment {
}
@Override
public void onSwiped(final RecyclerView.ViewHolder viewHolder, final int swipeDir) {
public void onSwiped(@NonNull final RecyclerView.ViewHolder viewHolder,
final int swipeDir) {
final int position = viewHolder.getAdapterPosition();
// do not allow swiping the selected instance
if (instanceList.get(position).getUrl().equals(selectedInstance.getUrl())) {

View File

@@ -8,6 +8,7 @@ import android.view.ViewGroup;
import android.widget.ImageView;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.content.res.AppCompatResources;
import androidx.fragment.app.DialogFragment;
@@ -92,7 +93,7 @@ public class SelectKioskFragment extends DialogFragment {
//////////////////////////////////////////////////////////////////////////*/
@Override
public void onCancel(final DialogInterface dialogInterface) {
public void onCancel(@NonNull final DialogInterface dialogInterface) {
super.onCancel(dialogInterface);
if (onCancelListener != null) {
onCancelListener.onCancel();
@@ -138,6 +139,7 @@ public class SelectKioskFragment extends DialogFragment {
return kioskList.size();
}
@NonNull
public SelectKioskItemHolder onCreateViewHolder(final ViewGroup parent, final int type) {
final View item = LayoutInflater.from(parent.getContext())
.inflate(R.layout.select_kiosk_item, parent, false);

View File

@@ -2,6 +2,7 @@ package org.schabi.newpipe.settings;
import android.content.Context;
import android.content.SharedPreferences;
import android.os.Build;
import android.util.Log;
import androidx.preference.PreferenceManager;
@@ -10,6 +11,7 @@ import org.schabi.newpipe.R;
import org.schabi.newpipe.error.ErrorActivity;
import org.schabi.newpipe.error.ErrorInfo;
import org.schabi.newpipe.error.UserAction;
import org.schabi.newpipe.util.DeviceUtils;
import static org.schabi.newpipe.MainActivity.DEBUG;
@@ -18,7 +20,7 @@ public final class SettingMigrations {
/**
* Version number for preferences. Must be incremented every time a migration is necessary.
*/
public static final int VERSION = 2;
public static final int VERSION = 3;
private static SharedPreferences sp;
public static final Migration MIGRATION_0_1 = new Migration(0, 1) {
@@ -54,6 +56,22 @@ public final class SettingMigrations {
}
};
public static final Migration MIGRATION_2_3 = new Migration(2, 3) {
@Override
protected void migrate(final Context context) {
// Storage Access Framework implementation was improved in #5415, allowing the modern
// and standard way to access folders and files to be used consistently everywhere.
// We reset the setting to its default value, i.e. "use SAF", since now there are no
// more issues with SAF and users should use that one instead of the old
// NoNonsenseFilePicker. SAF does not work on KitKat and below, though, so the setting
// is set to false in that case. Also, there's a bug on FireOS in which SAF open/close
// dialogs cannot be confirmed with a remote (see #6455).
sp.edit().putBoolean(context.getString(R.string.storage_use_saf),
Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP
&& !DeviceUtils.isFireTv()).apply();
}
};
/**
* List of all implemented migrations.
* <p>
@@ -62,7 +80,8 @@ public final class SettingMigrations {
*/
private static final Migration[] SETTING_MIGRATIONS = {
MIGRATION_0_1,
MIGRATION_1_2
MIGRATION_1_2,
MIGRATION_2_3
};

View File

@@ -1,6 +1,5 @@
package org.schabi.newpipe.settings;
import android.content.Context;
import android.os.Bundle;
import android.view.Menu;
import android.view.MenuItem;
@@ -41,11 +40,6 @@ import static org.schabi.newpipe.util.Localization.assureCorrectAppLanguage;
public class SettingsActivity extends AppCompatActivity
implements BasePreferenceFragment.OnPreferenceStartFragmentCallback {
public static void initSettings(final Context context) {
NewPipeSettings.initSettings(context);
}
@Override
protected void onCreate(final Bundle savedInstanceBundle) {
setTheme(ThemeHelper.getSettingsThemeStyle(this));

View File

@@ -106,7 +106,8 @@ public class ChooseTabsFragment extends Fragment {
//////////////////////////////////////////////////////////////////////////*/
@Override
public void onCreateOptionsMenu(final Menu menu, final MenuInflater inflater) {
public void onCreateOptionsMenu(@NonNull final Menu menu,
@NonNull final MenuInflater inflater) {
super.onCreateOptionsMenu(menu, inflater);
final MenuItem restoreItem = menu.add(Menu.NONE, MENU_ITEM_RESTORE_ID, Menu.NONE,
@@ -192,13 +193,13 @@ public class ChooseTabsFragment extends Fragment {
final SelectKioskFragment selectKioskFragment = new SelectKioskFragment();
selectKioskFragment.setOnSelectedListener((serviceId, kioskId, kioskName) ->
addTab(new Tab.KioskTab(serviceId, kioskId)));
selectKioskFragment.show(requireFragmentManager(), "select_kiosk");
selectKioskFragment.show(getParentFragmentManager(), "select_kiosk");
return;
case CHANNEL:
final SelectChannelFragment selectChannelFragment = new SelectChannelFragment();
selectChannelFragment.setOnSelectedListener((serviceId, url, name) ->
addTab(new Tab.ChannelTab(serviceId, url, name)));
selectChannelFragment.show(requireFragmentManager(), "select_channel");
selectChannelFragment.show(getParentFragmentManager(), "select_channel");
return;
case PLAYLIST:
final SelectPlaylistFragment selectPlaylistFragment = new SelectPlaylistFragment();
@@ -215,7 +216,7 @@ public class ChooseTabsFragment extends Fragment {
addTab(new Tab.PlaylistTab(serviceId, url, name));
}
});
selectPlaylistFragment.show(requireFragmentManager(), "select_playlist");
selectPlaylistFragment.show(getParentFragmentManager(), "select_playlist");
return;
default:
addTab(type.getTab());
@@ -277,7 +278,7 @@ public class ChooseTabsFragment extends Fragment {
return new ItemTouchHelper.SimpleCallback(ItemTouchHelper.UP | ItemTouchHelper.DOWN,
ItemTouchHelper.START | ItemTouchHelper.END) {
@Override
public int interpolateOutOfBoundsScroll(final RecyclerView recyclerView,
public int interpolateOutOfBoundsScroll(@NonNull final RecyclerView recyclerView,
final int viewSize,
final int viewSizeOutOfBounds,
final int totalSize,
@@ -290,9 +291,9 @@ public class ChooseTabsFragment extends Fragment {
}
@Override
public boolean onMove(final RecyclerView recyclerView,
final RecyclerView.ViewHolder source,
final RecyclerView.ViewHolder target) {
public boolean onMove(@NonNull final RecyclerView recyclerView,
@NonNull final RecyclerView.ViewHolder source,
@NonNull final RecyclerView.ViewHolder target) {
if (source.getItemViewType() != target.getItemViewType()
|| selectedTabsAdapter == null) {
return false;
@@ -315,7 +316,8 @@ public class ChooseTabsFragment extends Fragment {
}
@Override
public void onSwiped(final RecyclerView.ViewHolder viewHolder, final int swipeDir) {
public void onSwiped(@NonNull final RecyclerView.ViewHolder viewHolder,
final int swipeDir) {
final int position = viewHolder.getAdapterPosition();
tabList.remove(position);
selectedTabsAdapter.notifyItemRemoved(position);

View File

@@ -112,12 +112,16 @@ public abstract class Tab {
@Override
public boolean equals(final Object obj) {
if (obj == this) {
return true;
if (!(obj instanceof Tab)) {
return false;
}
final Tab other = (Tab) obj;
return getTabId() == other.getTabId();
}
return obj instanceof Tab && obj.getClass() == this.getClass()
&& ((Tab) obj).getTabId() == this.getTabId();
@Override
public int hashCode() {
return Objects.hashCode(getTabId());
}
/*//////////////////////////////////////////////////////////////////////////
@@ -358,8 +362,18 @@ public abstract class Tab {
@Override
public boolean equals(final Object obj) {
return super.equals(obj) && kioskServiceId == ((KioskTab) obj).kioskServiceId
&& Objects.equals(kioskId, ((KioskTab) obj).kioskId);
if (!(obj instanceof KioskTab)) {
return false;
}
final KioskTab other = (KioskTab) obj;
return super.equals(obj)
&& kioskServiceId == other.kioskServiceId
&& kioskId.equals(other.kioskId);
}
@Override
public int hashCode() {
return Objects.hash(getTabId(), kioskServiceId, kioskId);
}
public int getKioskServiceId() {
@@ -432,9 +446,19 @@ public abstract class Tab {
@Override
public boolean equals(final Object obj) {
return super.equals(obj) && channelServiceId == ((ChannelTab) obj).channelServiceId
&& Objects.equals(channelUrl, ((ChannelTab) obj).channelUrl)
&& Objects.equals(channelName, ((ChannelTab) obj).channelName);
if (!(obj instanceof ChannelTab)) {
return false;
}
final ChannelTab other = (ChannelTab) obj;
return super.equals(obj)
&& channelServiceId == other.channelServiceId
&& channelUrl.equals(other.channelName)
&& channelName.equals(other.channelName);
}
@Override
public int hashCode() {
return Objects.hash(getTabId(), channelServiceId, channelUrl, channelName);
}
public int getChannelServiceId() {
@@ -576,15 +600,30 @@ public abstract class Tab {
@Override
public boolean equals(final Object obj) {
if (!(super.equals(obj)
&& Objects.equals(playlistType, ((PlaylistTab) obj).playlistType)
&& Objects.equals(playlistName, ((PlaylistTab) obj).playlistName))) {
return false; // base objects are different
if (!(obj instanceof PlaylistTab)) {
return false;
}
return (playlistId == ((PlaylistTab) obj).playlistId) // local
|| (playlistServiceId == ((PlaylistTab) obj).playlistServiceId // remote
&& Objects.equals(playlistUrl, ((PlaylistTab) obj).playlistUrl));
final PlaylistTab other = (PlaylistTab) obj;
return super.equals(obj)
&& playlistServiceId == other.playlistServiceId // Remote
&& playlistId == other.playlistId // Local
&& playlistUrl.equals(other.playlistUrl)
&& playlistName.equals(other.playlistName)
&& playlistType == other.playlistType;
}
@Override
public int hashCode() {
return Objects.hash(
getTabId(),
playlistServiceId,
playlistId,
playlistUrl,
playlistName,
playlistType
);
}
public int getPlaylistServiceId() {

View File

@@ -0,0 +1,52 @@
package org.schabi.newpipe.streams.io;
import androidx.annotation.NonNull;
import java.io.IOException;
import java.io.InputStream;
/**
* Simply wraps a readable {@link SharpStream} allowing it to be used with built-in Java stuff that
* supports {@link InputStream}.
*/
public class SharpInputStream extends InputStream {
private final SharpStream stream;
public SharpInputStream(final SharpStream stream) throws IOException {
if (!stream.canRead()) {
throw new IOException("SharpStream is not readable");
}
this.stream = stream;
}
@Override
public int read() throws IOException {
return stream.read();
}
@Override
public int read(@NonNull final byte[] b) throws IOException {
return stream.read(b);
}
@Override
public int read(@NonNull final byte[] b, final int off, final int len) throws IOException {
return stream.read(b, off, len);
}
@Override
public long skip(final long n) throws IOException {
return stream.skip(n);
}
@Override
public int available() {
final long res = stream.available();
return res > Integer.MAX_VALUE ? Integer.MAX_VALUE : (int) res;
}
@Override
public void close() {
stream.close();
}
}

View File

@@ -0,0 +1,46 @@
package org.schabi.newpipe.streams.io;
import androidx.annotation.NonNull;
import java.io.IOException;
import java.io.OutputStream;
/**
* Simply wraps a writable {@link SharpStream} allowing it to be used with built-in Java stuff that
* supports {@link OutputStream}.
*/
public class SharpOutputStream extends OutputStream {
private final SharpStream stream;
public SharpOutputStream(final SharpStream stream) throws IOException {
if (!stream.canWrite()) {
throw new IOException("SharpStream is not writable");
}
this.stream = stream;
}
@Override
public void write(final int b) throws IOException {
stream.write((byte) b);
}
@Override
public void write(@NonNull final byte[] b) throws IOException {
stream.write(b);
}
@Override
public void write(@NonNull final byte[] b, final int off, final int len) throws IOException {
stream.write(b, off, len);
}
@Override
public void flush() throws IOException {
stream.flush();
}
@Override
public void close() {
stream.close();
}
}

View File

@@ -1,12 +1,20 @@
package org.schabi.newpipe.streams.io;
import java.io.Closeable;
import java.io.Flushable;
import java.io.IOException;
/**
* Based on C#'s Stream class.
* Based on C#'s Stream class. SharpStream is a wrapper around the 2 different APIs for SAF
* ({@link us.shandian.giga.io.FileStreamSAF}) and non-SAF ({@link us.shandian.giga.io.FileStream}).
* It has both input and output like in C#, while in Java those are usually different classes.
* {@link SharpInputStream} and {@link SharpOutputStream} are simple classes that wrap
* {@link SharpStream} and extend respectively {@link java.io.InputStream} and
* {@link java.io.OutputStream}, since unfortunately a class can only extend one class, so that a
* sharp stream can be used with built-in Java stuff that supports {@link java.io.InputStream}
* or {@link java.io.OutputStream}.
*/
public abstract class SharpStream implements Closeable {
public abstract class SharpStream implements Closeable, Flushable {
public abstract int read() throws IOException;
public abstract int read(byte[] buffer) throws IOException;

View File

@@ -1,6 +1,5 @@
package us.shandian.giga.io;
package org.schabi.newpipe.streams.io;
import android.annotation.TargetApi;
import android.content.ContentResolver;
import android.content.Context;
import android.content.Intent;
@@ -13,6 +12,9 @@ import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.documentfile.provider.DocumentFile;
import org.schabi.newpipe.settings.NewPipeSettings;
import org.schabi.newpipe.util.FilePickerActivityHelper;
import java.io.File;
import java.io.IOException;
import java.net.URI;
@@ -21,10 +23,11 @@ import java.util.Collections;
import static android.provider.DocumentsContract.Document.COLUMN_DISPLAY_NAME;
import static android.provider.DocumentsContract.Root.COLUMN_DOCUMENT_ID;
import static org.schabi.newpipe.extractor.utils.Utils.isNullOrEmpty;
public class StoredDirectoryHelper {
public final static int PERMISSION_FLAGS = Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION;
public static final int PERMISSION_FLAGS = Intent.FLAG_GRANT_READ_URI_PERMISSION
| Intent.FLAG_GRANT_WRITE_URI_PERMISSION;
private File ioTree;
private DocumentFile docTree;
@@ -33,7 +36,8 @@ public class StoredDirectoryHelper {
private final String tag;
public StoredDirectoryHelper(@NonNull Context context, @NonNull Uri path, String tag) throws IOException {
public StoredDirectoryHelper(@NonNull final Context context, @NonNull final Uri path,
final String tag) throws IOException {
this.tag = tag;
if (ContentResolver.SCHEME_FILE.equalsIgnoreCase(path.getScheme())) {
@@ -45,51 +49,49 @@ public class StoredDirectoryHelper {
try {
this.context.getContentResolver().takePersistableUriPermission(path, PERMISSION_FLAGS);
} catch (Exception e) {
} catch (final Exception e) {
throw new IOException(e);
}
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP)
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
throw new IOException("Storage Access Framework with Directory API is not available");
}
this.docTree = DocumentFile.fromTreeUri(context, path);
if (this.docTree == null)
if (this.docTree == null) {
throw new IOException("Failed to create the tree from Uri");
}
}
@TargetApi(Build.VERSION_CODES.KITKAT)
public StoredDirectoryHelper(@NonNull URI location, String tag) {
ioTree = new File(location);
this.tag = tag;
}
public StoredFileHelper createFile(String filename, String mime) {
public StoredFileHelper createFile(final String filename, final String mime) {
return createFile(filename, mime, false);
}
public StoredFileHelper createUniqueFile(String name, String mime) {
ArrayList<String> matches = new ArrayList<>();
String[] filename = splitFilename(name);
String lcFilename = filename[0].toLowerCase();
public StoredFileHelper createUniqueFile(final String name, final String mime) {
final ArrayList<String> matches = new ArrayList<>();
final String[] filename = splitFilename(name);
final String lcFilename = filename[0].toLowerCase();
if (docTree == null || Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
for (File file : ioTree.listFiles())
for (final File file : ioTree.listFiles()) {
addIfStartWith(matches, lcFilename, file.getName());
}
} else {
// warning: SAF file listing is very slow
Uri docTreeChildren = DocumentsContract.buildChildDocumentsUriUsingTree(
docTree.getUri(), DocumentsContract.getDocumentId(docTree.getUri())
);
final Uri docTreeChildren = DocumentsContract.buildChildDocumentsUriUsingTree(
docTree.getUri(), DocumentsContract.getDocumentId(docTree.getUri()));
String[] projection = {COLUMN_DISPLAY_NAME};
String selection = "(LOWER(" + COLUMN_DISPLAY_NAME + ") LIKE ?%";
ContentResolver cr = context.getContentResolver();
final String[] projection = new String[]{COLUMN_DISPLAY_NAME};
final String selection = "(LOWER(" + COLUMN_DISPLAY_NAME + ") LIKE ?%";
final ContentResolver cr = context.getContentResolver();
try (Cursor cursor = cr.query(docTreeChildren, projection, selection, new String[]{lcFilename}, null)) {
try (Cursor cursor = cr.query(docTreeChildren, projection, selection,
new String[]{lcFilename}, null)) {
if (cursor != null) {
while (cursor.moveToNext())
while (cursor.moveToNext()) {
addIfStartWith(matches, lcFilename, cursor.getString(0));
}
}
}
}
@@ -99,7 +101,7 @@ public class StoredDirectoryHelper {
} else {
// check if the filename is in use
String lcName = name.toLowerCase();
for (String testName : matches) {
for (final String testName : matches) {
if (testName.equals(lcName)) {
lcName = null;
break;
@@ -107,28 +109,34 @@ public class StoredDirectoryHelper {
}
// check if not in use
if (lcName != null) return createFile(name, mime, true);
if (lcName != null) {
return createFile(name, mime, true);
}
}
Collections.sort(matches, String::compareTo);
for (int i = 1; i < 1000; i++) {
if (Collections.binarySearch(matches, makeFileName(lcFilename, i, filename[1])) < 0)
if (Collections.binarySearch(matches, makeFileName(lcFilename, i, filename[1])) < 0) {
return createFile(makeFileName(filename[0], i, filename[1]), mime, true);
}
}
return createFile(String.valueOf(System.currentTimeMillis()).concat(filename[1]), mime, false);
return createFile(String.valueOf(System.currentTimeMillis()).concat(filename[1]), mime,
false);
}
private StoredFileHelper createFile(String filename, String mime, boolean safe) {
StoredFileHelper storage;
private StoredFileHelper createFile(final String filename, final String mime,
final boolean safe) {
final StoredFileHelper storage;
try {
if (docTree == null)
if (docTree == null) {
storage = new StoredFileHelper(ioTree, filename, mime);
else
} else {
storage = new StoredFileHelper(context, docTree, filename, mime, safe);
} catch (IOException e) {
}
} catch (final IOException e) {
return null;
}
@@ -146,7 +154,7 @@ public class StoredDirectoryHelper {
}
/**
* Indicates whatever if is possible access using the {@code java.io} API
* Indicates whether it's using the {@code java.io} API.
*
* @return {@code true} for Java I/O API, otherwise, {@code false} for Storage Access Framework
*/
@@ -169,7 +177,9 @@ public class StoredDirectoryHelper {
return ioTree.exists() || ioTree.mkdirs();
}
if (docTree.exists()) return true;
if (docTree.exists()) {
return true;
}
try {
DocumentFile parent;
@@ -177,14 +187,18 @@ public class StoredDirectoryHelper {
while (true) {
parent = docTree.getParentFile();
if (parent == null || child == null) break;
if (parent.exists()) return true;
if (parent == null || child == null) {
break;
}
if (parent.exists()) {
return true;
}
parent.createDirectory(child);
child = parent.getName();// for the next iteration
child = parent.getName(); // for the next iteration
}
} catch (Exception e) {
} catch (final Exception ignored) {
// no more parent directories or unsupported by the storage provider
}
@@ -195,13 +209,13 @@ public class StoredDirectoryHelper {
return tag;
}
public Uri findFile(String filename) {
public Uri findFile(final String filename) {
if (docTree == null) {
File res = new File(ioTree, filename);
final File res = new File(ioTree, filename);
return res.exists() ? Uri.fromFile(res) : null;
}
DocumentFile res = findFileSAFHelper(context, docTree, filename);
final DocumentFile res = findFileSAFHelper(context, docTree, filename);
return res == null ? null : res.getUri();
}
@@ -209,82 +223,115 @@ public class StoredDirectoryHelper {
return docTree == null ? ioTree.canWrite() : docTree.canWrite();
}
/**
* @return {@code false} if the storage is direct, or the SAF storage is valid; {@code true} if
* SAF access to this SAF storage is denied (e.g. the user clicked on {@code Android settings ->
* Apps & notifications -> NewPipe -> Storage & cache -> Clear access});
*/
public boolean isInvalidSafStorage() {
return docTree != null && docTree.getName() == null;
}
@NonNull
@Override
public String toString() {
return (docTree == null ? Uri.fromFile(ioTree) : docTree.getUri()).toString();
}
////////////////////
// Utils
///////////////////
private static void addIfStartWith(ArrayList<String> list, @NonNull String base, String str) {
if (str == null || str.isEmpty()) return;
str = str.toLowerCase();
if (str.startsWith(base)) list.add(str);
private static void addIfStartWith(final ArrayList<String> list, @NonNull final String base,
final String str) {
if (isNullOrEmpty(str)) {
return;
}
final String lowerStr = str.toLowerCase();
if (lowerStr.startsWith(base)) {
list.add(lowerStr);
}
}
private static String[] splitFilename(@NonNull String filename) {
int dotIndex = filename.lastIndexOf('.');
private static String[] splitFilename(@NonNull final String filename) {
final int dotIndex = filename.lastIndexOf('.');
if (dotIndex < 0 || (dotIndex == filename.length() - 1))
if (dotIndex < 0 || (dotIndex == filename.length() - 1)) {
return new String[]{filename, ""};
}
return new String[]{filename.substring(0, dotIndex), filename.substring(dotIndex)};
}
private static String makeFileName(String name, int idx, String ext) {
private static String makeFileName(final String name, final int idx, final String ext) {
return name.concat(" (").concat(String.valueOf(idx)).concat(")").concat(ext);
}
/**
* Fast (but not enough) file/directory finder under the storage access framework
* Fast (but not enough) file/directory finder under the storage access framework.
*
* @param context The context
* @param tree Directory where search
* @param filename Target filename
* @return A {@link DocumentFile} contain the reference, otherwise, null
*/
static DocumentFile findFileSAFHelper(@Nullable Context context, DocumentFile tree, String filename) {
static DocumentFile findFileSAFHelper(@Nullable final Context context, final DocumentFile tree,
final String filename) {
if (context == null || Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
return tree.findFile(filename);// warning: this is very slow
return tree.findFile(filename); // warning: this is very slow
}
if (!tree.canRead()) return null;// missing read permission
if (!tree.canRead()) {
return null; // missing read permission
}
final int name = 0;
final int documentId = 1;
// LOWER() SQL function is not supported
String selection = COLUMN_DISPLAY_NAME + " = ?";
//String selection = COLUMN_DISPLAY_NAME + " LIKE ?%";
final String selection = COLUMN_DISPLAY_NAME + " = ?";
//final String selection = COLUMN_DISPLAY_NAME + " LIKE ?%";
Uri childrenUri = DocumentsContract.buildChildDocumentsUriUsingTree(
tree.getUri(), DocumentsContract.getDocumentId(tree.getUri())
);
String[] projection = {COLUMN_DISPLAY_NAME, COLUMN_DOCUMENT_ID};
ContentResolver contentResolver = context.getContentResolver();
final Uri childrenUri = DocumentsContract.buildChildDocumentsUriUsingTree(tree.getUri(),
DocumentsContract.getDocumentId(tree.getUri()));
final String[] projection = {COLUMN_DISPLAY_NAME, COLUMN_DOCUMENT_ID};
final ContentResolver contentResolver = context.getContentResolver();
filename = filename.toLowerCase();
final String lowerFilename = filename.toLowerCase();
try (Cursor cursor = contentResolver.query(childrenUri, projection, selection, new String[]{filename}, null)) {
if (cursor == null) return null;
try (Cursor cursor = contentResolver.query(childrenUri, projection, selection,
new String[]{lowerFilename}, null)) {
if (cursor == null) {
return null;
}
while (cursor.moveToNext()) {
if (cursor.isNull(name) || !cursor.getString(name).toLowerCase().startsWith(filename))
if (cursor.isNull(name)
|| !cursor.getString(name).toLowerCase().startsWith(lowerFilename)) {
continue;
}
return DocumentFile.fromSingleUri(
context, DocumentsContract.buildDocumentUriUsingTree(
tree.getUri(), cursor.getString(documentId)
)
);
return DocumentFile.fromSingleUri(context,
DocumentsContract.buildDocumentUriUsingTree(tree.getUri(),
cursor.getString(documentId)));
}
}
return null;
}
public static Intent getPicker(final Context ctx) {
if (NewPipeSettings.useStorageAccessFramework(ctx)) {
return new Intent(Intent.ACTION_OPEN_DOCUMENT_TREE)
.putExtra("android.content.extra.SHOW_ADVANCED", true)
.addFlags(Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION
| StoredDirectoryHelper.PERMISSION_FLAGS);
} else {
return new Intent(ctx, FilePickerActivityHelper.class)
.putExtra(FilePickerActivityHelper.EXTRA_ALLOW_MULTIPLE, false)
.putExtra(FilePickerActivityHelper.EXTRA_ALLOW_CREATE_DIR, true)
.putExtra(FilePickerActivityHelper.EXTRA_MODE,
FilePickerActivityHelper.MODE_DIR);
}
}
}

View File

@@ -0,0 +1,565 @@
package org.schabi.newpipe.streams.io;
import android.annotation.TargetApi;
import android.content.ContentResolver;
import android.content.Context;
import android.content.Intent;
import android.net.Uri;
import android.os.Build;
import android.os.Environment;
import android.provider.DocumentsContract;
import android.util.Log;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.documentfile.provider.DocumentFile;
import com.nononsenseapps.filepicker.Utils;
import org.schabi.newpipe.MainActivity;
import org.schabi.newpipe.settings.NewPipeSettings;
import org.schabi.newpipe.util.FilePickerActivityHelper;
import java.io.File;
import java.io.IOException;
import java.io.Serializable;
import java.net.URI;
import us.shandian.giga.io.FileStream;
import us.shandian.giga.io.FileStreamSAF;
public class StoredFileHelper implements Serializable {
private static final boolean DEBUG = MainActivity.DEBUG;
private static final String TAG = StoredFileHelper.class.getSimpleName();
private static final long serialVersionUID = 0L;
public static final String DEFAULT_MIME = "application/octet-stream";
private transient DocumentFile docFile;
private transient DocumentFile docTree;
private transient File ioFile;
private transient Context context;
protected String source;
private String sourceTree;
protected String tag;
private String srcName;
private String srcType;
public StoredFileHelper(final Context context, final Uri uri, final String mime) {
if (FilePickerActivityHelper.isOwnFileUri(context, uri)) {
ioFile = Utils.getFileForUri(uri);
source = Uri.fromFile(ioFile).toString();
} else {
docFile = DocumentFile.fromSingleUri(context, uri);
source = uri.toString();
}
this.context = context;
this.srcType = mime;
}
public StoredFileHelper(@Nullable final Uri parent, final String filename, final String mime,
final String tag) {
this.source = null; // this instance will be "invalid" see invalidate()/isInvalid() methods
this.srcName = filename;
this.srcType = mime == null ? DEFAULT_MIME : mime;
if (parent != null) {
this.sourceTree = parent.toString();
}
this.tag = tag;
}
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
StoredFileHelper(@Nullable final Context context, final DocumentFile tree,
final String filename, final String mime, final boolean safe)
throws IOException {
this.docTree = tree;
this.context = context;
final DocumentFile res;
if (safe) {
// no conflicts (the filename is not in use)
res = this.docTree.createFile(mime, filename);
if (res == null) {
throw new IOException("Cannot create the file");
}
} else {
res = createSAF(context, mime, filename);
}
this.docFile = res;
this.source = docFile.getUri().toString();
this.sourceTree = docTree.getUri().toString();
this.srcName = this.docFile.getName();
this.srcType = this.docFile.getType();
}
StoredFileHelper(final File location, final String filename, final String mime)
throws IOException {
this.ioFile = new File(location, filename);
if (this.ioFile.exists()) {
if (!this.ioFile.isFile() && !this.ioFile.delete()) {
throw new IOException("The filename is already in use by non-file entity "
+ "and cannot overwrite it");
}
} else {
if (!this.ioFile.createNewFile()) {
throw new IOException("Cannot create the file");
}
}
this.source = Uri.fromFile(this.ioFile).toString();
this.sourceTree = Uri.fromFile(location).toString();
this.srcName = ioFile.getName();
this.srcType = mime;
}
@TargetApi(Build.VERSION_CODES.KITKAT)
public StoredFileHelper(final Context context, @Nullable final Uri parent,
@NonNull final Uri path, final String tag) throws IOException {
this.tag = tag;
this.source = path.toString();
if (path.getScheme() == null
|| path.getScheme().equalsIgnoreCase(ContentResolver.SCHEME_FILE)) {
this.ioFile = new File(URI.create(this.source));
} else {
final DocumentFile file = DocumentFile.fromSingleUri(context, path);
if (file == null) {
throw new RuntimeException("SAF not available");
}
this.context = context;
if (file.getName() == null) {
this.source = null;
return;
} else {
this.docFile = file;
takePermissionSAF();
}
}
if (parent != null) {
if (!ContentResolver.SCHEME_FILE.equals(parent.getScheme())) {
this.docTree = DocumentFile.fromTreeUri(context, parent);
}
this.sourceTree = parent.toString();
}
this.srcName = getName();
this.srcType = getType();
}
public static StoredFileHelper deserialize(@NonNull final StoredFileHelper storage,
final Context context) throws IOException {
final Uri treeUri = storage.sourceTree == null ? null : Uri.parse(storage.sourceTree);
if (storage.isInvalid()) {
return new StoredFileHelper(treeUri, storage.srcName, storage.srcType, storage.tag);
}
final StoredFileHelper instance = new StoredFileHelper(context, treeUri,
Uri.parse(storage.source), storage.tag);
// under SAF, if the target document is deleted, conserve the filename and mime
if (instance.srcName == null) {
instance.srcName = storage.srcName;
}
if (instance.srcType == null) {
instance.srcType = storage.srcType;
}
return instance;
}
public SharpStream getStream() throws IOException {
assertValid();
if (docFile == null) {
return new FileStream(ioFile);
} else {
return new FileStreamSAF(context.getContentResolver(), docFile.getUri());
}
}
/**
* Indicates whether it's using the {@code java.io} API.
*
* @return {@code true} for Java I/O API, otherwise, {@code false} for Storage Access Framework
*/
public boolean isDirect() {
assertValid();
return docFile == null;
}
public boolean isInvalid() {
return source == null;
}
public Uri getUri() {
assertValid();
return docFile == null ? Uri.fromFile(ioFile) : docFile.getUri();
}
public Uri getParentUri() {
assertValid();
return sourceTree == null ? null : Uri.parse(sourceTree);
}
public void truncate() throws IOException {
assertValid();
try (SharpStream fs = getStream()) {
fs.setLength(0);
}
}
public boolean delete() {
if (source == null) {
return true;
}
if (docFile == null) {
return ioFile.delete();
}
final boolean res = docFile.delete();
try {
final int flags = Intent.FLAG_GRANT_READ_URI_PERMISSION
| Intent.FLAG_GRANT_WRITE_URI_PERMISSION;
context.getContentResolver().releasePersistableUriPermission(docFile.getUri(), flags);
} catch (final Exception ex) {
// nothing to do
}
return res;
}
public long length() {
assertValid();
return docFile == null ? ioFile.length() : docFile.length();
}
public boolean canWrite() {
if (source == null) {
return false;
}
return docFile == null ? ioFile.canWrite() : docFile.canWrite();
}
public String getName() {
if (source == null) {
return srcName;
} else if (docFile == null) {
return ioFile.getName();
}
final String name = docFile.getName();
return name == null ? srcName : name;
}
public String getType() {
if (source == null || docFile == null) {
return srcType;
}
final String type = docFile.getType();
return type == null ? srcType : type;
}
public String getTag() {
return tag;
}
public boolean existsAsFile() {
if (source == null || (docFile == null && ioFile == null)) {
if (DEBUG) {
Log.d(TAG, "existsAsFile called but something is null: source = ["
+ (source == null ? "null => storage is invalid" : source)
+ "], docFile = [" + (docFile == null ? "null" : docFile)
+ "], ioFile = [" + (ioFile == null ? "null" : ioFile) + "]");
}
return false;
}
// WARNING: DocumentFile.exists() and DocumentFile.isFile() methods are slow
// docFile.isVirtual() means it is non-physical?
return docFile == null
? (ioFile.exists() && ioFile.isFile())
: (docFile.exists() && docFile.isFile());
}
public boolean create() {
assertValid();
final boolean result;
if (docFile == null) {
try {
result = ioFile.createNewFile();
} catch (final IOException e) {
return false;
}
} else if (docTree == null) {
result = false;
} else {
if (!docTree.canRead() || !docTree.canWrite()) {
return false;
}
try {
docFile = createSAF(context, srcType, srcName);
if (docFile.getName() == null) {
return false;
}
result = true;
} catch (final IOException e) {
return false;
}
}
if (result) {
source = (docFile == null ? Uri.fromFile(ioFile) : docFile.getUri()).toString();
srcName = getName();
srcType = getType();
}
return result;
}
public void invalidate() {
if (source == null) {
return;
}
srcName = getName();
srcType = getType();
source = null;
docTree = null;
docFile = null;
ioFile = null;
context = null;
}
public boolean equals(final StoredFileHelper storage) {
if (this == storage) {
return true;
}
// note: do not compare tags, files can have the same parent folder
//if (stringMismatch(this.tag, storage.tag)) return false;
if (stringMismatch(getLowerCase(this.sourceTree), getLowerCase(this.sourceTree))) {
return false;
}
if (this.isInvalid() || storage.isInvalid()) {
if (this.srcName == null || storage.srcName == null || this.srcType == null
|| storage.srcType == null) {
return false;
}
return this.srcName.equalsIgnoreCase(storage.srcName)
&& this.srcType.equalsIgnoreCase(storage.srcType);
}
if (this.isDirect() != storage.isDirect()) {
return false;
}
if (this.isDirect()) {
return this.ioFile.getPath().equalsIgnoreCase(storage.ioFile.getPath());
}
return DocumentsContract.getDocumentId(this.docFile.getUri())
.equalsIgnoreCase(DocumentsContract.getDocumentId(storage.docFile.getUri()));
}
@NonNull
@Override
public String toString() {
if (source == null) {
return "[Invalid state] name=" + srcName + " type=" + srcType + " tag=" + tag;
} else {
return "sourceFile=" + source + " treeSource=" + (sourceTree == null ? "" : sourceTree)
+ " tag=" + tag;
}
}
private void assertValid() {
if (source == null) {
throw new IllegalStateException("In invalid state");
}
}
private void takePermissionSAF() throws IOException {
try {
context.getContentResolver().takePersistableUriPermission(docFile.getUri(),
StoredDirectoryHelper.PERMISSION_FLAGS);
} catch (final Exception e) {
if (docFile.getName() == null) {
throw new IOException(e);
}
}
}
@NonNull
private DocumentFile createSAF(@Nullable final Context ctx, final String mime,
final String filename) throws IOException {
DocumentFile res = StoredDirectoryHelper.findFileSAFHelper(ctx, docTree, filename);
if (res != null && res.exists() && res.isDirectory()) {
if (!res.delete()) {
throw new IOException("Directory with the same name found but cannot delete");
}
res = null;
}
if (res == null) {
res = this.docTree.createFile(srcType == null ? DEFAULT_MIME : mime, filename);
if (res == null) {
throw new IOException("Cannot create the file");
}
}
return res;
}
private String getLowerCase(final String str) {
return str == null ? null : str.toLowerCase();
}
private boolean stringMismatch(final String str1, final String str2) {
if (str1 == null && str2 == null) {
return false;
}
if ((str1 == null) != (str2 == null)) {
return true;
}
return !str1.equals(str2);
}
public static Intent getPicker(@NonNull final Context ctx) {
if (NewPipeSettings.useStorageAccessFramework(ctx)) {
return new Intent(Intent.ACTION_OPEN_DOCUMENT)
.putExtra("android.content.extra.SHOW_ADVANCED", true)
.setType("*/*")
.addCategory(Intent.CATEGORY_OPENABLE)
.addFlags(Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION
| StoredDirectoryHelper.PERMISSION_FLAGS);
} else {
return new Intent(ctx, FilePickerActivityHelper.class)
.putExtra(FilePickerActivityHelper.EXTRA_ALLOW_MULTIPLE, false)
.putExtra(FilePickerActivityHelper.EXTRA_ALLOW_CREATE_DIR, true)
.putExtra(FilePickerActivityHelper.EXTRA_SINGLE_CLICK, true)
.putExtra(FilePickerActivityHelper.EXTRA_MODE,
FilePickerActivityHelper.MODE_FILE);
}
}
public static Intent getPicker(@NonNull final Context ctx, @Nullable final Uri initialPath) {
return applyInitialPathToPickerIntent(ctx, getPicker(ctx), initialPath, null);
}
public static Intent getNewPicker(@NonNull final Context ctx,
@Nullable final String filename,
@NonNull final String mimeType,
@Nullable final Uri initialPath) {
final Intent i;
if (NewPipeSettings.useStorageAccessFramework(ctx)) {
i = new Intent(Intent.ACTION_CREATE_DOCUMENT)
.putExtra("android.content.extra.SHOW_ADVANCED", true)
.setType(mimeType)
.addCategory(Intent.CATEGORY_OPENABLE)
.addFlags(Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION
| StoredDirectoryHelper.PERMISSION_FLAGS);
if (filename != null) {
i.putExtra(Intent.EXTRA_TITLE, filename);
}
} else {
i = new Intent(ctx, FilePickerActivityHelper.class)
.putExtra(FilePickerActivityHelper.EXTRA_ALLOW_MULTIPLE, false)
.putExtra(FilePickerActivityHelper.EXTRA_ALLOW_CREATE_DIR, true)
.putExtra(FilePickerActivityHelper.EXTRA_ALLOW_EXISTING_FILE, true)
.putExtra(FilePickerActivityHelper.EXTRA_MODE,
FilePickerActivityHelper.MODE_NEW_FILE);
}
return applyInitialPathToPickerIntent(ctx, i, initialPath, filename);
}
private static Intent applyInitialPathToPickerIntent(@NonNull final Context ctx,
@NonNull final Intent intent,
@Nullable final Uri initialPath,
@Nullable final String filename) {
if (NewPipeSettings.useStorageAccessFramework(ctx)) {
if (initialPath == null) {
return intent; // nothing to do, no initial path provided
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
return intent.putExtra(DocumentsContract.EXTRA_INITIAL_URI, initialPath);
} else {
return intent; // can't set initial path on API < 26
}
} else {
if (initialPath == null && filename == null) {
return intent; // nothing to do, no initial path and no file name provided
}
File file;
if (initialPath == null) {
// The only way to set the previewed filename in non-SAF FilePicker is to set a
// starting path ending with that filename. So when the initialPath is null but
// filename isn't just default to the external storage directory.
file = Environment.getExternalStorageDirectory();
} else {
try {
file = Utils.getFileForUri(initialPath);
} catch (final Throwable ignored) {
// getFileForUri() can't decode paths to 'storage', fallback to this
file = new File(initialPath.toString());
}
}
// remove any filename at the end of the path (get the parent directory in that case)
if (!file.exists() || !file.isDirectory()) {
file = file.getParentFile();
if (file == null || !file.exists()) {
// default to the external storage directory in case of an invalid path
file = Environment.getExternalStorageDirectory();
}
// else: file is surely a directory
}
if (filename != null) {
// append a filename so that the non-SAF FilePicker shows it as the preview
file = new File(file, filename);
}
return intent
.putExtra(FilePickerActivityHelper.EXTRA_START_PATH, file.getAbsolutePath());
}
}
}

View File

@@ -1,6 +1,5 @@
package org.schabi.newpipe.util;
import android.content.Context;
import android.text.Layout;
import android.text.Selection;
import android.text.Spannable;
@@ -11,27 +10,14 @@ import android.view.MotionEvent;
import android.view.View;
import android.widget.TextView;
import org.schabi.newpipe.extractor.NewPipe;
import org.schabi.newpipe.extractor.StreamingService;
import org.schabi.newpipe.extractor.exceptions.ExtractionException;
import org.schabi.newpipe.extractor.exceptions.ParsingException;
import org.schabi.newpipe.extractor.linkhandler.LinkHandlerFactory;
import org.schabi.newpipe.extractor.stream.StreamInfo;
import org.schabi.newpipe.player.playqueue.PlayQueue;
import org.schabi.newpipe.player.playqueue.SinglePlayQueue;
import org.schabi.newpipe.util.external_communication.ShareUtils;
import org.schabi.newpipe.util.external_communication.InternalUrlsHandler;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
import io.reactivex.rxjava3.core.Single;
import io.reactivex.rxjava3.schedulers.Schedulers;
import io.reactivex.rxjava3.disposables.CompositeDisposable;
public class CommentTextOnTouchListener implements View.OnTouchListener {
public static final CommentTextOnTouchListener INSTANCE = new CommentTextOnTouchListener();
private static final Pattern TIMESTAMP_PATTERN = Pattern.compile("(.*)#timestamp=(\\d+)");
@Override
public boolean onTouch(final View v, final MotionEvent event) {
if (!(v instanceof TextView)) {
@@ -64,13 +50,12 @@ public class CommentTextOnTouchListener implements View.OnTouchListener {
if (link.length != 0) {
if (action == MotionEvent.ACTION_UP) {
boolean handled = false;
if (link[0] instanceof URLSpan) {
handled = handleUrl(v.getContext(), (URLSpan) link[0]);
}
if (!handled) {
ShareUtils.openUrlInBrowser(v.getContext(),
((URLSpan) link[0]).getURL(), false);
final String url = ((URLSpan) link[0]).getURL();
if (!InternalUrlsHandler.handleUrlCommentsTimestamp(
new CompositeDisposable(), v.getContext(), url)) {
ShareUtils.openUrlInBrowser(v.getContext(), url, false);
}
}
} else if (action == MotionEvent.ACTION_DOWN) {
Selection.setSelection(buffer,
@@ -83,52 +68,4 @@ public class CommentTextOnTouchListener implements View.OnTouchListener {
}
return false;
}
private boolean handleUrl(final Context context, final URLSpan urlSpan) {
String url = urlSpan.getURL();
int seconds = -1;
final Matcher matcher = TIMESTAMP_PATTERN.matcher(url);
if (matcher.matches()) {
url = matcher.group(1);
seconds = Integer.parseInt(matcher.group(2));
}
final StreamingService service;
final StreamingService.LinkType linkType;
try {
service = NewPipe.getServiceByUrl(url);
linkType = service.getLinkTypeByUrl(url);
} catch (final ExtractionException e) {
return false;
}
if (linkType == StreamingService.LinkType.NONE) {
return false;
}
if (linkType == StreamingService.LinkType.STREAM && seconds != -1) {
return playOnPopup(context, url, service, seconds);
} else {
NavigationHelper.openRouterActivity(context, url);
return true;
}
}
private boolean playOnPopup(final Context context, final String url,
final StreamingService service, final int seconds) {
final LinkHandlerFactory factory = service.getStreamLHFactory();
final String cleanUrl;
try {
cleanUrl = factory.getUrl(factory.getId(url));
} catch (final ParsingException e) {
return false;
}
final Single single
= ExtractorHelper.getStreamInfo(service.getServiceId(), cleanUrl, false);
single.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(info -> {
final PlayQueue playQueue
= new SinglePlayQueue((StreamInfo) info, seconds * 1000);
NavigationHelper.playOnPopupPlayer(context, playQueue, false);
});
return true;
}
}

View File

@@ -12,13 +12,16 @@ import android.view.KeyEvent;
import androidx.annotation.Dimension;
import androidx.annotation.NonNull;
import androidx.core.content.ContextCompat;
import androidx.preference.PreferenceManager;
import org.schabi.newpipe.App;
import org.schabi.newpipe.R;
public final class DeviceUtils {
private static final String AMAZON_FEATURE_FIRE_TV = "amazon.hardware.fire_tv";
private static Boolean isTV = null;
private static Boolean isFireTV = null;
/*
* Devices that do not support media tunneling
@@ -33,6 +36,16 @@ public final class DeviceUtils {
private DeviceUtils() {
}
public static boolean isFireTv() {
if (isFireTV != null) {
return isFireTV;
}
isFireTV =
App.getApp().getPackageManager().hasSystemFeature(AMAZON_FEATURE_FIRE_TV);
return isFireTV;
}
public static boolean isTv(final Context context) {
if (isTV != null) {
return isTV;
@@ -43,7 +56,7 @@ public final class DeviceUtils {
// from doc: https://developer.android.com/training/tv/start/hardware.html#runtime-check
boolean isTv = ContextCompat.getSystemService(context, UiModeManager.class)
.getCurrentModeType() == Configuration.UI_MODE_TYPE_TELEVISION
|| pm.hasSystemFeature(AMAZON_FEATURE_FIRE_TV)
|| isFireTv()
|| pm.hasSystemFeature(PackageManager.FEATURE_TELEVISION);
// from https://stackoverflow.com/a/58932366
@@ -65,10 +78,18 @@ public final class DeviceUtils {
}
public static boolean isTablet(@NonNull final Context context) {
return (context
.getResources()
.getConfiguration().screenLayout & Configuration.SCREENLAYOUT_SIZE_MASK)
>= Configuration.SCREENLAYOUT_SIZE_LARGE;
final String tabletModeSetting = PreferenceManager.getDefaultSharedPreferences(context)
.getString(context.getString(R.string.tablet_mode_key), "");
if (tabletModeSetting.equals(context.getString(R.string.tablet_mode_on_key))) {
return true;
} else if (tabletModeSetting.equals(context.getString(R.string.tablet_mode_off_key))) {
return false;
}
// else automatically determine whether we are in a tablet or not
return (context.getResources().getConfiguration().screenLayout
& Configuration.SCREENLAYOUT_SIZE_MASK) >= Configuration.SCREENLAYOUT_SIZE_LARGE;
}
public static boolean isConfirmKey(final int keyCode) {

View File

@@ -30,6 +30,7 @@ import androidx.preference.PreferenceManager;
import org.schabi.newpipe.MainActivity;
import org.schabi.newpipe.R;
import org.schabi.newpipe.util.external_communication.TextLinkifier;
import org.schabi.newpipe.extractor.Info;
import org.schabi.newpipe.extractor.InfoItem;
import org.schabi.newpipe.extractor.ListExtractor.InfoItemsPage;
@@ -54,7 +55,7 @@ import java.util.List;
import io.reactivex.rxjava3.core.Maybe;
import io.reactivex.rxjava3.core.Single;
import io.reactivex.rxjava3.disposables.Disposable;
import io.reactivex.rxjava3.disposables.CompositeDisposable;
import static org.schabi.newpipe.extractor.utils.Utils.isNullOrEmpty;
@@ -268,18 +269,19 @@ public final class ExtractorHelper {
* @param metaInfos a list of meta information, can be null or empty
* @param metaInfoTextView the text view in which to show the formatted HTML
* @param metaInfoSeparator another view to be shown or hidden accordingly to the text view
* @return a disposable to be stored somewhere and disposed when activity/fragment is destroyed
* @param disposables disposables created by the method are added here and their lifecycle
* should be handled by the calling class
*/
public static Disposable showMetaInfoInTextView(@Nullable final List<MetaInfo> metaInfos,
final TextView metaInfoTextView,
final View metaInfoSeparator) {
public static void showMetaInfoInTextView(@Nullable final List<MetaInfo> metaInfos,
final TextView metaInfoTextView,
final View metaInfoSeparator,
final CompositeDisposable disposables) {
final Context context = metaInfoTextView.getContext();
if (metaInfos == null || metaInfos.isEmpty()
|| !PreferenceManager.getDefaultSharedPreferences(context).getBoolean(
context.getString(R.string.show_meta_info_key), true)) {
metaInfoTextView.setVisibility(View.GONE);
metaInfoSeparator.setVisibility(View.GONE);
return Disposable.empty();
} else {
final StringBuilder stringBuilder = new StringBuilder();
@@ -310,8 +312,8 @@ public final class ExtractorHelper {
}
metaInfoSeparator.setVisibility(View.VISIBLE);
return TextLinkifier.createLinksFromHtmlBlock(context, stringBuilder.toString(),
metaInfoTextView, HtmlCompat.FROM_HTML_SEPARATOR_LINE_BREAK_HEADING);
TextLinkifier.createLinksFromHtmlBlock(metaInfoTextView, stringBuilder.toString(),
HtmlCompat.FROM_HTML_SEPARATOR_LINE_BREAK_HEADING, null, disposables);
}
}

View File

@@ -1,22 +0,0 @@
package org.schabi.newpipe.util;
import java.io.File;
public final class FilePathUtils {
private FilePathUtils() { }
/**
* Check that the path is a valid directory path and it exists.
*
* @param path full path of directory,
* @return is path valid or not
*/
public static boolean isValidDirectoryPath(final String path) {
if (path == null || path.isEmpty()) {
return false;
}
final File file = new File(path);
return file.exists() && file.isDirectory();
}
}

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