1
0
mirror of https://github.com/TeamNewPipe/NewPipe synced 2026-03-28 02:29:43 +00:00

Compare commits

...

173 Commits

Author SHA1 Message Date
tobigr
da76ddbf19 Remove French (Lousiana) frc from app locales 2026-03-27 08:35:49 +01:00
TobiGr
75ba70ab41 Deleted translation using Weblate (Aymara (Southern)) 2026-03-27 08:30:49 +01:00
TobiGr
6d50fe79b8 Deleted translation using Weblate (Mainfränkisch) 2026-03-27 08:23:30 +01:00
TobiGr
f8c9ab8b11 Deleted translation using Weblate (Azerbaijani (Southern)) 2026-03-27 08:22:23 +01:00
TobiGr
949f0f3448 Deleted translation using Weblate (Azerbaijani (Southern)) 2026-03-27 08:22:22 +01:00
TobiGr
af08ddcf04 Deleted translation using Weblate (Kashmiri) 2026-03-27 08:19:25 +01:00
TobiGr
09c96159d1 Deleted translation using Weblate (Sicilian) 2026-03-27 08:19:05 +01:00
TobiGr
2dda39205d Deleted translation using Weblate (Aymara) 2026-03-27 08:18:48 +01:00
TobiGr
3112eefb99 Deleted translation using Weblate (Luri (Bakhtiari)) 2026-03-27 08:18:29 +01:00
TobiGr
cf0d7016ec Deleted translation using Weblate (Yiddish) 2026-03-27 08:18:05 +01:00
TobiGr
b0f2d509e6 Deleted translation using Weblate (Romany) 2026-03-27 08:17:48 +01:00
TobiGr
db61af15e8 Deleted translation using Weblate (Corsican) 2026-03-27 08:17:29 +01:00
TobiGr
80a47be218 Deleted translation using Weblate (Gaelic) 2026-03-27 08:17:11 +01:00
TobiGr
179a713561 Deleted translation using Weblate (Arabic (Tunisian)) 2026-03-27 08:16:35 +01:00
TobiGr
fa6412a9aa Deleted translation using Weblate (French (Louisiana)) 2026-03-27 08:15:57 +01:00
TobiGr
3f8d26d33e Deleted translation using Weblate (German (Low)) 2026-03-27 08:15:23 +01:00
TobiGr
2fec3a3c58 Deleted translation using Weblate (English (Old)) 2026-03-27 08:15:07 +01:00
TobiGr
08326c64cb Deleted translation using Weblate (English (Middle)) 2026-03-27 08:14:50 +01:00
TobiGr
831425a742 Deleted translation using Weblate (Arabic (Najdi)) 2026-03-27 08:14:12 +01:00
Hosted Weblate
fbf3b7a905 Translated using Weblate (Galician)
Currently translated at 94.9% (734 of 773 strings)

Translated using Weblate (Estonian)

Currently translated at 100.0% (773 of 773 strings)

Translated using Weblate (Vietnamese)

Currently translated at 98.3% (760 of 773 strings)

Translated using Weblate (Dutch)

Currently translated at 99.0% (766 of 773 strings)

Translated using Weblate (Chinese (Traditional Han script))

Currently translated at 100.0% (773 of 773 strings)

Translated using Weblate (Bulgarian)

Currently translated at 100.0% (773 of 773 strings)

Translated using Weblate (Chinese (Traditional Han script))

Currently translated at 100.0% (773 of 773 strings)

Translated using Weblate (Azerbaijani)

Currently translated at 100.0% (773 of 773 strings)

Translated using Weblate (Slovak)

Currently translated at 100.0% (773 of 773 strings)

Translated using Weblate (Czech)

Currently translated at 100.0% (773 of 773 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (773 of 773 strings)

Translated using Weblate (Turkish)

Currently translated at 100.0% (773 of 773 strings)

Translated using Weblate (Greek)

Currently translated at 100.0% (773 of 773 strings)

Translated using Weblate (Hungarian)

Currently translated at 100.0% (773 of 773 strings)

Translated using Weblate (German)

Currently translated at 100.0% (773 of 773 strings)

Translated using Weblate (Chinese (Simplified Han script))

Currently translated at 100.0% (773 of 773 strings)

Translated using Weblate (Turkish)

Currently translated at 99.0% (766 of 773 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (773 of 773 strings)

Translated using Weblate (French)

Currently translated at 100.0% (773 of 773 strings)

Translated using Weblate (Polish)

Currently translated at 100.0% (773 of 773 strings)

Translated using Weblate (Hindi)

Currently translated at 98.9% (765 of 773 strings)

Translated using Weblate (Korean)

Currently translated at 100.0% (773 of 773 strings)

Translated using Weblate (Punjabi)

Currently translated at 100.0% (773 of 773 strings)

Translated using Weblate (Bulgarian)

Currently translated at 100.0% (765 of 765 strings)

Translated using Weblate (Somali)

Currently translated at 73.3% (561 of 765 strings)

Translated using Weblate (Latvian)

Currently translated at 97.5% (746 of 765 strings)

Translated using Weblate (Latvian)

Currently translated at 97.6% (747 of 765 strings)

Translated using Weblate (Portuguese (Portugal))

Currently translated at 100.0% (765 of 765 strings)

Translated using Weblate (Punjabi)

Currently translated at 100.0% (90 of 90 strings)

Translated using Weblate (Hindi)

Currently translated at 100.0% (90 of 90 strings)

Translated using Weblate (Punjabi)

Currently translated at 100.0% (765 of 765 strings)

Translated using Weblate (Portuguese)

Currently translated at 100.0% (765 of 765 strings)

Translated using Weblate (Hindi)

Currently translated at 100.0% (765 of 765 strings)

Translated using Weblate (Turkish)

Currently translated at 34.4% (31 of 90 strings)

Translated using Weblate (Korean)

Currently translated at 100.0% (765 of 765 strings)

Translated using Weblate (Turkish)

Currently translated at 100.0% (765 of 765 strings)

Translated using Weblate (Romanian)

Currently translated at 12.2% (11 of 90 strings)

Translated using Weblate (Vietnamese)

Currently translated at 99.3% (760 of 765 strings)

Translated using Weblate (Vietnamese)

Currently translated at 73.3% (66 of 90 strings)

Translated using Weblate (Estonian)

Currently translated at 100.0% (765 of 765 strings)

Translated using Weblate (Chinese (Traditional Han script))

Currently translated at 100.0% (765 of 765 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (765 of 765 strings)

Added translation using Weblate (Corsican)

Translated using Weblate (Indonesian)

Currently translated at 86.6% (78 of 90 strings)

Translated using Weblate (German)

Currently translated at 100.0% (765 of 765 strings)

Translated using Weblate (Chinese (Simplified Han script))

Currently translated at 100.0% (765 of 765 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (765 of 765 strings)

Translated using Weblate (French)

Currently translated at 100.0% (765 of 765 strings)

Translated using Weblate (Indonesian)

Currently translated at 99.7% (763 of 765 strings)

Translated using Weblate (Greek)

Currently translated at 100.0% (765 of 765 strings)

Translated using Weblate (Italian)

Currently translated at 64.4% (58 of 90 strings)

Translated using Weblate (Hungarian)

Currently translated at 100.0% (765 of 765 strings)

Translated using Weblate (Azerbaijani)

Currently translated at 100.0% (765 of 765 strings)

Translated using Weblate (Slovak)

Currently translated at 100.0% (765 of 765 strings)

Translated using Weblate (Czech)

Currently translated at 100.0% (765 of 765 strings)

Translated using Weblate (Polish)

Currently translated at 100.0% (765 of 765 strings)

Translated using Weblate (Polish)

Currently translated at 100.0% (765 of 765 strings)

Translated using Weblate (Kabyle)

Currently translated at 27.9% (214 of 765 strings)

Translated using Weblate (German)

Currently translated at 99.8% (764 of 765 strings)

Co-authored-by: Agnieszka C <aga_04@o2.pl>
Co-authored-by: ButterflyOfFire <boffire@users.noreply.hosted.weblate.org>
Co-authored-by: Cabdi Waaxid Siciid <cabdiwaaxidsiciid100@gmail.com>
Co-authored-by: David Bol <CapybarGooner2@proton.me>
Co-authored-by: Dot Max <ahmadfadil.original@outlook.com>
Co-authored-by: EESF-2 <eesf-2@users.noreply.hosted.weblate.org>
Co-authored-by: Emin Tufan Çetin <etcetin@gmail.com>
Co-authored-by: Erenay <erenaydev@proton.me>
Co-authored-by: Femini <nizamismidov4@gmail.com>
Co-authored-by: Fjuro <fjuro@users.noreply.hosted.weblate.org>
Co-authored-by: Ghost of Sparta <makesocialfoss32@keemail.me>
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Hosted Weblate user 142171 <traaduttore@users.noreply.hosted.weblate.org>
Co-authored-by: Igor Rückert <igorruckert@yahoo.com.br>
Co-authored-by: Jeff Huang <s8321414@gmail.com>
Co-authored-by: Mickaël Binos <mickaelbinos@outlook.com>
Co-authored-by: Milan <mobrcian@hotmail.com>
Co-authored-by: Philip Goto <philip.goto@gmail.com>
Co-authored-by: Priit Jõerüüt <jrthwlate@users.noreply.hosted.weblate.org>
Co-authored-by: Quý <quycodergithub@gmail.com>
Co-authored-by: Random <random-r@users.noreply.hosted.weblate.org>
Co-authored-by: Stéphanie Olier <olier.stephanie130@gmail.com>
Co-authored-by: TXRdev Archive <lckphanaf9999@gmail.com>
Co-authored-by: Trunars <trunars@abv.bg>
Co-authored-by: Vasilis K. <skyhirules@gmail.com>
Co-authored-by: VfBFan <vfbfan@users.noreply.hosted.weblate.org>
Co-authored-by: WB <wb@users.noreply.hosted.weblate.org>
Co-authored-by: codetabsite <info.codetabteknofest@gmail.com>
Co-authored-by: justcontributor <kty5663@gmail.com>
Co-authored-by: rehork <cooky@e.email>
Co-authored-by: ssantos <ssantos@web.de>
Co-authored-by: whistlingwoods <72640314+whistlingwoods@users.noreply.github.com>
Co-authored-by: ℂ𝕠𝕠𝕠𝕝 (𝕘𝕚𝕥𝕙𝕦𝕓.𝕔𝕠𝕞/ℂ𝕠𝕠𝕠𝕝) <coool@mail.lv>
Co-authored-by: 大王叫我来巡山 <hamburger2048@users.noreply.hosted.weblate.org>
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/hi/
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/id/
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/it/
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/pa/
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/ro/
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/tr/
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/vi/
Translation: NewPipe/Metadata
2026-03-27 00:09:53 +00:00
Tobi
515bb6e94d Merge pull request #13363 from theimpulson/depUpdate
Update dependencies and Gradle to latest stable release
2026-03-18 10:16:47 +01:00
Aayush Gupta
349000857a Update dependencies and Gradle to latest stable release
Signed-off-by: Aayush Gupta <aayushgupta219@gmail.com>
2026-03-18 15:52:23 +08:00
Aayush Gupta
f1c608b396 Merge pull request #13352 from theimpulson/path
Port path related changes from refactor
2026-03-18 15:45:45 +08:00
Isira Seneviratne
5f1a270ca4 Fix database import 2026-03-16 14:48:23 +08:00
Isira Seneviratne
aba2a385fd Inline variable 2026-03-16 14:47:24 +08:00
Isira Seneviratne
71a3bf2855 Use InputStream#transferTo() 2026-03-16 14:47:17 +08:00
Isira Seneviratne
ebb937934a Fix DB import/export issue 2026-03-16 14:47:08 +08:00
Isira Seneviratne
223b240299 Refactor zip import/export using Path 2026-03-16 14:46:09 +08:00
Aayush Gupta
668af4fc3e Merge pull request #13347 from theimpulson/subscriptions
Port subscriptions related changes from refactor
2026-03-16 09:23:16 +08:00
evermind
bfcc31ec89 BackupRestoreSettingsFragment: add UI options to import/export subscriptions
* create SubscriptionsImportExportHelper to share common code used in
  SubscriptionFragment and BackupRestoreSettingsFragment
* Add UI options for import/export in BackupRestoreSettingsFragment
2026-03-15 20:54:58 +08:00
Aayush Gupta
6fa97e17f5 subscription: Port subscription import-export to workers from refactor
Please see https://github.com/TeamNewPipe/NewPipe/pull/11759/ for the original change

Signed-off-by: Aayush Gupta <aayushgupta219@gmail.com>
2026-03-15 20:51:29 +08:00
Aayush Gupta
0d65733e53 Merge pull request #13207 from TobiGr/small-fixes
Small improvements to Image handling
2026-03-15 19:21:06 +08:00
Aayush Gupta
9cc6f9fd68 Merge pull request #13305 from dustdfg/remove_freedroid_license
Remove freedroidwarn license
2026-03-15 19:18:32 +08:00
tobigr
79767f95f7 Merge branch 'master' into dev 2026-03-08 21:19:30 +01:00
Tobi
21b37b5fa4 Merge pull request #13296 from TeamNewPipe/release-0.28.4
Release 0.28.4
2026-03-08 11:07:54 -07:00
Tobi
675d15686d Merge pull request #13324 from TeamNewPipe/backport-13310-to-release-0.28.4
[Backport release-0.28.4] Add link to FAQ entry to "Sign in to confirm not a bot" exception message
2026-03-08 11:01:41 -07:00
tobigr
54021a96ea Complete sentence.
(cherry picked from commit 47624a575a)
2026-03-08 18:01:10 +00:00
AbsurdlyLongUsername
7c88f06196 Change to FAQ entry instead
(cherry picked from commit 3b3348e7a1)
2026-03-08 18:01:10 +00:00
AbsurdlyLongUsername
93f9a52e3e Change URL to FAQ
(cherry picked from commit 521f60af85)
2026-03-08 18:01:10 +00:00
AbsurdlyLongUsername
d9fd1bf003 Maybe this fix ktlint errors?
(cherry picked from commit b8ec9bf412)
2026-03-08 18:01:10 +00:00
AbsurdlyLongUsername
dc2e4aaea5 Add Sign in confirm not a bot issue URL to the exception error message
Make error panel and error activity exception message URLs clickable via extension method

Change ErrorMessage.getString to getText and return CharSequence, and use getText with formatArgs to preserve styling (i.e. URLs)

(cherry picked from commit e173bf4252)
2026-03-08 18:01:10 +00:00
Hosted Weblate
d5f941ff3d Translated using Weblate (Vietnamese)
Currently translated at 70.0% (63 of 90 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (765 of 765 strings)

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Nicolás Pérez <nicoperez241@proton.me>
Co-authored-by: Quý <quycodergithub@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/vi/
Translation: NewPipe/Metadata
2026-03-08 18:00:50 +00:00
Tobi
05f09c94d1 Merge pull request #13310 from absurdlylongusername/add-issue-url-sign-in-not-bot-exception
Add link to FAQ entry to "Sign in to confirm not a bot" exception message
2026-03-08 11:00:37 -07:00
Hosted Weblate
816f5f9aba Translated using Weblate (Vietnamese)
Currently translated at 70.0% (63 of 90 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (765 of 765 strings)

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Nicolás Pérez <nicoperez241@proton.me>
Co-authored-by: Quý <quycodergithub@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/vi/
Translation: NewPipe/Metadata
2026-03-08 18:57:50 +01:00
tobigr
47624a575a Complete sentence. 2026-03-08 18:54:56 +01:00
AbsurdlyLongUsername
3b3348e7a1 Change to FAQ entry instead 2026-03-07 23:38:51 +00:00
AbsurdlyLongUsername
521f60af85 Change URL to FAQ 2026-03-07 19:13:02 +00:00
Hosted Weblate
d497f1d88e Translated using Weblate (Latvian)
Currently translated at 97.9% (749 of 765 strings)

Translated using Weblate (Latvian)

Currently translated at 97.9% (749 of 765 strings)

Translated using Weblate (Latvian)

Currently translated at 18.8% (17 of 90 strings)

Translated using Weblate (Latvian)

Currently translated at 96.9% (742 of 765 strings)

Translated using Weblate (Latvian)

Currently translated at 97.3% (745 of 765 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (765 of 765 strings)

Translated using Weblate (Latvian)

Currently translated at 17.7% (16 of 90 strings)

Translated using Weblate (Turkish)

Currently translated at 100.0% (765 of 765 strings)

Translated using Weblate (Russian)

Currently translated at 100.0% (765 of 765 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (765 of 765 strings)

Translated using Weblate (Portuguese)

Currently translated at 100.0% (765 of 765 strings)

Translated using Weblate (Armenian)

Currently translated at 31.6% (242 of 765 strings)

Translated using Weblate (Portuguese (Portugal))

Currently translated at 100.0% (765 of 765 strings)

Translated using Weblate (Slovak)

Currently translated at 76.6% (69 of 90 strings)

Translated using Weblate (Azerbaijani)

Currently translated at 100.0% (765 of 765 strings)

Translated using Weblate (Slovak)

Currently translated at 100.0% (765 of 765 strings)

Translated using Weblate (Japanese)

Currently translated at 13.3% (12 of 90 strings)

Translated using Weblate (Czech)

Currently translated at 100.0% (90 of 90 strings)

Translated using Weblate (Korean)

Currently translated at 100.0% (765 of 765 strings)

Translated using Weblate (French)

Currently translated at 98.8% (89 of 90 strings)

Translated using Weblate (Japanese)

Currently translated at 95.5% (731 of 765 strings)

Translated using Weblate (Polish)

Currently translated at 56.6% (51 of 90 strings)

Translated using Weblate (Bulgarian)

Currently translated at 100.0% (765 of 765 strings)

Translated using Weblate (Chinese (Simplified Han script))

Currently translated at 100.0% (90 of 90 strings)

Translated using Weblate (Tamil)

Currently translated at 48.8% (44 of 90 strings)

Translated using Weblate (Hungarian)

Currently translated at 100.0% (765 of 765 strings)

Translated using Weblate (German)

Currently translated at 100.0% (765 of 765 strings)

Translated using Weblate (German)

Currently translated at 100.0% (90 of 90 strings)

Co-authored-by: 439JBYL80IGQTF25UXNR0X1BG <439jbyl80igqtf25uxnr0x1bg@users.noreply.hosted.weblate.org>
Co-authored-by: Agnieszka C <aga_04@o2.pl>
Co-authored-by: Davit Mayilyan <davit.mayilyan@protonmail.ch>
Co-authored-by: Erenay <erenaydev@proton.me>
Co-authored-by: Femini <nizamismidov4@gmail.com>
Co-authored-by: Fjuro <fjuro@users.noreply.hosted.weblate.org>
Co-authored-by: Ghost of Sparta <makesocialfoss32@keemail.me>
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Mickaël Binos <mickaelbinos@outlook.com>
Co-authored-by: Milan <mobrcian@hotmail.com>
Co-authored-by: Random <random-r@users.noreply.hosted.weblate.org>
Co-authored-by: Trunars <trunars@abv.bg>
Co-authored-by: VfBFan <vfbfan@users.noreply.hosted.weblate.org>
Co-authored-by: justcontributor <kty5663@gmail.com>
Co-authored-by: kuragehime <kuragehime641@gmail.com>
Co-authored-by: ssantos <ssantos@web.de>
Co-authored-by: தமிழ்நேரம் <tamilneram247@gmail.com>
Co-authored-by: ℂ𝕠𝕠𝕠𝕝 (𝕘𝕚𝕥𝕙𝕦𝕓.𝕔𝕠𝕞/ℂ𝕠𝕠𝕠𝕝) <coool@mail.lv>
Co-authored-by: 大王叫我来巡山 <hamburger2048@users.noreply.hosted.weblate.org>
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/cs/
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/de/
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/fr/
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/ja/
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/lv/
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/pl/
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/sk/
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/ta/
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/zh_Hans/
Translation: NewPipe/Metadata
2026-03-07 17:44:37 +01:00
Hosted Weblate
dda219a9e9 Translated using Weblate (Latvian)
Currently translated at 97.9% (749 of 765 strings)

Translated using Weblate (Latvian)

Currently translated at 97.9% (749 of 765 strings)

Translated using Weblate (Latvian)

Currently translated at 18.8% (17 of 90 strings)

Translated using Weblate (Latvian)

Currently translated at 96.9% (742 of 765 strings)

Translated using Weblate (Latvian)

Currently translated at 97.3% (745 of 765 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (765 of 765 strings)

Translated using Weblate (Latvian)

Currently translated at 17.7% (16 of 90 strings)

Translated using Weblate (Turkish)

Currently translated at 100.0% (765 of 765 strings)

Translated using Weblate (Russian)

Currently translated at 100.0% (765 of 765 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (765 of 765 strings)

Translated using Weblate (Portuguese)

Currently translated at 100.0% (765 of 765 strings)

Translated using Weblate (Armenian)

Currently translated at 31.6% (242 of 765 strings)

Translated using Weblate (Portuguese (Portugal))

Currently translated at 100.0% (765 of 765 strings)

Translated using Weblate (Slovak)

Currently translated at 76.6% (69 of 90 strings)

Translated using Weblate (Azerbaijani)

Currently translated at 100.0% (765 of 765 strings)

Translated using Weblate (Slovak)

Currently translated at 100.0% (765 of 765 strings)

Translated using Weblate (Japanese)

Currently translated at 13.3% (12 of 90 strings)

Translated using Weblate (Czech)

Currently translated at 100.0% (90 of 90 strings)

Translated using Weblate (Korean)

Currently translated at 100.0% (765 of 765 strings)

Translated using Weblate (French)

Currently translated at 98.8% (89 of 90 strings)

Translated using Weblate (Japanese)

Currently translated at 95.5% (731 of 765 strings)

Translated using Weblate (Polish)

Currently translated at 56.6% (51 of 90 strings)

Translated using Weblate (Bulgarian)

Currently translated at 100.0% (765 of 765 strings)

Translated using Weblate (Chinese (Simplified Han script))

Currently translated at 100.0% (90 of 90 strings)

Translated using Weblate (Tamil)

Currently translated at 48.8% (44 of 90 strings)

Translated using Weblate (Hungarian)

Currently translated at 100.0% (765 of 765 strings)

Translated using Weblate (German)

Currently translated at 100.0% (765 of 765 strings)

Translated using Weblate (German)

Currently translated at 100.0% (90 of 90 strings)

Co-authored-by: 439JBYL80IGQTF25UXNR0X1BG <439jbyl80igqtf25uxnr0x1bg@users.noreply.hosted.weblate.org>
Co-authored-by: Agnieszka C <aga_04@o2.pl>
Co-authored-by: Davit Mayilyan <davit.mayilyan@protonmail.ch>
Co-authored-by: Erenay <erenaydev@proton.me>
Co-authored-by: Femini <nizamismidov4@gmail.com>
Co-authored-by: Fjuro <fjuro@users.noreply.hosted.weblate.org>
Co-authored-by: Ghost of Sparta <makesocialfoss32@keemail.me>
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Mickaël Binos <mickaelbinos@outlook.com>
Co-authored-by: Milan <mobrcian@hotmail.com>
Co-authored-by: Random <random-r@users.noreply.hosted.weblate.org>
Co-authored-by: Trunars <trunars@abv.bg>
Co-authored-by: VfBFan <vfbfan@users.noreply.hosted.weblate.org>
Co-authored-by: justcontributor <kty5663@gmail.com>
Co-authored-by: kuragehime <kuragehime641@gmail.com>
Co-authored-by: ssantos <ssantos@web.de>
Co-authored-by: தமிழ்நேரம் <tamilneram247@gmail.com>
Co-authored-by: ℂ𝕠𝕠𝕠𝕝 (𝕘𝕚𝕥𝕙𝕦𝕓.𝕔𝕠𝕞/ℂ𝕠𝕠𝕠𝕝) <coool@mail.lv>
Co-authored-by: 大王叫我来巡山 <hamburger2048@users.noreply.hosted.weblate.org>
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/cs/
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/de/
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/fr/
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/ja/
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/lv/
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/pl/
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/sk/
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/ta/
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/zh_Hans/
Translation: NewPipe/Metadata
2026-03-06 20:30:04 +01:00
AbsurdlyLongUsername
b8ec9bf412 Maybe this fix ktlint errors? 2026-03-06 12:55:09 +00:00
AbsurdlyLongUsername
e173bf4252 Add Sign in confirm not a bot issue URL to the exception error message
Make error panel and error activity exception message URLs clickable via extension method

Change ErrorMessage.getString to getText and return CharSequence, and use getText with formatArgs to preserve styling (i.e. URLs)
2026-03-06 12:35:19 +00:00
Yevhen Babiichuk (DustDFG)
9f45aa571c Remove freedroidwarn license 2026-03-04 22:45:07 +02:00
Stypox
e6e5fc70f2 Merge pull request #13299 from TeamNewPipe/backport-13298-to-release-0.28.4 2026-03-02 09:27:38 +01:00
Yevhen Babiichuk (DustDFG)
8fa6f9670d Apply suggestion
(cherry picked from commit 0020a02a28)
2026-03-02 08:27:17 +00:00
Stypox
0cdf40cd5f Merge pull request #13298 from dustdfg/shareutils_kao 2026-03-02 09:26:52 +01:00
Yevhen Babiichuk (DustDFG)
0020a02a28 Apply suggestion 2026-03-02 08:42:07 +02:00
tobigr
1fbf9fc025 Bump version to 0.28.4 (1009) 2026-02-28 10:06:03 +01:00
tobigr
8b500c7b83 Add changelog for NewPipe 0.28.4 (1009) 2026-02-28 10:06:03 +01:00
tobigr
98a883d377 Update NewPipe Extractor to 0.26.0 2026-02-28 10:06:03 +01:00
Hosted Weblate
6dddcf3805 Translated using Weblate (French)
Currently translated at 100.0% (765 of 765 strings)

Translated using Weblate (Greek)

Currently translated at 100.0% (765 of 765 strings)

Translated using Weblate (Czech)

Currently translated at 100.0% (89 of 89 strings)

Translated using Weblate (Greek)

Currently translated at 99.8% (764 of 765 strings)

Translated using Weblate (Polish)

Currently translated at 100.0% (765 of 765 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (765 of 765 strings)

Translated using Weblate (Estonian)

Currently translated at 100.0% (765 of 765 strings)

Translated using Weblate (Chinese (Simplified Han script))

Currently translated at 100.0% (765 of 765 strings)

Translated using Weblate (Swedish)

Currently translated at 96.6% (86 of 89 strings)

Co-authored-by: Agnieszka C <aga_04@o2.pl>
Co-authored-by: Fjuro <fjuro@users.noreply.hosted.weblate.org>
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Mickaël Binos <mickaelbinos@outlook.com>
Co-authored-by: Mona Lisa <nickwick@users.noreply.hosted.weblate.org>
Co-authored-by: Priit Jõerüüt <jrthwlate@users.noreply.hosted.weblate.org>
Co-authored-by: Vasilis K. <skyhirules@gmail.com>
Co-authored-by: delvani <del.cidrak@users.noreply.hosted.weblate.org>
Co-authored-by: 大王叫我来巡山 <hamburger2048@users.noreply.hosted.weblate.org>
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/cs/
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/sv/
Translation: NewPipe/Metadata
2026-02-28 10:03:02 +01:00
Tobi
bf1633265c Merge pull request #13293 from TeamNewPipe/getQuantity-inconsistency
Fix inconsistency in getQuantity and add docs
2026-02-26 11:45:41 -08:00
Stypox
e22b046326 Fix inconsistency in getQuantity and add docs
`getQuantity()` was being called in a couple of places with `zeroCaseStringId=0`, but that wasn't documented anywhere, and if `count==0` then `getString(zeroCaseStringId /* == 0 */)` would be returned which doesn't make sense
2026-02-26 19:08:33 +01:00
Stypox
56fb31d0fd Merge pull request #13292 from dustdfg/kao_release_only 2026-02-26 11:01:56 +01:00
Yevhen Babiichuk (DustDFG)
042f9460b0 Don't show Keep Android Open popup on debug builds 2026-02-26 11:39:10 +02:00
Tobi
9f1e2c6fd0 Merge pull request #13282 from dustdfg/keep_android_open
Add warning banner about ongoing google certification for android apps
2026-02-25 17:08:49 -08:00
tobigr
66237abb3c KeepAndroidOpen: Choose website language from list of supported languages 2026-02-26 01:27:36 +01:00
Tobi
dd65db56a9 Merge pull request #13290 from dustdfg/correct_download_fragment
Correctly retrieve menu item inside download dialog
2026-02-25 15:42:05 -08:00
Yevhen Babiichuk (DustDFG)
195a76bb08 Correctly retrieve menu item inside download dialog 2026-02-26 01:09:07 +02:00
Yevhen Babiichuk (DustDFG)
06e4548c14 Add warning banner about ongoing google certification for android apps 2026-02-25 18:35:41 +02:00
Hosted Weblate
9a292e33f9 Translated using Weblate (Albanian)
Currently translated at 2.2% (2 of 89 strings)

Translated using Weblate (Georgian)

Currently translated at 55.0% (49 of 89 strings)

Translated using Weblate (Latvian)

Currently translated at 97.9% (748 of 764 strings)

Translated using Weblate (Georgian)

Currently translated at 96.5% (738 of 764 strings)

Co-authored-by: Flavjo Avdiu <flavjoavdiu10@gmail.com>
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Nikoloz <nukushatugushi@gmail.com>
Co-authored-by: ℂ𝕠𝕠𝕠𝕝 (𝕘𝕚𝕥𝕙𝕦𝕓.𝕔𝕠𝕞/ℂ𝕠𝕠𝕠𝕝) <coool@mail.lv>
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/ka/
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/sq/
Translation: NewPipe/Metadata
2026-02-22 11:32:29 +01:00
Tobi
d1ad1e843f Merge pull request #13276 from TeamNewPipe/depupdate
Update dependencies and Gradle to latest stable releases
2026-02-21 23:59:11 -08:00
Aayush Gupta
8902ce8678 Update dependencies and Gradle to latest stable releases
Signed-off-by: Aayush Gupta <aayushgupta219@gmail.com>
2026-02-22 10:46:58 +08:00
Aayush Gupta
9f193a3b00 Merge pull request #13272 from theimpulson/coilport
Migrate from Picasso to Coil
2026-02-22 10:35:43 +08:00
Tobi
232d023c02 Merge pull request #13030 from iampopovich/fix/6815-double-tap-to-resume
[fix] Hide controls when resuming playback via double tap
2026-02-21 11:08:51 -08:00
Alex Popov
423f95a65d Refactor double tap logic to use isPlaying() method for better readability 2026-02-21 20:03:25 +01:00
Alex Popov
caebf8461a Hide controls when resuming playback via double tap
Remove redundant comment about hiding controls on double tap
2026-02-21 19:54:59 +01:00
Tobi
52a9b60180 Merge pull request #13148 from dustdfg/extractor_enum_set
Accomodate extractor changes (EnumSet for service media capabilities)
2026-02-21 09:10:55 -08:00
Tobi
4bad882668 Merge pull request #13037 from dustdfg/tablet_related_items
VideoDetailFragment: hide relatedItemsLayout in tablet mode after fullscreen
2026-02-21 09:03:11 -08:00
tobigr
63e19890bf Update extractor to the latest version 2026-02-21 17:00:05 +01:00
Yevhen Babiichuk (DustDFG)
08b7da4b2b Accomodate extractor changes (EnumSet for service media capabilities) 2026-02-21 16:55:31 +01:00
Aayush Gupta
402a0aaaa3 Delete unused preference title and summary
Signed-off-by: Aayush Gupta <aayushgupta219@gmail.com>
2026-02-21 16:48:57 +08:00
Aayush Gupta
57364109f4 Migrate from Picasso to Coil
Based on changes from refactor

Signed-off-by: Aayush Gupta <aayushgupta219@gmail.com>
2026-02-21 16:48:57 +08:00
Clippy
4481dd7fe6 Merge pull request #13135 from dustdfg/player_loop_refactor
Misc player loop/stream refactors
2026-02-21 06:25:58 +00:00
Aayush Gupta
273b09a3e8 App: Migrate from Java to Kotlin
Based on changes from refactor branch for code parity

Signed-off-by: Aayush Gupta <aayushgupta219@gmail.com>
2026-02-21 00:06:12 +08:00
Tobi
35b70c5e9e Merge pull request #13194 from TeamNewPipe/r8fixes
Enable lint checks and resource shrinking
2026-02-20 00:47:16 -08:00
Aayush Gupta
653b33bdb9 FocusOverlayView: Avoid accessing restricted API
Signed-off-by: Aayush Gupta <aayushgupta219@gmail.com>
2026-02-20 16:16:50 +08:00
Aayush Gupta
5eb5f7533d ic_smart_display: Fix invalid vector path on older devices
Signed-off-by: Aayush Gupta <aayushgupta219@gmail.com>
2026-02-20 16:16:50 +08:00
Aayush Gupta
15829c882b lint: Supress more translation related errors
They should be fixed by translators or weblate in general.

 ../../src/main/res/values-lt/strings.xml:87: For locale "lt" (Lithuanian) the following quantity should also be defined: many (e.g. "1.1 obuolio")
 ../../src/main/res/values-is/strings.xml:318: The quantity 'one' matches more than one specific number in this locale (1, 21, 31, 41, 51, 61, 71, 81, 101, 1001, …), but the message did not include a formatting argument (such as %d). This is usually an internationalization error. See full issue explanation for more.

Signed-off-by: Aayush Gupta <aayushgupta219@gmail.com>
2026-02-20 16:16:50 +08:00
Aayush Gupta
10b943f37e DownloadDialog: Avoid using restricted API for menuitem
Signed-off-by: Aayush Gupta <aayushgupta219@gmail.com>
2026-02-20 16:16:50 +08:00
Aayush Gupta
3d43e52afb activity_player_queue_control: Switch to app:tint instead of android:tint
../../src/main/res/layout/activity_player_queue_control.xml:208: Must use app:tint instead of android:tint

Signed-off-by: Aayush Gupta <aayushgupta219@gmail.com>
2026-02-20 16:16:50 +08:00
Aayush Gupta
f64e40e958 DownloadRunnableFallback: Fix error with log tag being too long
Abbreviate Download -> DL

Signed-off-by: Aayush Gupta <aayushgupta219@gmail.com>
2026-02-20 16:16:50 +08:00
Aayush Gupta
2182ff12b7 Use correct constant for hiding keyboard
../../src/main/java/org/schabi/newpipe/local/subscription/dialog/FeedGroupDialog.kt:509: Must be one or more of: InputMethodManager.HIDE_IMPLICIT_ONLY, InputMethodManager.HIDE_NOT_ALWAYS

Signed-off-by: Aayush Gupta <aayushgupta219@gmail.com>
2026-02-20 16:16:50 +08:00
Aayush Gupta
b3f4cb8114 Use requireContext() instead of asserting non-null context
Signed-off-by: Aayush Gupta <aayushgupta219@gmail.com>
2026-02-20 16:16:50 +08:00
Aayush Gupta
deb6b4230d FinishedMIssionStore: Throw exception if column is missing
../../src/main/java/us/shandian/giga/get/sqlite/FinishedMissionStore.java:105: Value must be ≥ 0 but getColumnIndex can be -1

Signed-off-by: Aayush Gupta <aayushgupta219@gmail.com>
2026-02-20 16:16:50 +08:00
Aayush Gupta
2f3a993f8e ReCaptchaActivity: Supress lint error for missing super call
saveCookiesAndFinish method handles back navigation

Signed-off-by: Aayush Gupta <aayushgupta219@gmail.com>
2026-02-20 16:16:50 +08:00
Aayush Gupta
582f852e7a Add missing permission checks for notifications
Notifications can be disabled manually even after permission has been granted once.
Always check if they are enabled before notifying the user.

Signed-off-by: Aayush Gupta <aayushgupta219@gmail.com>
2026-02-20 16:16:50 +08:00
Aayush Gupta
89cb87b2a9 Introduce lint configuration and enable checks
Supress missing translation errors as they are done by volunteers

Signed-off-by: Aayush Gupta <aayushgupta219@gmail.com>
2026-02-20 16:16:50 +08:00
Aayush Gupta
8c016c95d2 Enable resources shrinking
AGP 9.0.0 has introduced additional resource shrinking tasks. Its better
to enable and fix this.

Signed-off-by: Aayush Gupta <aayushgupta219@gmail.com>
2026-02-20 16:16:50 +08:00
Aayush Gupta
5b009453f2 Address non-final resource IDs warnings
Signed-off-by: Aayush Gupta <aayushgupta219@gmail.com>
2026-02-20 16:16:50 +08:00
Hosted Weblate
feb442492b Translated using Weblate (Dutch)
Currently translated at 100.0% (764 of 764 strings)

Translated using Weblate (Dutch)

Currently translated at 57.3% (51 of 89 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (764 of 764 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (764 of 764 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (764 of 764 strings)

Translated using Weblate (Somali)

Currently translated at 72.1% (551 of 764 strings)

Translated using Weblate (Russian)

Currently translated at 100.0% (764 of 764 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 66.2% (59 of 89 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 66.2% (59 of 89 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (764 of 764 strings)

Translated using Weblate (Dutch)

Currently translated at 99.8% (763 of 764 strings)

Translated using Weblate (Latvian)

Currently translated at 97.6% (746 of 764 strings)

Translated using Weblate (Portuguese (Portugal))

Currently translated at 99.8% (763 of 764 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 53.9% (48 of 89 strings)

Translated using Weblate (Spanish)

Currently translated at 99.8% (763 of 764 strings)

Translated using Weblate (Croatian)

Currently translated at 100.0% (764 of 764 strings)

Translated using Weblate (Chinese (Simplified Han script))

Currently translated at 100.0% (89 of 89 strings)

Translated using Weblate (Marathi)

Currently translated at 30.4% (233 of 764 strings)

Translated using Weblate (Portuguese)

Currently translated at 99.8% (763 of 764 strings)

Translated using Weblate (Polish)

Currently translated at 56.1% (50 of 89 strings)

Co-authored-by: 439JBYL80IGQTF25UXNR0X1BG <439JBYL80IGQTF25UXNR0X1BG@users.noreply.hosted.weblate.org>
Co-authored-by: AMOL GHORPADE <amolghorpade4u@gmail.com>
Co-authored-by: Daria Szatan <bla_ke@tuta.io>
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Kairos <private.snazzy673@simplelogin.com>
Co-authored-by: Milo Ivir <mail@milotype.de>
Co-authored-by: Miq Maq <maqdishi@gmail.com>
Co-authored-by: Nicolás Pérez <nicoperez241@proton.me>
Co-authored-by: Philip Goto <philip.goto@gmail.com>
Co-authored-by: delvani <del.cidrak@gmail.com>
Co-authored-by: igoroliveira-letras <igoroliveiradasilvaofc@gmail.com>
Co-authored-by: ojppe <ojppe@users.noreply.hosted.weblate.org>
Co-authored-by: ssantos <ssantos@web.de>
Co-authored-by: ℂ𝕠𝕠𝕠𝕝 (𝕘𝕚𝕥𝕙𝕦𝕓.𝕔𝕠𝕞/ℂ𝕠𝕠𝕠𝕝) <coool@mail.lv>
Co-authored-by: 大王叫我来巡山 <hamburger2048@users.noreply.hosted.weblate.org>
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/nl/
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/pl/
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/pt_BR/
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/zh_Hans/
Translation: NewPipe/Metadata
2026-02-19 09:41:02 +01:00
Tobi
834f136102 Merge pull request #13247 from dustdfg/crashlog_fix
Partially revert:  ErrorActivity: Kotlin-fy buildMarkdown method
2026-02-19 00:37:56 -08:00
Tobi
01e77e2e26 Merge pull request #13256 from pierreeurope/fix/ttml-postprocessing-error-reporting
Fix subtitle post-processing error losing original exception
2026-02-19 00:21:17 -08:00
Tobi
4cfd36ce5b Merge pull request #13203 from dustdfg/video_detail_fragment_click_through
VideoDetailFragment: Forcefully catch click events uncaught by children
2026-02-18 23:45:35 -08:00
Tobi
51c9ed774c Merge pull request #13213 from aivelon/handle-comment-replies-screens
Handling Comment Replies fragments
2026-02-18 23:43:39 -08:00
vt
9f8055f018 remove existing comment replies screens before adding new one 2026-02-19 12:07:22 +05:30
Tobi
239f6c97a0 Merge pull request #13241 from aivelon/comment-replies-without-images
Fix comment replies header layout without avatar image
2026-02-18 03:20:19 -08:00
vt
7e7ad1e410 fix layout of comment replies header to work when avatar image is gone (disabled in content -> image settings) 2026-02-17 13:34:41 +05:30
pierreeurope
95367dd338 Fix subtitle post-processing error losing original exception
Previously, TtmlConverter.process() caught all exceptions during TTML
to SRT conversion and returned opaque error codes (1 for IOException,
8 for other exceptions). These error codes were then wrapped by
Postprocessing.run() into a generic RuntimeException with the message
'post-processing algorithm returned N', losing the original exception
and its stack trace.

This made it impossible for users and developers to diagnose the root
cause of subtitle download failures, as reported in #13206.

Now, IOExceptions are re-thrown directly, and other exceptions are
wrapped in an IOException with the original exception as the cause.
This allows DownloadMission.doPostprocessing() to properly catch and
report the actual error, including the full stack trace in the crash
report.

Fixes #13206

Signed-off-by: pierreeurope <pierre.europe@pm.me>
2026-02-16 12:46:30 +01:00
Yevhen Babiichuk (DustDFG)
70cdaf5550 Partially revert: ErrorActivity: Kotlin-fy buildMarkdown method
Partially reverts c3dbed54e5
Fix the bug where collapse container for several crashlogs is created
when only one crash log present
2026-02-13 13:43:54 +02:00
Aayush Gupta
3815f5f593 Translated using Weblate (Bengali (India))
Currently translated at 39.2% (300 of 764 strings)
2026-02-12 17:25:15 +01:00
Hosted Weblate
6c69a5409b Translated using Weblate (Slovak)
Currently translated at 77.5% (69 of 89 strings)

Translated using Weblate (Basque)

Currently translated at 100.0% (764 of 764 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (764 of 764 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 49.4% (44 of 89 strings)

Translated using Weblate (Icelandic)

Currently translated at 99.2% (758 of 764 strings)

Translated using Weblate (Czech)

Currently translated at 100.0% (89 of 89 strings)

Translated using Weblate (French)

Currently translated at 100.0% (89 of 89 strings)

Translated using Weblate (Chinese (Simplified Han script))

Currently translated at 100.0% (89 of 89 strings)

Translated using Weblate (German)

Currently translated at 100.0% (89 of 89 strings)

Added translation using Weblate (Gaelic)

Translated using Weblate (French)

Currently translated at 77.2% (68 of 88 strings)

Translated using Weblate (Turkish)

Currently translated at 34.0% (30 of 88 strings)

Co-authored-by: Erenay <erenaydev@proton.me>
Co-authored-by: Fjuro <fjuro@alius.cz>
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Mickaël Binos <mickaelbinos@outlook.com>
Co-authored-by: Milan <mobrcian@hotmail.com>
Co-authored-by: Nico <n2778370@gmail.com>
Co-authored-by: S.B. MacQueen <macqueen1729@protonmail.com>
Co-authored-by: Sveinn í Felli <sv1@fellsnet.is>
Co-authored-by: Thadah D. Denyse <thadahdenyse@protonmail.com>
Co-authored-by: VfBFan <drop0815@posteo.de>
Co-authored-by: delvani <del.cidrak@gmail.com>
Co-authored-by: 大王叫我来巡山 <hamburger2048@users.noreply.hosted.weblate.org>
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/cs/
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/de/
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/fr/
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/pt_BR/
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/sk/
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/tr/
Translate-URL: https://hosted.weblate.org/projects/newpipe/metadata/zh_Hans/
Translation: NewPipe/Metadata
2026-02-12 17:25:15 +01:00
Stypox
840664a8d7 Merge pull request #13236 from dustdfg/error_activity_newlines 2026-02-12 13:15:54 +01:00
Yevhen Babiichuk (DustDFG)
54090ca7b6 ErrorActivity use normal \n instead of \\n in resource strings 2026-02-11 16:54:35 +02:00
Aayush Gupta
8d45b6b8c9 Merge pull request #13225 from dustdfg/error_activity_kotlin
Convert ErrorActivity to kotlin
2026-02-11 21:50:37 +08:00
Aayush Gupta
c3dbed54e5 ErrorActivity: Kotlin-fy buildMarkdown method
Signed-off-by: Aayush Gupta <aayushgupta219@gmail.com>
2026-02-11 21:40:02 +08:00
Aayush Gupta
8968aab578 ErrorActivity: Catch exceptions not throwables
Signed-off-by: Aayush Gupta <aayushgupta219@gmail.com>
2026-02-11 21:40:02 +08:00
Aayush Gupta
d7a4435e94 ErrorActivity: Use better variable names and encapsulation
Signed-off-by: Aayush Gupta <aayushgupta219@gmail.com>
2026-02-11 21:39:56 +08:00
Yevhen Babiichuk (DustDFG)
4a7eaed3a7 ErrorActivity convert to kotlin
Co-authored-by: Aayush Gupta <aayushgupta219@protonmail.com>
2026-02-11 18:31:12 +08:00
Yevhen Babiichuk (DustDFG)
869a3cea9b ErrorActivity small refactor 2026-02-11 18:31:12 +08:00
Yevhen Babiichuk (DustDFG)
3a0a3a42af Misc loop/stream based player refactors 2026-02-10 11:26:23 +02:00
Yevhen Babiichuk (DustDFG)
59e5018c2d player/helper/PlayerHelper#getTimeString replace ints with longs
Duration data in the player code incosnistently typed. Half code
uses ints and half uses longs. Recieve longs in this function to
allow both halfs of player code just use the function without
nasty long to int downcasting warnings/errors in code
2026-02-10 11:24:25 +02:00
Aayush Gupta
e6e0be772a Merge pull request #13026 from dustdfg/kotlin_merged2
Convert a bunch of files to kotlin
2026-02-10 16:40:51 +08:00
Aayush Gupta
224a5d0cb9 Minor improvements
- Use early return in case of nulls
- Use better variable names
- Remove non-required newlines, imports and add missing ones

Signed-off-by: Aayush Gupta <aayushgupta219@gmail.com>
2026-02-10 16:33:43 +08:00
Yevhen Babiichuk (DustDFG)
c6fc94e7bd Convert newpipe/settings/export/PreferencesObjectInputStream to kotlin 2026-02-10 10:09:18 +02:00
Yevhen Babiichuk (DustDFG)
1eedfd7eee Convert /newpipe/util/StreamTypeUtil to kotlin 2026-02-10 10:09:18 +02:00
Yevhen Babiichuk (DustDFG)
2c7654a579 Covert newpipe/util/DependentPreferenceHelper to kotlin 2026-02-10 10:09:18 +02:00
Yevhen Babiichuk (DustDFG)
09a746dd6a Convert newpipe/util/ServiceHelper to kotlin 2026-02-10 10:09:18 +02:00
Yevhen Babiichuk (DustDFG)
d665a4f016 Convert newpipe/util/NewPipeTextViewHelper to kotlin 2026-02-10 10:09:18 +02:00
Yevhen Babiichuk (DustDFG)
6cf932b2a7 Convert newpipe/ExitActivity to kotlin 2026-02-10 10:09:18 +02:00
Yevhen Babiichuk (DustDFG)
48467669b6 Convert newpipe/util/PeertubeHelper to kotlin 2026-02-10 09:51:14 +02:00
Yevhen Babiichuk (DustDFG)
780e6a4848 Convert newpipe/util/PlayButtonHelper to kotlin 2026-02-10 09:51:13 +02:00
Aayush Gupta
13186c0b15 Merge pull request #13224 from dustdfg/kotlin_idiomatic
Kotlin misc idiomatic refactor
2026-02-10 15:43:15 +08:00
Yevhen Babiichuk (DustDFG)
edfdbe805f Uitilize kotlin elvis operator 2026-02-10 09:23:17 +02:00
Yevhen Babiichuk (DustDFG)
451409fc3b SharedPreferences.edit applies changes automatically 2026-02-10 09:23:17 +02:00
Yevhen Babiichuk (DustDFG)
289d22eed7 Utilize kotlins ifEmpty 2026-02-10 09:23:17 +02:00
Yevhen Babiichuk (DustDFG)
21f446a78e Refactor settings/preferencesearch/PreferenceSearchItem#allRelevantSearchFields
It doesn't need to return mutable list
2026-02-10 09:23:17 +02:00
Stypox
6214ae33f3 Merge pull request #13219 from dustdfg/kotlin_check_is_not_if
Correct inverted check (error fix on dev branch)
2026-02-09 14:51:06 +01:00
Yevhen Babiichuk (DustDFG)
37cef825a2 Correct inverted check
If performs action when value is true but check when false
Fix for d6be966db3
2026-02-09 14:31:56 +02:00
Aayush Gupta
dab8e056e9 Merge pull request #13137 from dustdfg/info_item_builder_dead
Remove dead code from info_list/InfoItemBuilder
2026-02-08 22:28:19 +08:00
Aayush Gupta
020dbdc82a Merge pull request #13131 from dustdfg/tabs_json_helper_refactor
TabsJsonHelper refactor
2026-02-08 22:06:25 +08:00
Aayush Gupta
5d7934249f Merge pull request #13028 from dustdfg/idiomatic_kotlin_exceptions
Replace Illegal{State,Argument} exceptions with more idiomatic kotlin code
2026-02-08 22:05:46 +08:00
Yevhen Babiichuk (DustDFG)
d6be966db3 Replace Illegal{State,Argument} exceptions with more idiomatic kotlin code 2026-02-08 21:59:10 +08:00
tobigr
e358867da8 Use dedicated constants for unknown image dimensions in ImageStrategy 2026-02-07 15:34:48 +01:00
TobiGr
e6daf45c83 [DescriptionFragment] Fix thumbnail size: width x height
height x width was used before which is an uncommon order.
2026-02-07 12:36:09 +01:00
Yevhen Babiichuk (DustDFG)
5e1a1989be VideoDetailFragment: Forcefully catch click events uncaught by children
Because otherwise they will be caught by underlying view and
"click through" will happen. Workaround for #5600
2026-02-06 19:03:01 +02:00
Tobi
56a043669a Merge pull request #13161 from jpds/outdated-subscription-shuffle
FeedLoadManager: Shuffle the order outdated subscriptions are updated in
2026-02-06 02:13:56 -08:00
tobigr
85abc58158 Merge branch 'master' into dev 2026-02-05 23:01:30 +01:00
Hosted Weblate
955844b3e1 Translated using Weblate (Basque)
Currently translated at 99.7% (762 of 764 strings)

Translated using Weblate (Latvian)

Currently translated at 97.6% (746 of 764 strings)

Translated using Weblate (Swedish)

Currently translated at 100.0% (764 of 764 strings)

Translated using Weblate (Latvian)

Currently translated at 97.5% (745 of 764 strings)

Translated using Weblate (Kabyle)

Currently translated at 27.7% (212 of 764 strings)

Translated using Weblate (German)

Currently translated at 100.0% (764 of 764 strings)

Translated using Weblate (Korean)

Currently translated at 100.0% (764 of 764 strings)

Translated using Weblate (Slovak)

Currently translated at 100.0% (764 of 764 strings)

Translated using Weblate (Vietnamese)

Currently translated at 99.3% (759 of 764 strings)

Translated using Weblate (Latvian)

Currently translated at 97.5% (745 of 764 strings)

Translated using Weblate (French)

Currently translated at 100.0% (764 of 764 strings)

Translated using Weblate (Hebrew)

Currently translated at 100.0% (764 of 764 strings)

Translated using Weblate (Azerbaijani)

Currently translated at 100.0% (764 of 764 strings)

Translated using Weblate (Greek)

Currently translated at 100.0% (764 of 764 strings)

Translated using Weblate (Icelandic)

Currently translated at 98.4% (752 of 764 strings)

Translated using Weblate (Malay)

Currently translated at 58.7% (449 of 764 strings)

Translated using Weblate (Turkish)

Currently translated at 100.0% (764 of 764 strings)

Translated using Weblate (Bulgarian)

Currently translated at 100.0% (764 of 764 strings)

Translated using Weblate (Bulgarian)

Currently translated at 99.8% (763 of 764 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (764 of 764 strings)

Translated using Weblate (Estonian)

Currently translated at 100.0% (764 of 764 strings)

Translated using Weblate (Czech)

Currently translated at 100.0% (764 of 764 strings)

Translated using Weblate (French)

Currently translated at 100.0% (764 of 764 strings)

Translated using Weblate (Polish)

Currently translated at 100.0% (764 of 764 strings)

Translated using Weblate (Hungarian)

Currently translated at 100.0% (764 of 764 strings)

Translated using Weblate (Chinese (Simplified Han script))

Currently translated at 100.0% (764 of 764 strings)

Translated using Weblate (Chinese (Traditional Han script))

Currently translated at 100.0% (764 of 764 strings)

Merge branch 'origin/dev' into Weblate.

Translated using Weblate (Somali)

Currently translated at 71.5% (547 of 764 strings)

Translated using Weblate (Somali)

Currently translated at 71.5% (547 of 764 strings)

Translated using Weblate (Danish)

Currently translated at 98.5% (753 of 764 strings)

Translated using Weblate (Danish)

Currently translated at 98.5% (753 of 764 strings)

Translated using Weblate (Kurdish (Central))

Currently translated at 80.4% (615 of 764 strings)

Translated using Weblate (Dutch (Belgium))

Currently translated at 73.5% (562 of 764 strings)

Translated using Weblate (Dutch (Belgium))

Currently translated at 73.5% (562 of 764 strings)

Translated using Weblate (Hungarian)

Currently translated at 99.7% (762 of 764 strings)

Translated using Weblate (Hungarian)

Currently translated at 99.7% (762 of 764 strings)

Translated using Weblate (Georgian)

Currently translated at 92.5% (707 of 764 strings)

Translated using Weblate (Kurdish)

Currently translated at 60.9% (466 of 764 strings)

Translated using Weblate (Catalan)

Currently translated at 96.0% (734 of 764 strings)

Translated using Weblate (Catalan)

Currently translated at 96.0% (734 of 764 strings)

Translated using Weblate (Greek)

Currently translated at 99.7% (762 of 764 strings)

Translated using Weblate (French)

Currently translated at 99.7% (762 of 764 strings)

Translated using Weblate (French)

Currently translated at 99.7% (762 of 764 strings)

Translated using Weblate (Japanese)

Currently translated at 95.5% (730 of 764 strings)

Translated using Weblate (Belarusian)

Currently translated at 98.8% (755 of 764 strings)

Translated using Weblate (Vietnamese)

Currently translated at 99.3% (759 of 764 strings)

Translated using Weblate (Odia)

Currently translated at 95.2% (728 of 764 strings)

Translated using Weblate (Russian)

Currently translated at 99.7% (762 of 764 strings)

Translated using Weblate (Russian)

Currently translated at 99.7% (762 of 764 strings)

Translated using Weblate (Korean)

Currently translated at 98.4% (752 of 764 strings)

Translated using Weblate (Korean)

Currently translated at 98.4% (752 of 764 strings)

Translated using Weblate (Interlingua)

Currently translated at 31.1% (238 of 764 strings)

Translated using Weblate (Interlingua)

Currently translated at 31.1% (238 of 764 strings)

Translated using Weblate (Indonesian)

Currently translated at 99.7% (762 of 764 strings)

Translated using Weblate (Indonesian)

Currently translated at 99.7% (762 of 764 strings)

Translated using Weblate (Tamazight (Central Atlas))

Currently translated at 18.5% (142 of 764 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 99.7% (762 of 764 strings)

Translated using Weblate (Turkish)

Currently translated at 99.7% (762 of 764 strings)

Translated using Weblate (Turkish)

Currently translated at 99.7% (762 of 764 strings)

Translated using Weblate (Portuguese (Portugal))

Currently translated at 99.7% (762 of 764 strings)

Translated using Weblate (Portuguese (Portugal))

Currently translated at 99.7% (762 of 764 strings)

Translated using Weblate (Arabic (Libya))

Currently translated at 93.4% (714 of 764 strings)

Translated using Weblate (Slovak)

Currently translated at 99.7% (762 of 764 strings)

Translated using Weblate (Azerbaijani)

Currently translated at 99.7% (762 of 764 strings)

Translated using Weblate (Lithuanian)

Currently translated at 98.9% (756 of 764 strings)

Translated using Weblate (Lithuanian)

Currently translated at 98.9% (756 of 764 strings)

Translated using Weblate (Portuguese)

Currently translated at 99.7% (762 of 764 strings)

Translated using Weblate (Portuguese)

Currently translated at 99.7% (762 of 764 strings)

Translated using Weblate (Hindi)

Currently translated at 99.7% (762 of 764 strings)

Translated using Weblate (Hindi)

Currently translated at 99.7% (762 of 764 strings)

Translated using Weblate (Czech)

Currently translated at 99.7% (762 of 764 strings)

Translated using Weblate (Asturian)

Currently translated at 60.6% (463 of 764 strings)

Translated using Weblate (Asturian)

Currently translated at 60.6% (463 of 764 strings)

Translated using Weblate (Hebrew)

Currently translated at 99.7% (762 of 764 strings)

Translated using Weblate (Hebrew)

Currently translated at 99.7% (762 of 764 strings)

Translated using Weblate (Sardinian)

Currently translated at 97.9% (748 of 764 strings)

Translated using Weblate (Sardinian)

Currently translated at 97.9% (748 of 764 strings)

Translated using Weblate (Chinese (Traditional Han script, Hong Kong))

Currently translated at 95.1% (727 of 764 strings)

Translated using Weblate (Albanian)

Currently translated at 76.3% (583 of 764 strings)

Translated using Weblate (Albanian)

Currently translated at 76.3% (583 of 764 strings)

Translated using Weblate (Nepali)

Currently translated at 56.2% (430 of 764 strings)

Translated using Weblate (Nepali)

Currently translated at 56.2% (430 of 764 strings)

Translated using Weblate (Finnish)

Currently translated at 94.2% (720 of 764 strings)

Translated using Weblate (Finnish)

Currently translated at 94.2% (720 of 764 strings)

Translated using Weblate (Uzbek (Latin script))

Currently translated at 59.2% (453 of 764 strings)

Translated using Weblate (Uzbek (Latin script))

Currently translated at 59.2% (453 of 764 strings)

Translated using Weblate (Punjabi)

Currently translated at 99.7% (762 of 764 strings)

Translated using Weblate (Kurdish (Northern))

Currently translated at 62.5% (478 of 764 strings)

Translated using Weblate (Kurdish (Northern))

Currently translated at 62.5% (478 of 764 strings)

Translated using Weblate (Icelandic)

Currently translated at 98.1% (750 of 764 strings)

Translated using Weblate (N’Ko)

Currently translated at 85.7% (655 of 764 strings)

Translated using Weblate (Croatian)

Currently translated at 99.7% (762 of 764 strings)

Translated using Weblate (Serbian)

Currently translated at 99.7% (762 of 764 strings)

Translated using Weblate (Serbian)

Currently translated at 99.7% (762 of 764 strings)

Translated using Weblate (Spanish)

Currently translated at 99.7% (762 of 764 strings)

Translated using Weblate (Spanish)

Currently translated at 99.7% (762 of 764 strings)

Translated using Weblate (Ukrainian)

Currently translated at 99.7% (762 of 764 strings)

Translated using Weblate (Ukrainian)

Currently translated at 99.7% (762 of 764 strings)

Translated using Weblate (Bengali)

Currently translated at 74.2% (567 of 764 strings)

Translated using Weblate (German)

Currently translated at 99.7% (762 of 764 strings)

Translated using Weblate (German)

Currently translated at 99.7% (762 of 764 strings)

Translated using Weblate (Romanian)

Currently translated at 97.6% (746 of 764 strings)

Translated using Weblate (Romanian)

Currently translated at 97.6% (746 of 764 strings)

Translated using Weblate (Chinese (Simplified Han script))

Currently translated at 99.7% (762 of 764 strings)

Translated using Weblate (ryu (generated) (ryu))

Currently translated at 95.2% (728 of 764 strings)

Translated using Weblate (Arabic)

Currently translated at 99.7% (762 of 764 strings)

Translated using Weblate (Galician)

Currently translated at 95.2% (728 of 764 strings)

Translated using Weblate (Galician)

Currently translated at 95.2% (728 of 764 strings)

Translated using Weblate (Italian)

Currently translated at 99.7% (762 of 764 strings)

Translated using Weblate (Italian)

Currently translated at 99.7% (762 of 764 strings)

Translated using Weblate (Dutch)

Currently translated at 99.7% (762 of 764 strings)

Translated using Weblate (Dutch)

Currently translated at 99.7% (762 of 764 strings)

Translated using Weblate (Estonian)

Currently translated at 99.7% (762 of 764 strings)

Translated using Weblate (Estonian)

Currently translated at 99.7% (762 of 764 strings)

Translated using Weblate (Bulgarian)

Currently translated at 99.7% (762 of 764 strings)

Translated using Weblate (Bulgarian)

Currently translated at 99.7% (762 of 764 strings)

Translated using Weblate (Malayalam)

Currently translated at 73.0% (558 of 764 strings)

Translated using Weblate (Malayalam)

Currently translated at 73.0% (558 of 764 strings)

Translated using Weblate (Swedish)

Currently translated at 99.7% (762 of 764 strings)

Translated using Weblate (Swedish)

Currently translated at 99.7% (762 of 764 strings)

Translated using Weblate (Persian)

Currently translated at 95.6% (731 of 764 strings)

Translated using Weblate (Norwegian Bokmål)

Currently translated at 89.9% (687 of 764 strings)

Translated using Weblate (Norwegian Bokmål)

Currently translated at 89.9% (687 of 764 strings)

Translated using Weblate (Polish)

Currently translated at 99.7% (762 of 764 strings)

Co-authored-by: 2-Seol <2Seol.0117@gmail.com>
Co-authored-by: Agnieszka C <aga_04@o2.pl>
Co-authored-by: Ajeje Brazorf <lmelonimamo@yahoo.it>
Co-authored-by: Alex25820 <alexs25820@gmail.com>
Co-authored-by: Allan Nordhøy <epost@anotheragency.no>
Co-authored-by: Andreas Westrell <andreas.westrell@gmail.com>
Co-authored-by: Anonymous <noreply@weblate.org>
Co-authored-by: Anxhelo Lushka <github@lushka.al>
Co-authored-by: AudricV <AudricV@users.noreply.hosted.weblate.org>
Co-authored-by: Aurelian Ciocîltan <aurelianciociltan@gmail.com>
Co-authored-by: Bakary Kaba <mbkaba@live.fr>
Co-authored-by: Balázs Meskó <meskobalazs@mailbox.org>
Co-authored-by: ButterflyOfFire <boffire@users.noreply.hosted.weblate.org>
Co-authored-by: Comrade KVRONV <naruto.tkntcube31@gmail.com>
Co-authored-by: D D <keptawesome@gmail.com>
Co-authored-by: David Rebolo Magariños <davidre345@hotmail.com>
Co-authored-by: Deleted User <jsdelvani@users.noreply.hosted.weblate.org>
Co-authored-by: Deleted User <noreply+10474@weblate.org>
Co-authored-by: Deleted User <noreply+19964@weblate.org>
Co-authored-by: Dormin <nkzo3d+1uozh1scczh8c@sharklasers.com>
Co-authored-by: Emin Tufan Çetin <etcetin@gmail.com>
Co-authored-by: Enol P. <enolp@softastur.org>
Co-authored-by: Erenay <erenaydev@proton.me>
Co-authored-by: Femini <nizamismidov4@gmail.com>
Co-authored-by: Fjuro <fjuro@alius.cz>
Co-authored-by: Francesco Saltori <francescosaltori@gmail.com>
Co-authored-by: GET100PERCENT <eraofphysics@yahoo.com>
Co-authored-by: Ghost of Sparta <makesocialfoss32@keemail.me>
Co-authored-by: Guillem <guillemglez@gmail.com>
Co-authored-by: Hakim Oubouali <hakim.oubouali.skr@gmail.com>
Co-authored-by: Hoseok Seo <ddinghoya@gmail.com>
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Igor Nedoboy <i.nedoboy@mail.ru>
Co-authored-by: Ishwor Ghimire <ghimire.esor09@gmail.com>
Co-authored-by: Jeff Huang <s8321414@gmail.com>
Co-authored-by: Kiss Attila <gaxeco4855@pro5g.com>
Co-authored-by: Ldm Public <ldmpub@gmail.com>
Co-authored-by: MS-PC <MSPCtranslator@gmail.com>
Co-authored-by: Marian Hanzel <marulinko@gmail.com>
Co-authored-by: Mathias Hamza Vedsted-Mirza <mathiashamzamirza@outlook.com>
Co-authored-by: Mickaël Binos <mickaelbinos@outlook.com>
Co-authored-by: Milan <mobrcian@hotmail.com>
Co-authored-by: Milo Ivir <mail@milotype.de>
Co-authored-by: Mukhamadjonov <abdukodir.9507@gmail.com>
Co-authored-by: NEXI <nexiphotographer@gmail.com>
Co-authored-by: Nadir Nour <dudethatwascool2@gmail.com>
Co-authored-by: Nikoloz <nukushatugushi@gmail.com>
Co-authored-by: Olivia Ng <uloo592@gmail.com>
Co-authored-by: Oymate <dhruboadittya96@gmail.com>
Co-authored-by: Philip Goto <philip.goto@gmail.com>
Co-authored-by: Priit Jõerüüt <jrthwlate@users.noreply.hosted.weblate.org>
Co-authored-by: Random <random-r@users.noreply.hosted.weblate.org>
Co-authored-by: SecularSteve <fairfull.playing@gmail.com>
Co-authored-by: ShareASmile <ShareASmile@users.noreply.hosted.weblate.org>
Co-authored-by: Sveinn í Felli <sv1@fellsnet.is>
Co-authored-by: Sérgio Marques <smarquespt@gmail.com>
Co-authored-by: TXRdev Archive <lckphanaf9999@gmail.com>
Co-authored-by: Thadah D. Denyse <thadahdenyse@protonmail.com>
Co-authored-by: Theophine Savio Theodore <theophinetheodore@gmail.com>
Co-authored-by: Trunars <trunars@abv.bg>
Co-authored-by: Vasilis K. <skyhirules@gmail.com>
Co-authored-by: VfBFan <drop0815@posteo.de>
Co-authored-by: Ville Rantanen <v.r@iki.fi>
Co-authored-by: Yaron Shahrabani <sh.yaron@gmail.com>
Co-authored-by: cat <catsnote@proton.me>
Co-authored-by: cehnemdark <cehennem1001@gmail.com>
Co-authored-by: gymka <gymka@archlinux.lt>
Co-authored-by: justcontributor <kty5663@gmail.com>
Co-authored-by: nautilusx <translate@disroot.org>
Co-authored-by: ngocanhtve <ngocanh.tve@gmail.com>
Co-authored-by: winqooq <winqooq@gmail.com>
Co-authored-by: zmni <zmni@outlook.com>
Co-authored-by: Макар Разин <makarrazin14@gmail.com>
Co-authored-by: Слободан Симић(Slobodan Simić) <slsimic@gmail.com>
Co-authored-by: ℂ𝕠𝕠𝕠𝕝 (𝕘𝕚𝕥𝕙𝕦𝕓.𝕔𝕠𝕞/ℂ𝕠𝕠𝕠𝕝) <coool@mail.lv>
Co-authored-by: 大王叫我来巡山 <hamburger2048@users.noreply.hosted.weblate.org>
2026-02-05 17:41:09 +01:00
Tobi
e74907561e Merge pull request #13195 from absurdlylongusername/fix-13139-resume-playback
Fix 13139 resume playback
2026-02-05 06:49:57 -08:00
AbsurdlyLongUsername
1554f77762 Fix additional setRecovery from rebase errors 2026-02-05 11:17:30 +00:00
AbsurdlyLongUsername
118def08b4 Add conditional guard to prevent useVideoAndSubtitles overwriting recovery position that was set in Player.handleIntent for RESUME_PLAYBACK when resuming playback 2026-02-05 05:05:41 +00:00
AbsurdlyLongUsername
725cb70cbd Update useVideoAndSubtitles rename in comment 2026-02-05 05:05:40 +00:00
AbsurdlyLongUsername
5525d206dc Small refactor getPlayQueueFromCache 2026-02-05 05:05:40 +00:00
Stypox
83f9646eec Merge pull request #13190 from TeamNewPipe/agp9fixes
Partially revert upgrade to AGP 9.0.0
2026-02-04 19:47:43 +01:00
Aayush Gupta
85d43fe45e proguard: Keep fields of generated proguard files
Inspired from https://github.com/protocolbuffers/protobuf/blob/main/java/lite.md#r8-rule-to-make-production-app-builds-work

Signed-off-by: Aayush Gupta <aayushgupta219@gmail.com>
2026-02-04 16:22:48 +08:00
Aayush Gupta
8d6e68d6f4 Partially revert upgrade to AGP 9.0.0
Building is broken on ecrypted filesystems

Signed-off-by: Aayush Gupta <aayushgupta219@gmail.com>
2026-02-04 15:56:44 +08:00
Yevhen Babiichuk (DustDFG)
07fe1e758a Refactor settings/tabs/TabsJsonHelper.java to use java streams 2026-02-02 23:59:01 +02:00
Aayush Gupta
15b5cef6c2 Merge pull request #13136 from TeamNewPipe/agp9
Upgrade Android Gradle Plugin to 9.0.0
2026-02-01 17:34:47 +08:00
Jonathan Davies
ae60f7d7eb FeedLoadManager: Shuffle the order outdated subscriptions are updated in 2026-01-31 20:47:48 +00:00
Tobi
739b6ae57b Merge pull request #13141 from salmanmkc/upgrade-github-actions-node24-general
Upgrade GitHub Actions to latest versions
2026-01-30 02:31:40 -08:00
Tobi
cc33b685a5 Merge pull request #13140 from salmanmkc/upgrade-github-actions-node24
Upgrade GitHub Actions for Node 24 compatibility
2026-01-30 02:31:29 -08:00
Salman Muin Kayser Chishti
d051e8ecc8 Upgrade GitHub Actions to latest versions
Signed-off-by: Salman Muin Kayser Chishti <13schishti@gmail.com>
2026-01-30 09:16:42 +00:00
Salman Muin Kayser Chishti
51e62f09ba Upgrade GitHub Actions for Node 24 compatibility
Signed-off-by: Salman Muin Kayser Chishti <13schishti@gmail.com>
2026-01-30 09:16:36 +00:00
Yevhen Babiichuk (DustDFG)
8a2c47bc12 Remove dead code from info_list/InfoItemBuilder
It no longer really builds any view and used only for stroing click
gesture callbacks. In the same way lik local/LocalItemBuilder does

Last usage of build functions: 2e9a860aaa
2026-01-29 15:01:40 +02:00
Aayush Gupta
a7aad63bbb Upgrade Kotlin and KSP
Fixes multiple build errors. Once parcelize is fixed, we should be good
to use built-in Kotlin completely

Ref: https://issuetracker.google.com/issues/478401081

Signed-off-by: Aayush Gupta <aayushgupta219@gmail.com>
2026-01-29 14:59:25 +08:00
Aayush Gupta
fd192b4f3f Drop default properties
Signed-off-by: Aayush Gupta <aayushgupta219@gmail.com>
2026-01-29 14:00:53 +08:00
Aayush Gupta
19e94bd30c Migrate from deprecated srcDir to directories method
Signed-off-by: Aayush Gupta <aayushgupta219@gmail.com>
2026-01-29 14:00:53 +08:00
Aayush Gupta
7758a27694 Migrate from deprecated android block to ApplicationExtension
Signed-off-by: Aayush Gupta <aayushgupta219@gmail.com>
2026-01-29 14:00:53 +08:00
Aayush Gupta
a3301dcfb1 Enable resValues as build feature
Signed-off-by: Aayush Gupta <aayushgupta219@gmail.com>
2026-01-29 14:00:53 +08:00
Aayush Gupta
d045b27cea Migrate to built-in Kotlin
Ref: https://developer.android.com/build/migrate-to-built-in-kotlin

Signed-off-by: Aayush Gupta <aayushgupta219@gmail.com>
2026-01-29 14:00:51 +08:00
Aayush Gupta
4f70235ee8 Enable proguard android optimizations
AGP 9.0+ requires enabling optimizations

Signed-off-by: Aayush Gupta <aayushgupta219@gmail.com>
2026-01-29 14:00:11 +08:00
Aayush Gupta
54f9bcb03e Upgrade AGP to 9.0.0
Signed-off-by: Aayush Gupta <aayushgupta219@gmail.com>
2026-01-29 14:00:11 +08:00
Yevhen Babiichuk (DustDFG)
6ddd4a7e63 VideoDetailFragment: hide relatedItemsLayout in tablet mode after fullscreen
Fixes: #7617
2026-01-11 18:04:26 +02:00
414 changed files with 5617 additions and 5573 deletions

View File

@@ -22,7 +22,7 @@ jobs:
github.event.comment.author_association == 'MEMBER'
)
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
- name: Get backport metadata
# the target branch is the first argument after `/backport`
env:

View File

@@ -38,7 +38,7 @@ jobs:
steps:
- uses: actions/checkout@v6
- uses: gradle/actions/wrapper-validation@v4
- uses: gradle/actions/wrapper-validation@v5
- name: create and checkout branch
# push events already checked out the branch

View File

@@ -3,12 +3,15 @@
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import com.android.build.api.dsl.ApplicationExtension
plugins {
alias(libs.plugins.android.application)
alias(libs.plugins.jetbrains.kotlin.android)
alias(libs.plugins.jetbrains.kotlin.kapt)
alias(libs.plugins.google.ksp)
alias(libs.plugins.jetbrains.kotlin.parcelize)
alias(libs.plugins.jetbrains.kotlinx.serialization)
alias(libs.plugins.sonarqube)
checkstyle
}
@@ -32,7 +35,7 @@ kotlin {
}
}
android {
configure<ApplicationExtension> {
compileSdk = 36
namespace = "org.schabi.newpipe"
@@ -42,9 +45,9 @@ android {
minSdk = 21
targetSdk = 35
versionCode = System.getProperty("versionCodeOverride")?.toInt() ?: 1008
versionCode = System.getProperty("versionCodeOverride")?.toInt() ?: 1009
versionName = "0.28.3"
versionName = "0.28.4"
System.getProperty("versionNameSuffix")?.let { versionNameSuffix = it }
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
@@ -77,19 +80,18 @@ android {
resValue("string", "app_name", "NewPipe $suffix")
}
isMinifyEnabled = true
isShrinkResources = false // disabled to fix F-Droid"s reproducible build
proguardFiles(getDefaultProguardFile("proguard-android.txt"), "proguard-rules.pro")
isShrinkResources = true
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro"
)
}
}
lint {
checkReleaseBuilds = false
// Or, if you prefer, you can continue to check for errors in release builds,
// but continue the build even when errors are found:
lintConfig = file("lint.xml")
// Continue the debug build even when errors are found
abortOnError = false
// suppress false warning ("Resource IDs will be non-final in Android Gradle Plugin version
// 5.0, avoid using them in switch case statements"), which affects only library projects
disable += "NonConstantResourceId"
}
compileOptions {
@@ -100,7 +102,7 @@ android {
sourceSets {
getByName("androidTest") {
assets.srcDir("$projectDir/schemas")
assets.directories += "$projectDir/schemas"
}
}
@@ -111,6 +113,7 @@ android {
buildFeatures {
viewBinding = true
buildConfig = true
resValues = true
}
packaging {
@@ -244,6 +247,12 @@ dependencies {
implementation(libs.google.android.material)
implementation(libs.androidx.webkit)
// Coroutines interop
implementation(libs.kotlinx.coroutines.rx3)
// Kotlinx Serialization
implementation(libs.kotlinx.serialization.json)
/** Third-party libraries **/
implementation(libs.livefront.bridge)
implementation(libs.evernote.statesaver.core)
@@ -270,7 +279,8 @@ dependencies {
implementation(libs.lisawray.groupie.viewbinding)
// Image loading
implementation(libs.squareup.picasso)
implementation(libs.coil.compose)
implementation(libs.coil.network.okhttp)
// Markdown library for Android
implementation(libs.noties.markwon.core)

10
app/lint.xml Normal file
View File

@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
~ SPDX-FileCopyrightText: 2026 NewPipe e.V. <https://newpipe-ev.de>
~ SPDX-License-Identifier: GPL-3.0-or-later
-->
<lint>
<issue id="MissingTranslation" severity="ignore" />
<issue id="MissingQuantity" severity="ignore" />
<issue id="ImpliedQuantity" severity="ignore" />
</lint>

View File

@@ -39,3 +39,23 @@
## For some reason NotificationModeConfigFragment wasn't kept (only referenced in a preference xml)
-keep class org.schabi.newpipe.settings.notifications.** { *; }
# Prevent R8 from stripping or renaming Protobuf internal fields
-keepclassmembers class * extends com.google.protobuf.GeneratedMessageLite {
<fields>;
}
## Keep Kotlinx Serialization classes
-keepclassmembers class kotlinx.serialization.json.** {
*** Companion;
}
-keepclasseswithmembers class kotlinx.serialization.json.** {
kotlinx.serialization.KSerializer serializer(...);
}
-keep,includedescriptorclasses class org.schabi.newpipe.**$$serializer { *; }
-keepclassmembers class org.schabi.newpipe.** {
*** Companion;
}
-keepclasseswithmembers class org.schabi.newpipe.** {
kotlinx.serialization.KSerializer serializer(...);
}

View File

@@ -96,14 +96,6 @@
android:exported="false"
android:label="@string/title_activity_about" />
<service
android:name=".local.subscription.services.SubscriptionsImportService"
android:foregroundServiceType="dataSync" />
<service
android:name=".local.subscription.services.SubscriptionsExportService"
android:foregroundServiceType="dataSync" />
<service
android:name=".local.feed.service.FeedLoadService"
android:foregroundServiceType="dataSync" />

View File

@@ -1,285 +0,0 @@
package org.schabi.newpipe;
import android.app.Application;
import android.content.Context;
import android.content.SharedPreferences;
import android.util.Log;
import androidx.annotation.NonNull;
import androidx.core.app.NotificationChannelCompat;
import androidx.core.app.NotificationManagerCompat;
import androidx.preference.PreferenceManager;
import com.jakewharton.processphoenix.ProcessPhoenix;
import org.acra.ACRA;
import org.acra.config.CoreConfigurationBuilder;
import org.schabi.newpipe.error.ReCaptchaActivity;
import org.schabi.newpipe.extractor.NewPipe;
import org.schabi.newpipe.extractor.downloader.Downloader;
import org.schabi.newpipe.extractor.services.youtube.extractors.YoutubeStreamExtractor;
import org.schabi.newpipe.ktx.ExceptionUtils;
import org.schabi.newpipe.settings.NewPipeSettings;
import org.schabi.newpipe.util.BridgeStateSaverInitializer;
import org.schabi.newpipe.util.Localization;
import org.schabi.newpipe.util.ServiceHelper;
import org.schabi.newpipe.util.StateSaver;
import org.schabi.newpipe.util.image.ImageStrategy;
import org.schabi.newpipe.util.image.PicassoHelper;
import org.schabi.newpipe.util.image.PreferredImageQuality;
import org.schabi.newpipe.util.potoken.PoTokenProviderImpl;
import java.io.IOException;
import java.io.InterruptedIOException;
import java.net.SocketException;
import java.util.List;
import java.util.Objects;
import io.reactivex.rxjava3.exceptions.CompositeException;
import io.reactivex.rxjava3.exceptions.MissingBackpressureException;
import io.reactivex.rxjava3.exceptions.OnErrorNotImplementedException;
import io.reactivex.rxjava3.exceptions.UndeliverableException;
import io.reactivex.rxjava3.functions.Consumer;
import io.reactivex.rxjava3.plugins.RxJavaPlugins;
/*
* Copyright (C) Hans-Christoph Steiner 2016 <hans@eds.org>
* App.java is part of NewPipe.
*
* NewPipe is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* NewPipe is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with NewPipe. If not, see <http://www.gnu.org/licenses/>.
*/
public class App extends Application {
public static final String PACKAGE_NAME = BuildConfig.APPLICATION_ID;
private static final String TAG = App.class.toString();
private boolean isFirstRun = false;
private boolean notificationsRequested = false;
private static App app;
@NonNull
public static App getApp() {
return app;
}
public boolean getNotificationsRequested() {
return notificationsRequested;
}
public void setNotificationsRequested() {
notificationsRequested = true;
}
@Override
protected void attachBaseContext(final Context base) {
super.attachBaseContext(base);
initACRA();
}
@Override
public void onCreate() {
super.onCreate();
app = this;
if (ProcessPhoenix.isPhoenixProcess(this)) {
Log.i(TAG, "This is a phoenix process! "
+ "Aborting initialization of App[onCreate]");
return;
}
// check if the last used preference version is set
// to determine whether this is the first app run
final int lastUsedPrefVersion = PreferenceManager.getDefaultSharedPreferences(this)
.getInt(getString(R.string.last_used_preferences_version), -1);
isFirstRun = lastUsedPrefVersion == -1;
// Initialize settings first because other initializations can use its values
NewPipeSettings.initSettings(this);
NewPipe.init(getDownloader(),
Localization.getPreferredLocalization(this),
Localization.getPreferredContentCountry(this));
Localization.initPrettyTime(Localization.resolvePrettyTime());
BridgeStateSaverInitializer.init(this);
StateSaver.init(this);
initNotificationChannels();
ServiceHelper.initServices(this);
// Initialize image loader
final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this);
PicassoHelper.init(this);
ImageStrategy.setPreferredImageQuality(PreferredImageQuality.fromPreferenceKey(this,
prefs.getString(getString(R.string.image_quality_key),
getString(R.string.image_quality_default))));
PicassoHelper.setIndicatorsEnabled(MainActivity.DEBUG
&& prefs.getBoolean(getString(R.string.show_image_indicators_key), false));
configureRxJavaErrorHandler();
YoutubeStreamExtractor.setPoTokenProvider(PoTokenProviderImpl.INSTANCE);
}
@Override
public void onTerminate() {
super.onTerminate();
PicassoHelper.terminate();
}
protected Downloader getDownloader() {
final DownloaderImpl downloader = DownloaderImpl.init(null);
setCookiesToDownloader(downloader);
return downloader;
}
protected void setCookiesToDownloader(final DownloaderImpl downloader) {
final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(
getApplicationContext());
final String key = getApplicationContext().getString(R.string.recaptcha_cookies_key);
downloader.setCookie(ReCaptchaActivity.RECAPTCHA_COOKIES_KEY, prefs.getString(key, null));
downloader.updateYoutubeRestrictedModeCookies(getApplicationContext());
}
private void configureRxJavaErrorHandler() {
// https://github.com/ReactiveX/RxJava/wiki/What's-different-in-2.0#error-handling
RxJavaPlugins.setErrorHandler(new Consumer<Throwable>() {
@Override
public void accept(@NonNull final Throwable throwable) {
Log.e(TAG, "RxJavaPlugins.ErrorHandler called with -> : "
+ "throwable = [" + throwable.getClass().getName() + "]");
final Throwable actualThrowable;
if (throwable instanceof UndeliverableException) {
// As UndeliverableException is a wrapper,
// get the cause of it to get the "real" exception
actualThrowable = Objects.requireNonNull(throwable.getCause());
} else {
actualThrowable = throwable;
}
final List<Throwable> errors;
if (actualThrowable instanceof CompositeException) {
errors = ((CompositeException) actualThrowable).getExceptions();
} else {
errors = List.of(actualThrowable);
}
for (final Throwable error : errors) {
if (isThrowableIgnored(error)) {
return;
}
if (isThrowableCritical(error)) {
reportException(error);
return;
}
}
// Out-of-lifecycle exceptions should only be reported if a debug user wishes so,
// When exception is not reported, log it
if (isDisposedRxExceptionsReported()) {
reportException(actualThrowable);
} else {
Log.e(TAG, "RxJavaPlugin: Undeliverable Exception received: ", actualThrowable);
}
}
private boolean isThrowableIgnored(@NonNull final Throwable throwable) {
// Don't crash the application over a simple network problem
return ExceptionUtils.hasAssignableCause(throwable,
// network api cancellation
IOException.class, SocketException.class,
// blocking code disposed
InterruptedException.class, InterruptedIOException.class);
}
private boolean isThrowableCritical(@NonNull final Throwable throwable) {
// Though these exceptions cannot be ignored
return ExceptionUtils.hasAssignableCause(throwable,
NullPointerException.class, IllegalArgumentException.class, // bug in app
OnErrorNotImplementedException.class, MissingBackpressureException.class,
IllegalStateException.class); // bug in operator
}
private void reportException(@NonNull final Throwable throwable) {
// Throw uncaught exception that will trigger the report system
Thread.currentThread().getUncaughtExceptionHandler()
.uncaughtException(Thread.currentThread(), throwable);
}
});
}
/**
* Called in {@link #attachBaseContext(Context)} after calling the {@code super} method.
* Should be overridden if MultiDex is enabled, since it has to be initialized before ACRA.
*/
protected void initACRA() {
if (ACRA.isACRASenderServiceProcess()) {
return;
}
final CoreConfigurationBuilder acraConfig = new CoreConfigurationBuilder()
.withBuildConfigClass(BuildConfig.class);
ACRA.init(this, acraConfig);
}
private void initNotificationChannels() {
// Keep the importance below DEFAULT to avoid making noise on every notification update for
// the main and update channels
final List<NotificationChannelCompat> notificationChannelCompats = List.of(
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(),
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(),
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(),
new NotificationChannelCompat.Builder(getString(R.string.error_report_channel_id),
NotificationManagerCompat.IMPORTANCE_LOW)
.setName(getString(R.string.error_report_channel_name))
.setDescription(getString(R.string.error_report_channel_description))
.build(),
new NotificationChannelCompat
.Builder(getString(R.string.streams_notification_channel_id),
NotificationManagerCompat.IMPORTANCE_DEFAULT)
.setName(getString(R.string.streams_notification_channel_name))
.setDescription(
getString(R.string.streams_notification_channel_description))
.build()
);
final NotificationManagerCompat notificationManager = NotificationManagerCompat.from(this);
notificationManager.createNotificationChannelsCompat(notificationChannelCompats);
}
protected boolean isDisposedRxExceptionsReported() {
return false;
}
public boolean isFirstRun() {
return isFirstRun;
}
}

View File

@@ -0,0 +1,293 @@
package org.schabi.newpipe
import android.app.ActivityManager
import android.app.Application
import android.content.Context
import android.util.Log
import androidx.core.app.NotificationChannelCompat
import androidx.core.app.NotificationManagerCompat
import androidx.core.content.getSystemService
import androidx.preference.PreferenceManager
import coil3.ImageLoader
import coil3.SingletonImageLoader
import coil3.network.okhttp.OkHttpNetworkFetcherFactory
import coil3.request.allowRgb565
import coil3.request.crossfade
import coil3.util.DebugLogger
import com.jakewharton.processphoenix.ProcessPhoenix
import io.reactivex.rxjava3.exceptions.CompositeException
import io.reactivex.rxjava3.exceptions.MissingBackpressureException
import io.reactivex.rxjava3.exceptions.OnErrorNotImplementedException
import io.reactivex.rxjava3.exceptions.UndeliverableException
import io.reactivex.rxjava3.functions.Consumer
import io.reactivex.rxjava3.plugins.RxJavaPlugins
import java.io.IOException
import java.io.InterruptedIOException
import java.net.SocketException
import org.acra.ACRA.init
import org.acra.ACRA.isACRASenderServiceProcess
import org.acra.config.CoreConfigurationBuilder
import org.schabi.newpipe.error.ReCaptchaActivity
import org.schabi.newpipe.extractor.NewPipe
import org.schabi.newpipe.extractor.downloader.Downloader
import org.schabi.newpipe.extractor.services.youtube.extractors.YoutubeStreamExtractor
import org.schabi.newpipe.ktx.hasAssignableCause
import org.schabi.newpipe.settings.NewPipeSettings
import org.schabi.newpipe.util.BridgeStateSaverInitializer
import org.schabi.newpipe.util.Localization
import org.schabi.newpipe.util.ServiceHelper
import org.schabi.newpipe.util.StateSaver
import org.schabi.newpipe.util.image.ImageStrategy
import org.schabi.newpipe.util.image.PreferredImageQuality
import org.schabi.newpipe.util.potoken.PoTokenProviderImpl
/*
* Copyright (C) Hans-Christoph Steiner 2016 <hans@eds.org>
* App.kt is part of NewPipe.
*
* NewPipe is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* NewPipe is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with NewPipe. If not, see <http://www.gnu.org/licenses/>.
*/
open class App :
Application(),
SingletonImageLoader.Factory {
var isFirstRun = false
private set
var notificationsRequested = false
private set
fun setNotificationsRequested() {
notificationsRequested = true
}
override fun attachBaseContext(base: Context?) {
super.attachBaseContext(base)
initACRA()
}
override fun onCreate() {
super.onCreate()
instance = this
if (ProcessPhoenix.isPhoenixProcess(this)) {
Log.i(TAG, "This is a phoenix process! Aborting initialization of App[onCreate]")
return
}
// check if the last used preference version is set
// to determine whether this is the first app run
val lastUsedPrefVersion =
PreferenceManager
.getDefaultSharedPreferences(this)
.getInt(getString(R.string.last_used_preferences_version), -1)
isFirstRun = lastUsedPrefVersion == -1
// Initialize settings first because other initializations can use its values
NewPipeSettings.initSettings(this)
NewPipe.init(
getDownloader(),
Localization.getPreferredLocalization(this),
Localization.getPreferredContentCountry(this)
)
Localization.initPrettyTime(Localization.resolvePrettyTime())
BridgeStateSaverInitializer.init(this)
StateSaver.init(this)
initNotificationChannels()
ServiceHelper.initServices(this)
// Initialize image loader
val prefs = PreferenceManager.getDefaultSharedPreferences(this)
ImageStrategy.setPreferredImageQuality(
PreferredImageQuality.fromPreferenceKey(
this,
prefs.getString(
getString(R.string.image_quality_key),
getString(R.string.image_quality_default)
)
)
)
configureRxJavaErrorHandler()
YoutubeStreamExtractor.setPoTokenProvider(PoTokenProviderImpl)
}
override fun newImageLoader(context: Context): ImageLoader = ImageLoader
.Builder(this)
.logger(if (BuildConfig.DEBUG) DebugLogger() else null)
.allowRgb565(getSystemService<ActivityManager>()!!.isLowRamDevice)
.crossfade(true)
.components {
add(OkHttpNetworkFetcherFactory(callFactory = DownloaderImpl.getInstance().client))
}.build()
protected open fun getDownloader(): Downloader {
val downloader = DownloaderImpl.init(null)
setCookiesToDownloader(downloader)
return downloader
}
protected fun setCookiesToDownloader(downloader: DownloaderImpl) {
val prefs = PreferenceManager.getDefaultSharedPreferences(this)
val key = getString(R.string.recaptcha_cookies_key)
downloader.setCookie(ReCaptchaActivity.RECAPTCHA_COOKIES_KEY, prefs.getString(key, null))
downloader.updateYoutubeRestrictedModeCookies(this)
}
private fun configureRxJavaErrorHandler() {
// https://github.com/ReactiveX/RxJava/wiki/What's-different-in-2.0#error-handling
RxJavaPlugins.setErrorHandler(
object : Consumer<Throwable> {
override fun accept(throwable: Throwable) {
Log.e(TAG, "RxJavaPlugins.ErrorHandler called with -> : throwable = [${throwable.javaClass.getName()}]")
// As UndeliverableException is a wrapper,
// get the cause of it to get the "real" exception
val actualThrowable = (throwable as? UndeliverableException)?.cause ?: throwable
val errors = (actualThrowable as? CompositeException)?.exceptions ?: listOf(actualThrowable)
for (error in errors) {
if (isThrowableIgnored(error)) {
return
}
if (isThrowableCritical(error)) {
reportException(error)
return
}
}
// Out-of-lifecycle exceptions should only be reported if a debug user wishes so,
// When exception is not reported, log it
if (isDisposedRxExceptionsReported()) {
reportException(actualThrowable)
} else {
Log.e(TAG, "RxJavaPlugin: Undeliverable Exception received: ", actualThrowable)
}
}
fun isThrowableIgnored(throwable: Throwable): Boolean {
// Don't crash the application over a simple network problem
return throwable // network api cancellation
.hasAssignableCause(
IOException::class.java,
SocketException::class.java, // blocking code disposed
InterruptedException::class.java,
InterruptedIOException::class.java
)
}
fun isThrowableCritical(throwable: Throwable): Boolean {
// Though these exceptions cannot be ignored
return throwable
.hasAssignableCause(
// bug in app
NullPointerException::class.java,
IllegalArgumentException::class.java,
OnErrorNotImplementedException::class.java,
MissingBackpressureException::class.java,
// bug in operator
IllegalStateException::class.java
)
}
fun reportException(throwable: Throwable) {
// Throw uncaught exception that will trigger the report system
Thread
.currentThread()
.uncaughtExceptionHandler
.uncaughtException(Thread.currentThread(), throwable)
}
}
)
}
/**
* Called in [.attachBaseContext] after calling the `super` method.
* Should be overridden if MultiDex is enabled, since it has to be initialized before ACRA.
*/
protected fun initACRA() {
if (isACRASenderServiceProcess()) {
return
}
val acraConfig =
CoreConfigurationBuilder()
.withBuildConfigClass(BuildConfig::class.java)
init(this, acraConfig)
}
private fun initNotificationChannels() {
// Keep the importance below DEFAULT to avoid making noise on every notification update for
// the main and update channels
val mainChannel =
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()
val appUpdateChannel =
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()
val hashChannel =
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()
val errorReportChannel =
NotificationChannelCompat
.Builder(
getString(R.string.error_report_channel_id),
NotificationManagerCompat.IMPORTANCE_LOW
).setName(getString(R.string.error_report_channel_name))
.setDescription(getString(R.string.error_report_channel_description))
.build()
val newStreamChannel =
NotificationChannelCompat
.Builder(
getString(R.string.streams_notification_channel_id),
NotificationManagerCompat.IMPORTANCE_DEFAULT
).setName(getString(R.string.streams_notification_channel_name))
.setDescription(getString(R.string.streams_notification_channel_description))
.build()
val channels = listOf(mainChannel, appUpdateChannel, hashChannel, errorReportChannel, newStreamChannel)
NotificationManagerCompat.from(this).createNotificationChannelsCompat(channels)
}
protected open fun isDisposedRxExceptionsReported(): Boolean = false
companion object {
const val PACKAGE_NAME: String = BuildConfig.APPLICATION_ID
private val TAG = App::class.java.toString()
@JvmStatic
lateinit var instance: App
private set
}
}

View File

@@ -48,6 +48,11 @@ public final class DownloaderImpl extends Downloader {
this.mCookies = new HashMap<>();
}
@NonNull
public OkHttpClient getClient() {
return client;
}
/**
* It's recommended to call exactly once in the entire lifetime of the application.
*

View File

@@ -1,50 +0,0 @@
package org.schabi.newpipe;
import android.annotation.SuppressLint;
import android.app.Activity;
import android.content.Intent;
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.
*
* NewPipe is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* NewPipe is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with NewPipe. If not, see <http://www.gnu.org/licenses/>.
*/
public class ExitActivity extends Activity {
public static void exitAndRemoveFromRecentApps(final Activity activity) {
final Intent intent = new Intent(activity, ExitActivity.class);
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK
| Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS
| Intent.FLAG_ACTIVITY_CLEAR_TASK
| Intent.FLAG_ACTIVITY_NO_ANIMATION);
activity.startActivity(intent);
}
@SuppressLint("NewApi")
@Override
protected void onCreate(final Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
finishAndRemoveTask();
NavigationHelper.restartApp(this);
}
}

View File

@@ -0,0 +1,36 @@
/*
* SPDX-FileCopyrightText: 2016-2026 NewPipe contributors <https://newpipe.net>
* SPDX-License-Identifier: GPL-3.0-or-later
*/
package org.schabi.newpipe
import android.annotation.SuppressLint
import android.app.Activity
import android.content.Intent
import android.os.Bundle
import org.schabi.newpipe.util.NavigationHelper
class ExitActivity : Activity() {
@SuppressLint("NewApi")
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
finishAndRemoveTask()
NavigationHelper.restartApp(this)
}
companion object {
@JvmStatic
fun exitAndRemoveFromRecentApps(activity: Activity) {
val intent = Intent(activity, ExitActivity::class.java)
intent.addFlags(
Intent.FLAG_ACTIVITY_NEW_TASK
or Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS
or Intent.FLAG_ACTIVITY_CLEAR_TASK
or Intent.FLAG_ACTIVITY_NO_ANIMATION
)
activity.startActivity(intent)
}
}
}

View File

@@ -20,6 +20,7 @@
package org.schabi.newpipe;
import android.app.AlertDialog;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
@@ -96,6 +97,8 @@ import org.schabi.newpipe.util.ThemeHelper;
import org.schabi.newpipe.util.external_communication.ShareUtils;
import org.schabi.newpipe.views.FocusOverlayView;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
@@ -191,11 +194,17 @@ public class MainActivity extends AppCompatActivity {
NotificationWorker.initialize(this);
}
if (!UpdateSettingsFragment.wasUserAskedForConsent(this)
&& !App.getApp().isFirstRun()
&& !App.getInstance().isFirstRun()
&& ReleaseVersionUtil.INSTANCE.isReleaseApk()) {
UpdateSettingsFragment.askForConsentToUpdateChecks(this);
}
// ReleaseVersionUtil.INSTANCE.isReleaseApk() will be true only for main official build
// We want every release build (nightly, nightly-refactor) to show the popup
if (!DEBUG) {
showKeepAndroidDialog();
}
MigrationManager.showUserInfoIfPresent(this);
}
@@ -203,7 +212,7 @@ public class MainActivity extends AppCompatActivity {
protected void onPostCreate(final Bundle savedInstanceState) {
super.onPostCreate(savedInstanceState);
final App app = App.getApp();
final App app = App.getInstance();
if (sharedPreferences.getBoolean(app.getString(R.string.update_app_key), false)
&& sharedPreferences
@@ -309,25 +318,21 @@ public class MainActivity extends AppCompatActivity {
}
private boolean drawerItemSelected(final MenuItem item) {
switch (item.getGroupId()) {
case R.id.menu_services_group:
changeService(item);
break;
case R.id.menu_tabs_group:
tabSelected(item);
break;
case R.id.menu_kiosks_group:
try {
kioskSelected(item);
} catch (final Exception e) {
ErrorUtil.showUiErrorSnackbar(this, "Selecting drawer kiosk", e);
}
break;
case R.id.menu_options_about_group:
optionsAboutSelected(item);
break;
default:
return false;
final int groupId = item.getGroupId();
if (groupId == R.id.menu_services_group) {
changeService(item);
} else if (groupId == R.id.menu_tabs_group) {
tabSelected(item);
} else if (groupId == R.id.menu_kiosks_group) {
try {
kioskSelected(item);
} catch (final Exception e) {
ErrorUtil.showUiErrorSnackbar(this, "Selecting drawer kiosk", e);
}
} else if (groupId == R.id.menu_options_about_group) {
optionsAboutSelected(item);
} else {
return false;
}
mainBinding.getRoot().closeDrawers();
@@ -977,4 +982,57 @@ public class MainActivity extends AppCompatActivity {
|| sheetState == BottomSheetBehavior.STATE_COLLAPSED;
}
private void showKeepAndroidDialog() {
final var prefs = PreferenceManager.getDefaultSharedPreferences(this);
final var now = Instant.now();
final var kaoLastCheck = Instant.ofEpochMilli(prefs.getLong(
getString(R.string.kao_last_checked_key),
0
));
final var supportedLannguages = List.of("fr", "de", "ca", "es", "id", "it", "pl",
"pt", "cs", "sk", "fa", "ar", "tr", "el", "th", "ru", "uk", "ko", "zh", "ja");
final var locale = Localization.getAppLocale();
final String kaoBaseUrl = "https://keepandroidopen.org/";
final String kaoURI;
if (supportedLannguages.contains(locale.getLanguage())) {
if ("zh".equals(locale.getLanguage())) {
kaoURI = kaoBaseUrl + ("TW".equals(locale.getCountry()) ? "zh-TW" : "zh-CN");
} else {
kaoURI = kaoBaseUrl + locale.getLanguage();
}
} else {
kaoURI = kaoBaseUrl;
}
final var solutionURI =
"https://github.com/woheller69/FreeDroidWarn?tab=readme-ov-file#solutions";
if (kaoLastCheck.plus(30, ChronoUnit.DAYS).isBefore(now)) {
final var dialog = new AlertDialog.Builder(this)
.setTitle("Keep Android Open")
.setCancelable(false)
.setMessage(this.getString(R.string.kao_dialog_warning))
.setPositiveButton(this.getString(android.R.string.ok), (d, w) -> {
prefs.edit()
.putLong(
getString(R.string.kao_last_checked_key),
now.toEpochMilli()
)
.apply();
})
.setNeutralButton(this.getString(R.string.kao_solution), null)
.setNegativeButton(this.getString(R.string.kao_dialog_more_info), null)
.show();
// If we use setNeutralButton and etc. dialog will close after pressing the buttons,
// but we want it to close only when positive button is pressed
dialog.getButton(AlertDialog.BUTTON_NEGATIVE).setOnClickListener(v ->
ShareUtils.openUrlInBrowser(this, kaoURI)
);
dialog.getButton(AlertDialog.BUTTON_NEUTRAL).setOnClickListener(v ->
ShareUtils.openUrlInBrowser(this, solutionURI)
);
}
}
}

View File

@@ -82,7 +82,9 @@ class NewVersionWorker(
)
val notificationManager = NotificationManagerCompat.from(applicationContext)
notificationManager.notify(2000, notificationBuilder.build())
if (notificationManager.areNotificationsEnabled()) {
notificationManager.notify(2000, notificationBuilder.build())
}
}
@Throws(IOException::class, ReCaptchaException::class)

View File

@@ -41,50 +41,50 @@ public final class QueueItemMenuUtil {
}
popupMenu.setOnMenuItemClickListener(menuItem -> {
switch (menuItem.getItemId()) {
case R.id.menu_item_remove:
final int index = playQueue.indexOf(item);
playQueue.remove(index);
return true;
case R.id.menu_item_details:
// playQueue is null since we don't want any queue change
NavigationHelper.openVideoDetail(context, item.getServiceId(),
item.getUrl(), item.getTitle(), null,
false);
return true;
case R.id.menu_item_append_playlist:
PlaylistDialog.createCorrespondingDialog(
context,
List.of(new StreamEntity(item)),
dialog -> dialog.show(
fragmentManager,
"QueueItemMenuUtil@append_playlist"
)
);
final int itemId = menuItem.getItemId();
if (itemId == R.id.menu_item_remove) {
final int index = playQueue.indexOf(item);
playQueue.remove(index);
return true;
} else if (itemId == R.id.menu_item_details) {
// playQueue is null since we don't want any queue change
NavigationHelper.openVideoDetail(context, item.getServiceId(),
item.getUrl(), item.getTitle(), null,
false);
return true;
} else if (itemId == R.id.menu_item_append_playlist) {
PlaylistDialog.createCorrespondingDialog(
context,
List.of(new StreamEntity(item)),
dialog -> dialog.show(
fragmentManager,
"QueueItemMenuUtil@append_playlist"
)
);
return true;
case R.id.menu_item_channel_details:
SparseItemUtil.fetchUploaderUrlIfSparse(context, item.getServiceId(),
item.getUrl(), item.getUploaderUrl(),
// An intent must be used here.
// Opening with FragmentManager transactions is not working,
// as PlayQueueActivity doesn't use fragments.
uploaderUrl -> NavigationHelper.openChannelFragmentUsingIntent(
context, item.getServiceId(), uploaderUrl, item.getUploader()
));
return true;
case R.id.menu_item_share:
shareText(context, item.getTitle(), item.getUrl(),
item.getThumbnails());
return true;
case R.id.menu_item_download:
fetchStreamInfoAndSaveToDatabase(context, item.getServiceId(), item.getUrl(),
info -> {
final DownloadDialog downloadDialog = new DownloadDialog(context,
info);
downloadDialog.show(fragmentManager, "downloadDialog");
});
return true;
return true;
} else if (itemId == R.id.menu_item_channel_details) {
SparseItemUtil.fetchUploaderUrlIfSparse(context, item.getServiceId(),
item.getUrl(), item.getUploaderUrl(),
// An intent must be used here.
// Opening with FragmentManager transactions is not working,
// as PlayQueueActivity doesn't use fragments.
uploaderUrl -> NavigationHelper.openChannelFragmentUsingIntent(
context, item.getServiceId(), uploaderUrl, item.getUploader()
));
return true;
} else if (itemId == R.id.menu_item_share) {
shareText(context, item.getTitle(), item.getUrl(),
item.getThumbnails());
return true;
} else if (itemId == R.id.menu_item_download) {
fetchStreamInfoAndSaveToDatabase(context, item.getServiceId(), item.getUrl(),
info -> {
final DownloadDialog downloadDialog = new DownloadDialog(context,
info);
downloadDialog.show(fragmentManager, "downloadDialog");
});
return true;
}
return false;
});

View File

@@ -343,8 +343,7 @@ public class RouterActivity extends AppCompatActivity {
return;
}
final List<StreamingService.ServiceInfo.MediaCapability> capabilities =
currentService.getServiceInfo().getMediaCapabilities();
final var capabilities = currentService.getServiceInfo().getMediaCapabilities();
// Check if the service supports the choice
if ((isVideoPlayerSelected && capabilities.contains(VIDEO))
@@ -528,8 +527,7 @@ public class RouterActivity extends AppCompatActivity {
final List<AdapterChoiceItem> returnedItems = new ArrayList<>();
returnedItems.add(showInfo); // Always present
final List<StreamingService.ServiceInfo.MediaCapability> capabilities =
service.getServiceInfo().getMediaCapabilities();
final var capabilities = service.getServiceInfo().getMediaCapabilities();
if (linkType == LinkType.STREAM || linkType == LinkType.PLAYLIST) {
if (capabilities.contains(VIDEO)) {

View File

@@ -92,7 +92,7 @@ class AboutActivity : AppCompatActivity() {
return when (position) {
posAbout -> AboutFragment()
posLicense -> LicenseFragment.newInstance(SOFTWARE_COMPONENTS)
else -> throw IllegalArgumentException("Unknown position for ViewPager2")
else -> error("Unknown position for ViewPager2")
}
}
@@ -105,7 +105,7 @@ class AboutActivity : AppCompatActivity() {
return when (position) {
posAbout -> R.string.tab_about
posLicense -> R.string.tab_licenses
else -> throw IllegalArgumentException("Unknown position for ViewPager2")
else -> error("Unknown position for ViewPager2")
}
}
}
@@ -207,10 +207,10 @@ class AboutActivity : AppCompatActivity() {
StandardLicenses.APACHE2
),
SoftwareComponent(
"Picasso",
"2013",
"Square, Inc.",
"https://square.github.io/picasso/",
"Coil",
"2023",
"Coil Contributors",
"https://coil-kt.github.io/coil/",
StandardLicenses.APACHE2
),
SoftwareComponent(

View File

@@ -62,11 +62,7 @@ data class PlaylistRemoteEntity(
orderingName = playlistInfo.name,
url = playlistInfo.url,
thumbnailUrl = ImageStrategy.imageListToDbUrl(
if (playlistInfo.thumbnails.isEmpty()) {
playlistInfo.uploaderAvatars
} else {
playlistInfo.thumbnails
}
playlistInfo.thumbnails.ifEmpty { playlistInfo.uploaderAvatars }
),
uploader = playlistInfo.uploaderName,
streamCount = playlistInfo.streamCount

View File

@@ -87,7 +87,7 @@ abstract class StreamDAO : BasicDAO<StreamEntity> {
private fun compareAndUpdateStream(newerStream: StreamEntity) {
val existentMinimalStream = getMinimalStreamForCompare(newerStream.serviceId, newerStream.url)
?: throw IllegalStateException("Stream cannot be null just after insertion.")
?: error("Stream cannot be null just after insertion.")
newerStream.uid = existentMinimalStream.uid
if (!StreamTypeUtil.isLiveStream(newerStream.streamType)) {

View File

@@ -100,7 +100,7 @@ abstract class SubscriptionDAO : BasicDAO<SubscriptionEntity> {
entity.uid = uidFromInsert
} else {
val subscriptionIdFromDb = getSubscriptionIdInternal(entity.serviceId, entity.url!!)
?: throw IllegalStateException("Subscription cannot be null just after insertion.")
?: error("Subscription cannot be null just after insertion.")
entity.uid = subscriptionIdFromDb
update(entity)

View File

@@ -16,6 +16,7 @@ import android.os.IBinder;
import android.provider.Settings;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.widget.AdapterView;
@@ -31,7 +32,6 @@ import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.StringRes;
import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.view.menu.ActionMenuItemView;
import androidx.appcompat.widget.Toolbar;
import androidx.collection.SparseArrayCompat;
import androidx.documentfile.provider.DocumentFile;
@@ -113,7 +113,7 @@ public class DownloadDialog extends DialogFragment
private StoredDirectoryHelper mainStorageAudio = null;
private StoredDirectoryHelper mainStorageVideo = null;
private DownloadManager downloadManager = null;
private ActionMenuItemView okButton = null;
private MenuItem okButton = null;
private Context context = null;
private boolean askForSavePath;
@@ -344,7 +344,7 @@ public class DownloadDialog extends DialogFragment
toolbar.setNavigationOnClickListener(v -> dismiss());
toolbar.setNavigationContentDescription(R.string.cancel);
okButton = toolbar.findViewById(R.id.okay);
okButton = toolbar.getMenu().findItem(R.id.okay);
okButton.setEnabled(false); // disable until the download service connection is done
toolbar.setOnMenuItemClickListener(item -> {
@@ -558,17 +558,13 @@ public class DownloadDialog extends DialogFragment
}
boolean flag = true;
switch (checkedId) {
case R.id.audio_button:
setupAudioSpinner();
break;
case R.id.video_button:
setupVideoSpinner();
break;
case R.id.subtitle_button:
setupSubtitleSpinner();
flag = false;
break;
if (checkedId == R.id.audio_button) {
setupAudioSpinner();
} else if (checkedId == R.id.video_button) {
setupVideoSpinner();
} else if (checkedId == R.id.subtitle_button) {
setupSubtitleSpinner();
flag = false;
}
dialogBinding.threads.setEnabled(flag);
@@ -585,29 +581,26 @@ public class DownloadDialog extends DialogFragment
+ "position = [" + position + "], id = [" + id + "]");
}
switch (parent.getId()) {
case R.id.quality_spinner:
switch (dialogBinding.videoAudioGroup.getCheckedRadioButtonId()) {
case R.id.video_button:
selectedVideoIndex = position;
onVideoStreamSelected();
break;
case R.id.subtitle_button:
selectedSubtitleIndex = position;
break;
}
onItemSelectedSetFileName();
break;
case R.id.audio_track_spinner:
final boolean trackChanged = selectedAudioTrackIndex != position;
selectedAudioTrackIndex = position;
if (trackChanged) {
updateSecondaryStreams();
fetchStreamsSize();
}
break;
case R.id.audio_stream_spinner:
selectedAudioIndex = position;
final int parentId = parent.getId();
if (parentId == R.id.quality_spinner) {
final int checkedRadioButtonId = dialogBinding.videoAudioGroup
.getCheckedRadioButtonId();
if (checkedRadioButtonId == R.id.video_button) {
selectedVideoIndex = position;
onVideoStreamSelected();
} else if (checkedRadioButtonId == R.id.subtitle_button) {
selectedSubtitleIndex = position;
}
onItemSelectedSetFileName();
} else if (parentId == R.id.audio_track_spinner) {
final boolean trackChanged = selectedAudioTrackIndex != position;
selectedAudioTrackIndex = position;
if (trackChanged) {
updateSecondaryStreams();
fetchStreamsSize();
}
} else if (parentId == R.id.audio_stream_spinner) {
selectedAudioIndex = position;
}
}
@@ -622,23 +615,20 @@ public class DownloadDialog extends DialogFragment
|| prevFileName.startsWith(getString(R.string.caption_file_name, fileName, ""))) {
// only update the file name field if it was not edited by the user
switch (dialogBinding.videoAudioGroup.getCheckedRadioButtonId()) {
case R.id.audio_button:
case R.id.video_button:
if (!prevFileName.equals(fileName)) {
// since the user might have switched between audio and video, the correct
// text might already be in place, so avoid resetting the cursor position
dialogBinding.fileName.setText(fileName);
}
break;
case R.id.subtitle_button:
final String setSubtitleLanguageCode = subtitleStreamsAdapter
.getItem(selectedSubtitleIndex).getLanguageTag();
// this will reset the cursor position, which is bad UX, but it can't be avoided
dialogBinding.fileName.setText(getString(
R.string.caption_file_name, fileName, setSubtitleLanguageCode));
break;
final int radioButtonId = dialogBinding.videoAudioGroup
.getCheckedRadioButtonId();
if (radioButtonId == R.id.audio_button || radioButtonId == R.id.video_button) {
if (!prevFileName.equals(fileName)) {
// since the user might have switched between audio and video, the correct
// text might already be in place, so avoid resetting the cursor position
dialogBinding.fileName.setText(fileName);
}
} else if (radioButtonId == R.id.subtitle_button) {
final String setSubtitleLanguageCode = subtitleStreamsAdapter
.getItem(selectedSubtitleIndex).getLanguageTag();
// this will reset the cursor position, which is bad UX, but it can't be avoided
dialogBinding.fileName.setText(getString(
R.string.caption_file_name, fileName, setSubtitleLanguageCode));
}
}
}
@@ -770,47 +760,44 @@ public class DownloadDialog extends DialogFragment
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();
size = getWrappedAudioStreams().getSizeInBytes(selectedAudioIndex);
if (format == MediaFormat.WEBMA_OPUS) {
mimeTmp = "audio/ogg";
filenameTmp += "opus";
} else if (format != null) {
mimeTmp = format.mimeType;
filenameTmp += format.getSuffix();
}
break;
case R.id.video_button:
selectedMediaType = getString(R.string.last_download_type_video_key);
mainStorage = mainStorageVideo;
format = videoStreamsAdapter.getItem(selectedVideoIndex).getFormat();
size = wrappedVideoStreams.getSizeInBytes(selectedVideoIndex);
if (format != null) {
mimeTmp = format.mimeType;
filenameTmp += format.getSuffix();
}
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();
size = wrappedSubtitleStreams.getSizeInBytes(selectedSubtitleIndex);
if (format != null) {
mimeTmp = format.mimeType;
}
final int checkedRadioButtonId = dialogBinding.videoAudioGroup.getCheckedRadioButtonId();
if (checkedRadioButtonId == R.id.audio_button) {
selectedMediaType = getString(R.string.last_download_type_audio_key);
mainStorage = mainStorageAudio;
format = audioStreamsAdapter.getItem(selectedAudioIndex).getFormat();
size = getWrappedAudioStreams().getSizeInBytes(selectedAudioIndex);
if (format == MediaFormat.WEBMA_OPUS) {
mimeTmp = "audio/ogg";
filenameTmp += "opus";
} else if (format != null) {
mimeTmp = format.mimeType;
filenameTmp += format.getSuffix();
}
} else if (checkedRadioButtonId == R.id.video_button) {
selectedMediaType = getString(R.string.last_download_type_video_key);
mainStorage = mainStorageVideo;
format = videoStreamsAdapter.getItem(selectedVideoIndex).getFormat();
size = wrappedVideoStreams.getSizeInBytes(selectedVideoIndex);
if (format != null) {
mimeTmp = format.mimeType;
filenameTmp += format.getSuffix();
}
} else if (checkedRadioButtonId == 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();
size = wrappedSubtitleStreams.getSizeInBytes(selectedSubtitleIndex);
if (format != null) {
mimeTmp = format.mimeType;
}
if (format == MediaFormat.TTML) {
filenameTmp += MediaFormat.SRT.getSuffix();
} else if (format != null) {
filenameTmp += format.getSuffix();
}
break;
default:
throw new RuntimeException("No stream selected");
if (format == MediaFormat.TTML) {
filenameTmp += MediaFormat.SRT.getSuffix();
} else if (format != null) {
filenameTmp += format.getSuffix();
}
} else {
throw new RuntimeException("No stream selected");
}
if (!askForSavePath && (mainStorage == null
@@ -1057,59 +1044,56 @@ public class DownloadDialog extends DialogFragment
long nearLength = 0;
// more download logic: select muxer, subtitle converter, etc.
switch (dialogBinding.videoAudioGroup.getCheckedRadioButtonId()) {
case R.id.audio_button:
kind = 'a';
selectedStream = audioStreamsAdapter.getItem(selectedAudioIndex);
final int checkedRadioButtonId = dialogBinding.videoAudioGroup.getCheckedRadioButtonId();
if (checkedRadioButtonId == R.id.audio_button) {
kind = 'a';
selectedStream = audioStreamsAdapter.getItem(selectedAudioIndex);
if (selectedStream.getFormat() == MediaFormat.M4A) {
psName = Postprocessing.ALGORITHM_M4A_NO_DASH;
} else if (selectedStream.getFormat() == MediaFormat.WEBMA_OPUS) {
psName = Postprocessing.ALGORITHM_OGG_FROM_WEBM_DEMUXER;
if (selectedStream.getFormat() == MediaFormat.M4A) {
psName = Postprocessing.ALGORITHM_M4A_NO_DASH;
} else if (selectedStream.getFormat() == MediaFormat.WEBMA_OPUS) {
psName = Postprocessing.ALGORITHM_OGG_FROM_WEBM_DEMUXER;
}
} else if (checkedRadioButtonId == R.id.video_button) {
kind = 'v';
selectedStream = videoStreamsAdapter.getItem(selectedVideoIndex);
final SecondaryStreamHelper<AudioStream> secondary = videoStreamsAdapter
.getAllSecondary()
.get(wrappedVideoStreams.getStreamsList().indexOf(selectedStream));
if (secondary != null) {
secondaryStream = secondary.getStream();
if (selectedStream.getFormat() == MediaFormat.MPEG_4) {
psName = Postprocessing.ALGORITHM_MP4_FROM_DASH_MUXER;
} else {
psName = Postprocessing.ALGORITHM_WEBM_MUXER;
}
break;
case R.id.video_button:
kind = 'v';
selectedStream = videoStreamsAdapter.getItem(selectedVideoIndex);
final SecondaryStreamHelper<AudioStream> secondary = videoStreamsAdapter
.getAllSecondary()
.get(wrappedVideoStreams.getStreamsList().indexOf(selectedStream));
final long videoSize = wrappedVideoStreams.getSizeInBytes(
(VideoStream) selectedStream);
if (secondary != null) {
secondaryStream = secondary.getStream();
if (selectedStream.getFormat() == MediaFormat.MPEG_4) {
psName = Postprocessing.ALGORITHM_MP4_FROM_DASH_MUXER;
} else {
psName = Postprocessing.ALGORITHM_WEBM_MUXER;
}
final long videoSize = wrappedVideoStreams.getSizeInBytes(
(VideoStream) selectedStream);
// set nearLength, only, if both sizes are fetched or known. This probably
// does not work on slow networks but is later updated in the downloader
if (secondary.getSizeInBytes() > 0 && videoSize > 0) {
nearLength = secondary.getSizeInBytes() + videoSize;
}
// set nearLength, only, if both sizes are fetched or known. This probably
// does not work on slow networks but is later updated in the downloader
if (secondary.getSizeInBytes() > 0 && videoSize > 0) {
nearLength = secondary.getSizeInBytes() + videoSize;
}
break;
case R.id.subtitle_button:
threads = 1; // use unique thread for subtitles due small file size
kind = 's';
selectedStream = subtitleStreamsAdapter.getItem(selectedSubtitleIndex);
}
} else if (checkedRadioButtonId == R.id.subtitle_button) {
threads = 1; // use unique thread for subtitles due small file size
kind = 's';
selectedStream = subtitleStreamsAdapter.getItem(selectedSubtitleIndex);
if (selectedStream.getFormat() == MediaFormat.TTML) {
psName = Postprocessing.ALGORITHM_TTML_CONVERTER;
psArgs = new String[] {
selectedStream.getFormat().getSuffix(),
"false" // ignore empty frames
};
}
break;
default:
return;
if (selectedStream.getFormat() == MediaFormat.TTML) {
psName = Postprocessing.ALGORITHM_TTML_CONVERTER;
psArgs = new String[]{
selectedStream.getFormat().getSuffix(),
"false" // ignore empty frames
};
}
} else {
return;
}
if (secondaryStream == null) {

View File

@@ -1,324 +0,0 @@
package org.schabi.newpipe.error;
import android.content.Context;
import android.content.Intent;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.util.Log;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import androidx.appcompat.app.ActionBar;
import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.app.AppCompatActivity;
import androidx.core.content.IntentCompat;
import com.grack.nanojson.JsonWriter;
import org.schabi.newpipe.BuildConfig;
import org.schabi.newpipe.R;
import org.schabi.newpipe.databinding.ActivityErrorBinding;
import org.schabi.newpipe.util.Localization;
import org.schabi.newpipe.util.ThemeHelper;
import org.schabi.newpipe.util.external_communication.ShareUtils;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
import java.util.Arrays;
import java.util.stream.Collectors;
/*
* Created by Christian Schabesberger on 24.10.15.
*
* Copyright (C) Christian Schabesberger 2016 <chris.schabesberger@mailbox.org>
* ErrorActivity.java is part of NewPipe.
*
* NewPipe is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
* <
* NewPipe is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
* <
* You should have received a copy of the GNU General Public License
* along with NewPipe. If not, see <http://www.gnu.org/licenses/>.
*/
/**
* This activity is used to show error details and allow reporting them in various ways. Use {@link
* ErrorUtil#openActivity(Context, ErrorInfo)} to correctly open this activity.
*/
public class ErrorActivity extends AppCompatActivity {
// LOG TAGS
public static final String TAG = ErrorActivity.class.toString();
// BUNDLE TAGS
public static final String ERROR_INFO = "error_info";
public static final String ERROR_EMAIL_ADDRESS = "crashreport@newpipe.schabi.org";
public static final String ERROR_EMAIL_SUBJECT = "Exception in ";
public static final String ERROR_GITHUB_ISSUE_URL =
"https://github.com/TeamNewPipe/NewPipe/issues";
private ErrorInfo errorInfo;
private String currentTimeStamp;
private ActivityErrorBinding activityErrorBinding;
////////////////////////////////////////////////////////////////////////
// Activity lifecycle
////////////////////////////////////////////////////////////////////////
@Override
protected void onCreate(final Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
ThemeHelper.setDayNightMode(this);
ThemeHelper.setTheme(this);
activityErrorBinding = ActivityErrorBinding.inflate(getLayoutInflater());
setContentView(activityErrorBinding.getRoot());
final Intent intent = getIntent();
setSupportActionBar(activityErrorBinding.toolbarLayout.toolbar);
final ActionBar actionBar = getSupportActionBar();
if (actionBar != null) {
actionBar.setDisplayHomeAsUpEnabled(true);
actionBar.setTitle(R.string.error_report_title);
actionBar.setDisplayShowTitleEnabled(true);
}
errorInfo = IntentCompat.getParcelableExtra(intent, ERROR_INFO, ErrorInfo.class);
// important add guru meditation
addGuruMeditation();
// print current time, as zoned ISO8601 timestamp
final ZonedDateTime now = ZonedDateTime.now();
currentTimeStamp = now.format(DateTimeFormatter.ISO_OFFSET_DATE_TIME);
activityErrorBinding.errorReportEmailButton.setOnClickListener(v ->
openPrivacyPolicyDialog(this, "EMAIL"));
activityErrorBinding.errorReportCopyButton.setOnClickListener(v ->
ShareUtils.copyToClipboard(this, buildMarkdown()));
activityErrorBinding.errorReportGitHubButton.setOnClickListener(v ->
openPrivacyPolicyDialog(this, "GITHUB"));
// normal bugreport
buildInfo(errorInfo);
activityErrorBinding.errorMessageView.setText(errorInfo.getMessage(this));
activityErrorBinding.errorView.setText(formErrorText(errorInfo.getStackTraces()));
// print stack trace once again for debugging:
for (final String e : errorInfo.getStackTraces()) {
Log.e(TAG, e);
}
}
@Override
public boolean onCreateOptionsMenu(final Menu menu) {
final MenuInflater inflater = getMenuInflater();
inflater.inflate(R.menu.error_menu, menu);
return true;
}
@Override
public boolean onOptionsItemSelected(final MenuItem item) {
switch (item.getItemId()) {
case android.R.id.home:
onBackPressed();
return true;
case R.id.menu_item_share_error:
ShareUtils.shareText(getApplicationContext(),
getString(R.string.error_report_title), buildJson());
return true;
default:
return false;
}
}
private void openPrivacyPolicyDialog(final Context context, final String action) {
new AlertDialog.Builder(context)
.setIcon(android.R.drawable.ic_dialog_alert)
.setTitle(R.string.privacy_policy_title)
.setMessage(R.string.start_accept_privacy_policy)
.setCancelable(false)
.setNeutralButton(R.string.read_privacy_policy, (dialog, which) ->
ShareUtils.openUrlInApp(context,
context.getString(R.string.privacy_policy_url)))
.setPositiveButton(R.string.accept, (dialog, which) -> {
if (action.equals("EMAIL")) { // send on email
final Intent i = new Intent(Intent.ACTION_SENDTO)
.setData(Uri.parse("mailto:")) // only email apps should handle this
.putExtra(Intent.EXTRA_EMAIL, new String[]{ERROR_EMAIL_ADDRESS})
.putExtra(Intent.EXTRA_SUBJECT, ERROR_EMAIL_SUBJECT
+ getString(R.string.app_name) + " "
+ BuildConfig.VERSION_NAME)
.putExtra(Intent.EXTRA_TEXT, buildJson());
ShareUtils.openIntentInApp(context, i);
} else if (action.equals("GITHUB")) { // open the NewPipe issue page on GitHub
ShareUtils.openUrlInApp(this, ERROR_GITHUB_ISSUE_URL);
}
})
.setNegativeButton(R.string.decline, null)
.show();
}
private String formErrorText(final String[] el) {
final String separator = "-------------------------------------";
return Arrays.stream(el)
.collect(Collectors.joining(separator + "\n", separator + "\n", separator));
}
private void buildInfo(final ErrorInfo info) {
String text = "";
activityErrorBinding.errorInfoLabelsView.setText(getString(R.string.info_labels)
.replace("\\n", "\n"));
text += getUserActionString(info.getUserAction()) + "\n"
+ info.getRequest() + "\n"
+ getContentLanguageString() + "\n"
+ getContentCountryString() + "\n"
+ getAppLanguage() + "\n"
+ info.getServiceName() + "\n"
+ currentTimeStamp + "\n"
+ getPackageName() + "\n"
+ BuildConfig.VERSION_NAME + "\n"
+ getOsString();
activityErrorBinding.errorInfosView.setText(text);
}
private String buildJson() {
try {
return JsonWriter.string()
.object()
.value("user_action", getUserActionString(errorInfo.getUserAction()))
.value("request", errorInfo.getRequest())
.value("content_language", getContentLanguageString())
.value("content_country", getContentCountryString())
.value("app_language", getAppLanguage())
.value("service", errorInfo.getServiceName())
.value("package", getPackageName())
.value("version", BuildConfig.VERSION_NAME)
.value("os", getOsString())
.value("time", currentTimeStamp)
.array("exceptions", Arrays.asList(errorInfo.getStackTraces()))
.value("user_comment", activityErrorBinding.errorCommentBox.getText()
.toString())
.end()
.done();
} catch (final Throwable e) {
Log.e(TAG, "Error while erroring: Could not build json");
e.printStackTrace();
}
return "";
}
private String buildMarkdown() {
try {
final StringBuilder htmlErrorReport = new StringBuilder();
final String userComment = activityErrorBinding.errorCommentBox.getText().toString();
if (!userComment.isEmpty()) {
htmlErrorReport.append(userComment).append("\n");
}
// basic error info
htmlErrorReport
.append("## Exception")
.append("\n* __User Action:__ ")
.append(getUserActionString(errorInfo.getUserAction()))
.append("\n* __Request:__ ").append(errorInfo.getRequest())
.append("\n* __Content Country:__ ").append(getContentCountryString())
.append("\n* __Content Language:__ ").append(getContentLanguageString())
.append("\n* __App Language:__ ").append(getAppLanguage())
.append("\n* __Service:__ ").append(errorInfo.getServiceName())
.append("\n* __Timestamp:__ ").append(currentTimeStamp)
.append("\n* __Package:__ ").append(getPackageName())
.append("\n* __Service:__ ").append(errorInfo.getServiceName())
.append("\n* __Version:__ ").append(BuildConfig.VERSION_NAME)
.append("\n* __OS:__ ").append(getOsString()).append("\n");
// Collapse all logs to a single paragraph when there are more than one
// to keep the GitHub issue clean.
if (errorInfo.getStackTraces().length > 1) {
htmlErrorReport
.append("<details><summary><b>Exceptions (")
.append(errorInfo.getStackTraces().length)
.append(")</b></summary><p>\n");
}
// add the logs
for (int i = 0; i < errorInfo.getStackTraces().length; i++) {
htmlErrorReport.append("<details><summary><b>Crash log ");
if (errorInfo.getStackTraces().length > 1) {
htmlErrorReport.append(i + 1);
}
htmlErrorReport.append("</b>")
.append("</summary><p>\n")
.append("\n```\n").append(errorInfo.getStackTraces()[i]).append("\n```\n")
.append("</details>\n");
}
// make sure to close everything
if (errorInfo.getStackTraces().length > 1) {
htmlErrorReport.append("</p></details>\n");
}
htmlErrorReport.append("<hr>\n");
return htmlErrorReport.toString();
} catch (final Throwable e) {
Log.e(TAG, "Error while erroring: Could not build markdown");
e.printStackTrace();
return "";
}
}
private String getUserActionString(final UserAction userAction) {
if (userAction == null) {
return "Your description is in another castle.";
} else {
return userAction.getMessage();
}
}
private String getContentCountryString() {
return Localization.getPreferredContentCountry(this).getCountryCode();
}
private String getContentLanguageString() {
return Localization.getPreferredLocalization(this).getLocalizationCode();
}
private String getAppLanguage() {
return Localization.getAppLocale().toString();
}
private String getOsString() {
final String osBase = Build.VERSION.SDK_INT >= Build.VERSION_CODES.M
? Build.VERSION.BASE_OS : "Android";
return System.getProperty("os.name")
+ " " + (osBase.isEmpty() ? "Android" : osBase)
+ " " + Build.VERSION.RELEASE
+ " - " + Build.VERSION.SDK_INT;
}
private void addGuruMeditation() {
//just an easter egg
String text = activityErrorBinding.errorSorryView.getText().toString();
text += "\n" + getString(R.string.guru_meditation);
activityErrorBinding.errorSorryView.setText(text);
}
}

View File

@@ -0,0 +1,282 @@
/*
* SPDX-FileCopyrightText: 2015-2026 NewPipe contributors <https://newpipe.net>
* SPDX-License-Identifier: GPL-3.0-or-later
*/
package org.schabi.newpipe.error
import android.content.Context
import android.content.Intent
import android.os.Build
import android.os.Bundle
import android.util.Log
import android.view.Menu
import android.view.MenuItem
import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.IntentCompat
import androidx.core.net.toUri
import com.grack.nanojson.JsonWriter
import java.time.ZonedDateTime
import java.time.format.DateTimeFormatter
import org.schabi.newpipe.BuildConfig
import org.schabi.newpipe.R
import org.schabi.newpipe.databinding.ActivityErrorBinding
import org.schabi.newpipe.util.Localization
import org.schabi.newpipe.util.ThemeHelper
import org.schabi.newpipe.util.external_communication.ShareUtils
import org.schabi.newpipe.util.text.setTextWithLinks
/**
* This activity is used to show error details and allow reporting them in various ways.
* Use [ErrorUtil.openActivity] to correctly open this activity.
*/
class ErrorActivity : AppCompatActivity() {
private lateinit var errorInfo: ErrorInfo
private lateinit var currentTimeStamp: String
private lateinit var binding: ActivityErrorBinding
private val contentCountryString: String
get() = Localization.getPreferredContentCountry(this).countryCode
private val contentLanguageString: String
get() = Localization.getPreferredLocalization(this).localizationCode
private val appLanguage: String
get() = Localization.getAppLocale().toString()
private val osString: String
get() {
val name = System.getProperty("os.name")!!
val osBase = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
Build.VERSION.BASE_OS.ifEmpty { "Android" }
} else {
"Android"
}
return "$name $osBase ${Build.VERSION.RELEASE} - ${Build.VERSION.SDK_INT}"
}
private val errorEmailSubject: String
get() = "$ERROR_EMAIL_SUBJECT ${getString(R.string.app_name)} ${BuildConfig.VERSION_NAME}"
// /////////////////////////////////////////////////////////////////////
// Activity lifecycle
// /////////////////////////////////////////////////////////////////////
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
ThemeHelper.setDayNightMode(this)
ThemeHelper.setTheme(this)
binding = ActivityErrorBinding.inflate(layoutInflater)
setContentView(binding.getRoot())
setSupportActionBar(binding.toolbarLayout.toolbar)
supportActionBar?.apply {
setDisplayHomeAsUpEnabled(true)
setTitle(R.string.error_report_title)
setDisplayShowTitleEnabled(true)
}
errorInfo = IntentCompat.getParcelableExtra(intent, ERROR_INFO, ErrorInfo::class.java)!!
// important add guru meditation
addGuruMeditation()
// print current time, as zoned ISO8601 timestamp
currentTimeStamp = ZonedDateTime.now().format(DateTimeFormatter.ISO_OFFSET_DATE_TIME)
binding.errorReportEmailButton.setOnClickListener { _ ->
openPrivacyPolicyDialog(this, "EMAIL")
}
binding.errorReportCopyButton.setOnClickListener { _ ->
ShareUtils.copyToClipboard(this, buildMarkdown())
}
binding.errorReportGitHubButton.setOnClickListener { _ ->
openPrivacyPolicyDialog(this, "GITHUB")
}
// normal bugreport
buildInfo(errorInfo)
binding.errorMessageView.setTextWithLinks(errorInfo.getMessage(this))
binding.errorView.text = formErrorText(errorInfo.stackTraces)
// print stack trace once again for debugging:
errorInfo.stackTraces.forEach { Log.e(TAG, it) }
}
override fun onCreateOptionsMenu(menu: Menu): Boolean {
menuInflater.inflate(R.menu.error_menu, menu)
return true
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
return when (item.itemId) {
android.R.id.home -> {
onBackPressed()
true
}
R.id.menu_item_share_error -> {
ShareUtils.shareText(
applicationContext,
getString(R.string.error_report_title),
buildJson()
)
true
}
else -> false
}
}
private fun openPrivacyPolicyDialog(context: Context, action: String) {
AlertDialog.Builder(context)
.setIcon(android.R.drawable.ic_dialog_alert)
.setTitle(R.string.privacy_policy_title)
.setMessage(R.string.start_accept_privacy_policy)
.setCancelable(false)
.setNeutralButton(R.string.read_privacy_policy) { _, _ ->
ShareUtils.openUrlInApp(context, context.getString(R.string.privacy_policy_url))
}
.setPositiveButton(R.string.accept) { _, _ ->
if (action == "EMAIL") { // send on email
val intent = Intent(Intent.ACTION_SENDTO)
.setData("mailto:".toUri()) // only email apps should handle this
.putExtra(Intent.EXTRA_EMAIL, arrayOf(ERROR_EMAIL_ADDRESS))
.putExtra(Intent.EXTRA_SUBJECT, errorEmailSubject)
.putExtra(Intent.EXTRA_TEXT, buildJson())
ShareUtils.openIntentInApp(context, intent)
} else if (action == "GITHUB") { // open the NewPipe issue page on GitHub
ShareUtils.openUrlInApp(this, ERROR_GITHUB_ISSUE_URL)
}
}
.setNegativeButton(R.string.decline, null)
.show()
}
private fun formErrorText(stacktrace: Array<String>): String {
val separator = "-------------------------------------"
return stacktrace.joinToString(separator + "\n", separator + "\n", separator)
}
private fun buildInfo(info: ErrorInfo) {
binding.errorInfoLabelsView.text = getString(R.string.info_labels)
val text = info.userAction.message + "\n" +
info.request + "\n" +
contentLanguageString + "\n" +
contentCountryString + "\n" +
appLanguage + "\n" +
info.getServiceName() + "\n" +
currentTimeStamp + "\n" +
packageName + "\n" +
BuildConfig.VERSION_NAME + "\n" +
osString
binding.errorInfosView.text = text
}
private fun buildJson(): String {
try {
return JsonWriter.string()
.`object`()
.value("user_action", errorInfo.userAction.message)
.value("request", errorInfo.request)
.value("content_language", contentLanguageString)
.value("content_country", contentCountryString)
.value("app_language", appLanguage)
.value("service", errorInfo.getServiceName())
.value("package", packageName)
.value("version", BuildConfig.VERSION_NAME)
.value("os", osString)
.value("time", currentTimeStamp)
.array("exceptions", errorInfo.stackTraces.toList())
.value("user_comment", binding.errorCommentBox.getText().toString())
.end()
.done()
} catch (exception: Exception) {
Log.e(TAG, "Error while erroring: Could not build json", exception)
}
return ""
}
private fun buildMarkdown(): String {
try {
return buildString(1024) {
val userComment = binding.errorCommentBox.text.toString()
if (userComment.isNotEmpty()) {
appendLine(userComment)
}
// basic error info
appendLine("## Exception")
appendLine("* __User Action:__ ${errorInfo.userAction.message}")
appendLine("* __Request:__ ${errorInfo.request}")
appendLine("* __Content Country:__ $contentCountryString")
appendLine("* __Content Language:__ $contentLanguageString")
appendLine("* __App Language:__ $appLanguage")
appendLine("* __Service:__ ${errorInfo.getServiceName()}")
appendLine("* __Timestamp:__ $currentTimeStamp")
appendLine("* __Package:__ $packageName")
appendLine("* __Service:__ ${errorInfo.getServiceName()}")
appendLine("* __Version:__ ${BuildConfig.VERSION_NAME}")
appendLine("* __OS:__ $osString")
// Collapse all logs to a single paragraph when there are more than one
// to keep the GitHub issue clean.
if (errorInfo.stackTraces.size > 1) {
append("<details><summary><b>Exceptions (")
append(errorInfo.stackTraces.size)
append(")</b></summary><p>\n")
}
// add the logs
errorInfo.stackTraces.forEachIndexed { index, stacktrace ->
append("<details><summary><b>Crash log ")
if (errorInfo.stackTraces.size > 1) {
append(index + 1)
}
append("</b>")
append("</summary><p>\n")
append("\n```\n${stacktrace}\n```\n")
append("</details>\n")
}
// make sure to close everything
if (errorInfo.stackTraces.size > 1) {
append("</p></details>\n")
}
append("<hr>\n")
}
} catch (exception: Exception) {
Log.e(TAG, "Error while erroring: Could not build markdown", exception)
return ""
}
}
private fun addGuruMeditation() {
// just an easter egg
var text = binding.errorSorryView.text.toString()
text += "\n" + getString(R.string.guru_meditation)
binding.errorSorryView.text = text
}
companion object {
// LOG TAGS
private val TAG = ErrorActivity::class.java.toString()
// BUNDLE TAGS
const val ERROR_INFO = "error_info"
private const val ERROR_EMAIL_ADDRESS = "crashreport@newpipe.schabi.org"
private const val ERROR_EMAIL_SUBJECT = "Exception in "
private const val ERROR_GITHUB_ISSUE_URL = "https://github.com/TeamNewPipe/NewPipe/issues"
}
}

View File

@@ -29,6 +29,7 @@ import org.schabi.newpipe.extractor.exceptions.YoutubeMusicPremiumContentExcepti
import org.schabi.newpipe.ktx.isNetworkRelated
import org.schabi.newpipe.player.mediasource.FailedMediaSource
import org.schabi.newpipe.player.resolver.PlaybackResolver
import org.schabi.newpipe.util.text.getText
/**
* An error has occurred in the app. This class contains plain old parcelable data that can be used
@@ -135,8 +136,8 @@ class ErrorInfo private constructor(
return getServiceName(serviceId)
}
fun getMessage(context: Context): String {
return message.getString(context)
fun getMessage(context: Context): CharSequence {
return message.getText(context)
}
companion object {
@@ -146,20 +147,23 @@ class ErrorInfo private constructor(
private val stringRes: Int,
private vararg val formatArgs: String
) : Parcelable {
fun getString(context: Context): String {
fun getText(context: Context): CharSequence {
// Ensure locale aware context via ContextCompat.getContextForLanguage() (just in case context is not AppCompatActivity)
val ctx = ContextCompat.getContextForLanguage(context)
return if (formatArgs.isEmpty()) {
// use ContextCompat.getString() just in case context is not AppCompatActivity
ContextCompat.getString(context, stringRes)
ctx.getText(stringRes)
} else {
// ContextCompat.getString() with formatArgs does not exist, so we just
// replicate its source code but with formatArgs
ContextCompat.getContextForLanguage(context).getString(stringRes, *formatArgs)
ctx.resources.getText(stringRes, *formatArgs)
}
}
}
const val SERVICE_NONE = "<unknown_service>"
const val YOUTUBE_IP_BAN_FAQ_URL = "https://newpipe.net/FAQ/#ip-banned-youtube"
private fun getServiceName(serviceId: Int?) = // not using getNameOfServiceById since we want to accept a nullable serviceId and we
// want to default to SERVICE_NONE
ServiceList.all().firstOrNull { it.serviceId == serviceId }?.serviceInfo?.name
@@ -247,7 +251,11 @@ class ErrorInfo private constructor(
ErrorMessage(R.string.youtube_music_premium_content)
throwable is SignInConfirmNotBotException ->
ErrorMessage(R.string.sign_in_confirm_not_bot_error, getServiceName(serviceId))
ErrorMessage(
R.string.sign_in_confirm_not_bot_error,
getServiceName(serviceId),
YOUTUBE_IP_BAN_FAQ_URL
)
throwable is ContentNotAvailableException ->
ErrorMessage(R.string.content_not_available)

View File

@@ -16,6 +16,7 @@ import org.schabi.newpipe.MainActivity
import org.schabi.newpipe.R
import org.schabi.newpipe.ktx.animate
import org.schabi.newpipe.util.external_communication.ShareUtils
import org.schabi.newpipe.util.text.setTextWithLinks
class ErrorPanelHelper(
private val fragment: Fragment,
@@ -64,7 +65,7 @@ class ErrorPanelHelper(
fun showError(errorInfo: ErrorInfo) {
ensureDefaultVisibility()
errorTextView.text = errorInfo.getMessage(context)
errorTextView.setTextWithLinks(errorInfo.getMessage(context))
if (errorInfo.recaptchaUrl != null) {
showAndSetErrorButtonAction(R.string.recaptcha_solve) {
@@ -109,7 +110,7 @@ class ErrorPanelHelper(
fun showTextError(errorString: String) {
ensureDefaultVisibility()
errorTextView.text = errorString
errorTextView.setTextWithLinks(errorString)
setRootVisible()
}

View File

@@ -134,8 +134,11 @@ class ErrorUtil {
)
)
NotificationManagerCompat.from(context)
.notify(ERROR_REPORT_NOTIFICATION_ID, notificationBuilder.build())
val notificationManager = NotificationManagerCompat.from(context)
if (notificationManager.areNotificationsEnabled()) {
notificationManager
.notify(ERROR_REPORT_NOTIFICATION_ID, notificationBuilder.build())
}
ContextCompat.getMainExecutor(context).execute {
// since the notification is silent, also show a toast, otherwise the user is confused

View File

@@ -126,6 +126,7 @@ public class ReCaptchaActivity extends AppCompatActivity {
}
@Override
@SuppressLint("MissingSuperCall") // saveCookiesAndFinish method handles back navigation
public void onBackPressed() {
saveCookiesAndFinish();
}

View File

@@ -216,9 +216,9 @@ public abstract class BaseDescriptionFragment extends BaseFragment {
|| image.getWidth() != Image.WIDTH_UNKNOWN
// if even the resolution level is unknown, ?x? will be shown
|| image.getEstimatedResolutionLevel() == Image.ResolutionLevel.UNKNOWN) {
urls.append(imageSizeToText(image.getHeight()));
urls.append('x');
urls.append(imageSizeToText(image.getWidth()));
urls.append('x');
urls.append(imageSizeToText(image.getHeight()));
} else {
switch (image.getEstimatedResolutionLevel()) {
case LOW -> urls.append(getString(R.string.image_quality_low));

View File

@@ -118,7 +118,7 @@ import org.schabi.newpipe.util.StreamTypeUtil;
import org.schabi.newpipe.util.ThemeHelper;
import org.schabi.newpipe.util.external_communication.KoreUtils;
import org.schabi.newpipe.util.external_communication.ShareUtils;
import org.schabi.newpipe.util.image.PicassoHelper;
import org.schabi.newpipe.util.image.CoilHelper;
import java.util.ArrayList;
import java.util.Iterator;
@@ -129,6 +129,7 @@ import java.util.Optional;
import java.util.concurrent.TimeUnit;
import java.util.function.Consumer;
import coil3.util.CoilUtils;
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
import io.reactivex.rxjava3.disposables.CompositeDisposable;
import io.reactivex.rxjava3.disposables.Disposable;
@@ -160,8 +161,6 @@ public final class VideoDetailFragment
private static final String DESCRIPTION_TAB_TAG = "DESCRIPTION TAB";
private static final String EMPTY_TAB_TAG = "EMPTY TAB";
private static final String PICASSO_VIDEO_DETAILS_TAG = "PICASSO_VIDEO_DETAILS_TAG";
// tabs
private boolean showComments;
private boolean showRelatedItems;
@@ -646,6 +645,12 @@ public final class VideoDetailFragment
protected void initListeners() {
super.initListeners();
// Workaround for #5600
// Forcefully catch click events uncaught by children because otherwise
// they will be caught by underlying view and "click through" will happen
binding.getRoot().setOnClickListener(v -> { });
binding.getRoot().setOnLongClickListener(v -> true);
setOnClickListeners();
setOnLongClickListeners();
@@ -1493,7 +1498,10 @@ public final class VideoDetailFragment
}
}
PicassoHelper.cancelTag(PICASSO_VIDEO_DETAILS_TAG);
CoilUtils.dispose(binding.detailThumbnailImageView);
CoilUtils.dispose(binding.detailSubChannelThumbnailView);
CoilUtils.dispose(binding.overlayThumbnail);
CoilUtils.dispose(binding.detailUploaderThumbnailView);
binding.detailThumbnailImageView.setImageBitmap(null);
binding.detailSubChannelThumbnailView.setImageBitmap(null);
}
@@ -1584,8 +1592,8 @@ public final class VideoDetailFragment
binding.detailSecondaryControlPanel.setVisibility(View.GONE);
checkUpdateProgressInfo(info);
PicassoHelper.loadDetailsThumbnail(info.getThumbnails()).tag(PICASSO_VIDEO_DETAILS_TAG)
.into(binding.detailThumbnailImageView);
CoilHelper.INSTANCE.loadDetailsThumbnail(binding.detailThumbnailImageView,
info.getThumbnails());
showMetaInfoInTextView(info.getMetaInfo(), binding.detailMetaInfoTextView,
binding.detailMetaInfoSeparator, disposables);
@@ -1635,8 +1643,8 @@ public final class VideoDetailFragment
binding.detailUploaderTextView.setVisibility(View.GONE);
}
PicassoHelper.loadAvatar(info.getUploaderAvatars()).tag(PICASSO_VIDEO_DETAILS_TAG)
.into(binding.detailSubChannelThumbnailView);
CoilHelper.INSTANCE.loadAvatar(binding.detailSubChannelThumbnailView,
info.getUploaderAvatars());
binding.detailSubChannelThumbnailView.setVisibility(View.VISIBLE);
binding.detailUploaderThumbnailView.setVisibility(View.GONE);
}
@@ -1667,11 +1675,11 @@ public final class VideoDetailFragment
binding.detailUploaderTextView.setVisibility(View.GONE);
}
PicassoHelper.loadAvatar(info.getSubChannelAvatars()).tag(PICASSO_VIDEO_DETAILS_TAG)
.into(binding.detailSubChannelThumbnailView);
CoilHelper.INSTANCE.loadAvatar(binding.detailSubChannelThumbnailView,
info.getSubChannelAvatars());
binding.detailSubChannelThumbnailView.setVisibility(View.VISIBLE);
PicassoHelper.loadAvatar(info.getUploaderAvatars()).tag(PICASSO_VIDEO_DETAILS_TAG)
.into(binding.detailUploaderThumbnailView);
CoilHelper.INSTANCE.loadAvatar(binding.detailUploaderThumbnailView,
info.getUploaderAvatars());
binding.detailUploaderThumbnailView.setVisibility(View.VISIBLE);
}
@@ -1899,7 +1907,11 @@ public final class VideoDetailFragment
}
if (binding.relatedItemsLayout != null) {
binding.relatedItemsLayout.setVisibility(fullscreen ? View.GONE : View.VISIBLE);
if (showRelatedItems) {
binding.relatedItemsLayout.setVisibility(fullscreen ? View.GONE : View.VISIBLE);
} else {
binding.relatedItemsLayout.setVisibility(View.GONE);
}
}
scrollToTop();
@@ -2429,8 +2441,7 @@ public final class VideoDetailFragment
binding.overlayTitleTextView.setText(isEmpty(overlayTitle) ? "" : overlayTitle);
binding.overlayChannelTextView.setText(isEmpty(uploader) ? "" : uploader);
binding.overlayThumbnail.setImageDrawable(null);
PicassoHelper.loadDetailsThumbnail(thumbnails).tag(PICASSO_VIDEO_DETAILS_TAG)
.into(binding.overlayThumbnail);
CoilHelper.INSTANCE.loadDetailsThumbnail(binding.overlayThumbnail, thumbnails);
}
private void setOverlayPlayPauseImage(final boolean playerIsPlaying) {

View File

@@ -53,13 +53,14 @@ import org.schabi.newpipe.util.NavigationHelper;
import org.schabi.newpipe.util.StateSaver;
import org.schabi.newpipe.util.ThemeHelper;
import org.schabi.newpipe.util.external_communication.ShareUtils;
import org.schabi.newpipe.util.image.CoilHelper;
import org.schabi.newpipe.util.image.ImageStrategy;
import org.schabi.newpipe.util.image.PicassoHelper;
import java.util.List;
import java.util.Queue;
import java.util.concurrent.TimeUnit;
import coil3.util.CoilUtils;
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
import io.reactivex.rxjava3.core.Observable;
import io.reactivex.rxjava3.disposables.CompositeDisposable;
@@ -73,7 +74,6 @@ public class ChannelFragment extends BaseStateFragment<ChannelInfo>
implements StateSaver.WriteRead {
private static final int BUTTON_DEBOUNCE_INTERVAL = 100;
private static final String PICASSO_CHANNEL_TAG = "PICASSO_CHANNEL_TAG";
@State
protected int serviceId = Constants.NO_SERVICE_ID;
@@ -160,34 +160,29 @@ public class ChannelFragment extends BaseStateFragment<ChannelInfo>
@Override
public boolean onMenuItemSelected(@NonNull final MenuItem item) {
switch (item.getItemId()) {
case R.id.menu_item_notify:
final boolean value = !item.isChecked();
item.setEnabled(false);
setNotify(value);
break;
case R.id.action_settings:
NavigationHelper.openSettings(requireContext());
break;
case R.id.menu_item_rss:
if (currentInfo != null) {
ShareUtils.openUrlInApp(requireContext(), currentInfo.getFeedUrl());
}
break;
case R.id.menu_item_openInBrowser:
if (currentInfo != null) {
ShareUtils.openUrlInBrowser(requireContext(),
currentInfo.getOriginalUrl());
}
break;
case R.id.menu_item_share:
if (currentInfo != null) {
ShareUtils.shareText(requireContext(), name,
currentInfo.getOriginalUrl(), currentInfo.getAvatars());
}
break;
default:
return false;
final int itemId = item.getItemId();
if (itemId == R.id.menu_item_notify) {
final boolean value = !item.isChecked();
item.setEnabled(false);
setNotify(value);
} else if (itemId == R.id.action_settings) {
NavigationHelper.openSettings(requireContext());
} else if (itemId == R.id.menu_item_rss) {
if (currentInfo != null) {
ShareUtils.openUrlInApp(requireContext(), currentInfo.getFeedUrl());
}
} else if (itemId == R.id.menu_item_openInBrowser) {
if (currentInfo != null) {
ShareUtils.openUrlInBrowser(requireContext(),
currentInfo.getOriginalUrl());
}
} else if (itemId == R.id.menu_item_share) {
if (currentInfo != null) {
ShareUtils.shareText(requireContext(), name,
currentInfo.getOriginalUrl(), currentInfo.getAvatars());
}
} else {
return false;
}
return true;
}
@@ -583,7 +578,9 @@ public class ChannelFragment extends BaseStateFragment<ChannelInfo>
@Override
public void showLoading() {
super.showLoading();
PicassoHelper.cancelTag(PICASSO_CHANNEL_TAG);
CoilUtils.dispose(binding.channelAvatarView);
CoilUtils.dispose(binding.channelBannerImage);
CoilUtils.dispose(binding.subChannelAvatarView);
animate(binding.channelSubscribeButton, false, 100);
}
@@ -594,17 +591,15 @@ public class ChannelFragment extends BaseStateFragment<ChannelInfo>
setInitialData(result.getServiceId(), result.getOriginalUrl(), result.getName());
if (ImageStrategy.shouldLoadImages() && !result.getBanners().isEmpty()) {
PicassoHelper.loadBanner(result.getBanners()).tag(PICASSO_CHANNEL_TAG)
.into(binding.channelBannerImage);
CoilHelper.INSTANCE.loadBanner(binding.channelBannerImage, result.getBanners());
} else {
// do not waste space for the banner, if the user disabled images or there is not one
binding.channelBannerImage.setImageDrawable(null);
}
PicassoHelper.loadAvatar(result.getAvatars()).tag(PICASSO_CHANNEL_TAG)
.into(binding.channelAvatarView);
PicassoHelper.loadAvatar(result.getParentChannelAvatars()).tag(PICASSO_CHANNEL_TAG)
.into(binding.subChannelAvatarView);
CoilHelper.INSTANCE.loadAvatar(binding.channelAvatarView, result.getAvatars());
CoilHelper.INSTANCE.loadAvatar(binding.subChannelAvatarView,
result.getParentChannelAvatars());
binding.channelTitleView.setText(result.getName());
binding.channelSubscriberView.setVisibility(View.VISIBLE);

View File

@@ -25,8 +25,8 @@ import org.schabi.newpipe.util.DeviceUtils;
import org.schabi.newpipe.util.ExtractorHelper;
import org.schabi.newpipe.util.Localization;
import org.schabi.newpipe.util.NavigationHelper;
import org.schabi.newpipe.util.image.CoilHelper;
import org.schabi.newpipe.util.image.ImageStrategy;
import org.schabi.newpipe.util.image.PicassoHelper;
import org.schabi.newpipe.util.text.TextLinkifier;
import org.schabi.newpipe.util.text.LongPressLinkMovementMethod;
@@ -84,7 +84,7 @@ public final class CommentRepliesFragment
final CommentsInfoItem item = commentsInfoItem;
// load the author avatar
PicassoHelper.loadAvatar(item.getUploaderAvatars()).into(binding.authorAvatar);
CoilHelper.INSTANCE.loadAvatar(binding.authorAvatar, item.getUploaderAvatars());
binding.authorAvatar.setVisibility(ImageStrategy.shouldLoadImages()
? View.VISIBLE : View.GONE);

View File

@@ -53,7 +53,7 @@ import org.schabi.newpipe.util.Localization;
import org.schabi.newpipe.util.NavigationHelper;
import org.schabi.newpipe.util.PlayButtonHelper;
import org.schabi.newpipe.util.external_communication.ShareUtils;
import org.schabi.newpipe.util.image.PicassoHelper;
import org.schabi.newpipe.util.image.CoilHelper;
import org.schabi.newpipe.util.text.TextEllipsizer;
import java.util.ArrayList;
@@ -62,6 +62,7 @@ import java.util.concurrent.atomic.AtomicBoolean;
import java.util.function.Supplier;
import java.util.stream.Collectors;
import coil3.util.CoilUtils;
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
import io.reactivex.rxjava3.core.Flowable;
import io.reactivex.rxjava3.core.Single;
@@ -71,8 +72,6 @@ import io.reactivex.rxjava3.disposables.Disposable;
public class PlaylistFragment extends BaseListInfoFragment<StreamInfoItem, PlaylistInfo>
implements PlaylistControlViewHolder {
private static final String PICASSO_PLAYLIST_TAG = "PICASSO_PLAYLIST_TAG";
private CompositeDisposable disposables;
private Subscription bookmarkReactor;
private AtomicBoolean isBookmarkButtonReady;
@@ -232,35 +231,30 @@ public class PlaylistFragment extends BaseListInfoFragment<StreamInfoItem, Playl
@Override
public boolean onOptionsItemSelected(final MenuItem item) {
switch (item.getItemId()) {
case R.id.action_settings:
NavigationHelper.openSettings(requireContext());
break;
case R.id.menu_item_openInBrowser:
ShareUtils.openUrlInBrowser(requireContext(), url);
break;
case R.id.menu_item_share:
ShareUtils.shareText(requireContext(), name, url,
currentInfo == null ? List.of() : currentInfo.getThumbnails());
break;
case R.id.menu_item_bookmark:
onBookmarkClicked();
break;
case R.id.menu_item_append_playlist:
if (currentInfo != null) {
disposables.add(PlaylistDialog.createCorrespondingDialog(
getContext(),
getPlayQueue()
.getStreams()
.stream()
.map(StreamEntity::new)
.collect(Collectors.toList()),
dialog -> dialog.show(getFM(), TAG)
));
}
break;
default:
return super.onOptionsItemSelected(item);
final int itemId = item.getItemId();
if (itemId == R.id.action_settings) {
NavigationHelper.openSettings(requireContext());
} else if (itemId == R.id.menu_item_openInBrowser) {
ShareUtils.openUrlInBrowser(requireContext(), url);
} else if (itemId == R.id.menu_item_share) {
ShareUtils.shareText(requireContext(), name, url,
currentInfo == null ? List.of() : currentInfo.getThumbnails());
} else if (itemId == R.id.menu_item_bookmark) {
onBookmarkClicked();
} else if (itemId == R.id.menu_item_append_playlist) {
if (currentInfo != null) {
disposables.add(PlaylistDialog.createCorrespondingDialog(
getContext(),
getPlayQueue()
.getStreams()
.stream()
.map(StreamEntity::new)
.collect(Collectors.toList()),
dialog -> dialog.show(getFM(), TAG)
));
}
} else {
return super.onOptionsItemSelected(item);
}
return true;
}
@@ -276,7 +270,7 @@ public class PlaylistFragment extends BaseListInfoFragment<StreamInfoItem, Playl
animate(headerBinding.getRoot(), false, 200);
animateHideRecyclerViewAllowingScrolling(itemsList);
PicassoHelper.cancelTag(PICASSO_PLAYLIST_TAG);
CoilUtils.dispose(headerBinding.uploaderAvatarView);
animate(headerBinding.uploaderLayout, false, 200);
}
@@ -327,8 +321,8 @@ public class PlaylistFragment extends BaseListInfoFragment<StreamInfoItem, Playl
R.drawable.ic_radio)
);
} else {
PicassoHelper.loadAvatar(result.getUploaderAvatars()).tag(PICASSO_PLAYLIST_TAG)
.into(headerBinding.uploaderAvatarView);
CoilHelper.INSTANCE.loadAvatar(headerBinding.uploaderAvatarView,
result.getUploaderAvatars());
}
streamCount = result.getStreamCount();

View File

@@ -1009,7 +1009,11 @@ public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.I
Log.d(TAG, "handleSuggestions() called with: suggestions = [" + suggestions + "]");
}
suggestionListAdapter.submitList(suggestions,
() -> searchBinding.suggestionsList.scrollToPosition(0));
() -> {
if (searchBinding != null) {
searchBinding.suggestionsList.scrollToPosition(0);
}
});
if (suggestionsPanelVisible && isErrorPanelVisible()) {
hideLoading();

View File

@@ -1,131 +0,0 @@
package org.schabi.newpipe.info_list;
import android.content.Context;
import android.view.View;
import android.view.ViewGroup;
import androidx.annotation.NonNull;
import org.schabi.newpipe.extractor.InfoItem;
import org.schabi.newpipe.extractor.channel.ChannelInfoItem;
import org.schabi.newpipe.extractor.comments.CommentsInfoItem;
import org.schabi.newpipe.extractor.playlist.PlaylistInfoItem;
import org.schabi.newpipe.extractor.stream.StreamInfoItem;
import org.schabi.newpipe.info_list.holder.ChannelInfoItemHolder;
import org.schabi.newpipe.info_list.holder.ChannelMiniInfoItemHolder;
import org.schabi.newpipe.info_list.holder.CommentInfoItemHolder;
import org.schabi.newpipe.info_list.holder.InfoItemHolder;
import org.schabi.newpipe.info_list.holder.PlaylistInfoItemHolder;
import org.schabi.newpipe.info_list.holder.PlaylistMiniInfoItemHolder;
import org.schabi.newpipe.info_list.holder.StreamInfoItemHolder;
import org.schabi.newpipe.info_list.holder.StreamMiniInfoItemHolder;
import org.schabi.newpipe.local.history.HistoryRecordManager;
import org.schabi.newpipe.util.OnClickGesture;
/*
* Created by Christian Schabesberger on 26.09.16.
* <p>
* Copyright (C) Christian Schabesberger 2016 <chris.schabesberger@mailbox.org>
* InfoItemBuilder.java is part of NewPipe.
* </p>
* <p>
* NewPipe is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
* </p>
* <p>
* NewPipe is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
* </p>
* <p>
* You should have received a copy of the GNU General Public License
* along with NewPipe. If not, see <http://www.gnu.org/licenses/>.
* </p>
*/
public class InfoItemBuilder {
private final Context context;
private OnClickGesture<StreamInfoItem> onStreamSelectedListener;
private OnClickGesture<ChannelInfoItem> onChannelSelectedListener;
private OnClickGesture<PlaylistInfoItem> onPlaylistSelectedListener;
private OnClickGesture<CommentsInfoItem> onCommentsSelectedListener;
public InfoItemBuilder(final Context context) {
this.context = context;
}
public View buildView(@NonNull final ViewGroup parent, @NonNull final InfoItem infoItem,
final HistoryRecordManager historyRecordManager) {
return buildView(parent, infoItem, historyRecordManager, false);
}
public View buildView(@NonNull final ViewGroup parent, @NonNull final InfoItem infoItem,
final HistoryRecordManager historyRecordManager,
final boolean useMiniVariant) {
final InfoItemHolder holder =
holderFromInfoType(parent, infoItem.getInfoType(), useMiniVariant);
holder.updateFromItem(infoItem, historyRecordManager);
return holder.itemView;
}
private InfoItemHolder holderFromInfoType(@NonNull final ViewGroup parent,
@NonNull final InfoItem.InfoType infoType,
final boolean useMiniVariant) {
switch (infoType) {
case STREAM:
return useMiniVariant ? new StreamMiniInfoItemHolder(this, parent)
: new StreamInfoItemHolder(this, parent);
case CHANNEL:
return useMiniVariant ? new ChannelMiniInfoItemHolder(this, parent)
: new ChannelInfoItemHolder(this, parent);
case PLAYLIST:
return useMiniVariant ? new PlaylistMiniInfoItemHolder(this, parent)
: new PlaylistInfoItemHolder(this, parent);
case COMMENT:
return new CommentInfoItemHolder(this, parent);
default:
throw new RuntimeException("InfoType not expected = " + infoType.name());
}
}
public Context getContext() {
return context;
}
public OnClickGesture<StreamInfoItem> getOnStreamSelectedListener() {
return onStreamSelectedListener;
}
public void setOnStreamSelectedListener(final OnClickGesture<StreamInfoItem> listener) {
this.onStreamSelectedListener = listener;
}
public OnClickGesture<ChannelInfoItem> getOnChannelSelectedListener() {
return onChannelSelectedListener;
}
public void setOnChannelSelectedListener(final OnClickGesture<ChannelInfoItem> listener) {
this.onChannelSelectedListener = listener;
}
public OnClickGesture<PlaylistInfoItem> getOnPlaylistSelectedListener() {
return onPlaylistSelectedListener;
}
public void setOnPlaylistSelectedListener(final OnClickGesture<PlaylistInfoItem> listener) {
this.onPlaylistSelectedListener = listener;
}
public OnClickGesture<CommentsInfoItem> getOnCommentsSelectedListener() {
return onCommentsSelectedListener;
}
public void setOnCommentsSelectedListener(
final OnClickGesture<CommentsInfoItem> onCommentsSelectedListener) {
this.onCommentsSelectedListener = onCommentsSelectedListener;
}
}

View File

@@ -0,0 +1,20 @@
/*
* SPDX-FileCopyrightText: 2016-2026 NewPipe contributors <https://newpipe.net>
* SPDX-License-Identifier: GPL-3.0-or-later
*/
package org.schabi.newpipe.info_list
import android.content.Context
import org.schabi.newpipe.extractor.channel.ChannelInfoItem
import org.schabi.newpipe.extractor.comments.CommentsInfoItem
import org.schabi.newpipe.extractor.playlist.PlaylistInfoItem
import org.schabi.newpipe.extractor.stream.StreamInfoItem
import org.schabi.newpipe.util.OnClickGesture
class InfoItemBuilder(val context: Context) {
var onStreamSelectedListener: OnClickGesture<StreamInfoItem>? = null
var onChannelSelectedListener: OnClickGesture<ChannelInfoItem>? = null
var onPlaylistSelectedListener: OnClickGesture<PlaylistInfoItem>? = null
var onCommentsSelectedListener: OnClickGesture<CommentsInfoItem>? = null
}

View File

@@ -1,19 +1,18 @@
package org.schabi.newpipe.info_list
import android.view.View
import android.widget.ImageView
import android.widget.TextView
import com.xwray.groupie.GroupieViewHolder
import com.xwray.groupie.Item
import com.xwray.groupie.viewbinding.BindableItem
import com.xwray.groupie.viewbinding.GroupieViewHolder
import org.schabi.newpipe.R
import org.schabi.newpipe.databinding.ItemStreamSegmentBinding
import org.schabi.newpipe.extractor.stream.StreamSegment
import org.schabi.newpipe.util.Localization
import org.schabi.newpipe.util.image.PicassoHelper
import org.schabi.newpipe.util.image.CoilHelper
class StreamSegmentItem(
private val item: StreamSegment,
private val onClick: StreamSegmentAdapter.StreamSegmentListener
) : Item<GroupieViewHolder>() {
) : BindableItem<ItemStreamSegmentBinding>() {
companion object {
const val PAYLOAD_SELECT = 1
@@ -21,34 +20,35 @@ class StreamSegmentItem(
var isSelected = false
override fun bind(viewHolder: GroupieViewHolder, position: Int) {
item.previewUrl?.let {
PicassoHelper.loadThumbnail(it)
.into(viewHolder.root.findViewById<ImageView>(R.id.previewImage))
}
viewHolder.root.findViewById<TextView>(R.id.textViewTitle).text = item.title
override fun bind(viewBinding: ItemStreamSegmentBinding, position: Int) {
CoilHelper.loadThumbnail(viewBinding.previewImage, item.previewUrl)
viewBinding.textViewTitle.text = item.title
if (item.channelName == null) {
viewHolder.root.findViewById<TextView>(R.id.textViewChannel).visibility = View.GONE
viewBinding.textViewChannel.visibility = View.GONE
// When the channel name is displayed there is less space
// and thus the segment title needs to be only one line height.
// But when there is no channel name displayed, the title can be two lines long.
// The default maxLines value is set to 1 to display all elements in the AS preview,
viewHolder.root.findViewById<TextView>(R.id.textViewTitle).maxLines = 2
viewBinding.textViewTitle.maxLines = 2
} else {
viewHolder.root.findViewById<TextView>(R.id.textViewChannel).text = item.channelName
viewHolder.root.findViewById<TextView>(R.id.textViewChannel).visibility = View.VISIBLE
viewBinding.textViewChannel.text = item.channelName
viewBinding.textViewChannel.visibility = View.VISIBLE
}
viewHolder.root.findViewById<TextView>(R.id.textViewStartSeconds).text =
viewBinding.textViewStartSeconds.text =
Localization.getDurationString(item.startTimeSeconds.toLong())
viewHolder.root.setOnClickListener { onClick.onItemClick(this, item.startTimeSeconds) }
viewHolder.root.setOnLongClickListener {
viewBinding.root.setOnClickListener { onClick.onItemClick(this, item.startTimeSeconds) }
viewBinding.root.setOnLongClickListener {
onClick.onItemLongClick(this, item.startTimeSeconds)
true
}
viewHolder.root.isSelected = isSelected
viewBinding.root.isSelected = isSelected
}
override fun bind(viewHolder: GroupieViewHolder, position: Int, payloads: MutableList<Any>) {
override fun bind(
viewHolder: GroupieViewHolder<ItemStreamSegmentBinding>,
position: Int,
payloads: MutableList<Any>
) {
if (payloads.contains(PAYLOAD_SELECT)) {
viewHolder.root.isSelected = isSelected
return
@@ -57,4 +57,6 @@ class StreamSegmentItem(
}
override fun getLayout() = R.layout.item_stream_segment
override fun initializeViewBinding(view: View) = ItemStreamSegmentBinding.bind(view)
}

View File

@@ -346,7 +346,7 @@ public final class InfoItemDialog {
public static void reportErrorDuringInitialization(final Throwable throwable,
final InfoItem item) {
ErrorUtil.showSnackbar(App.getApp().getBaseContext(), new ErrorInfo(
ErrorUtil.showSnackbar(App.getInstance().getBaseContext(), new ErrorInfo(
throwable,
UserAction.OPEN_INFO_ITEM_DIALOG,
"none",

View File

@@ -13,8 +13,8 @@ import org.schabi.newpipe.extractor.channel.ChannelInfoItem;
import org.schabi.newpipe.extractor.utils.Utils;
import org.schabi.newpipe.info_list.InfoItemBuilder;
import org.schabi.newpipe.local.history.HistoryRecordManager;
import org.schabi.newpipe.util.image.PicassoHelper;
import org.schabi.newpipe.util.Localization;
import org.schabi.newpipe.util.image.CoilHelper;
public class ChannelMiniInfoItemHolder extends InfoItemHolder {
private final ImageView itemThumbnailView;
@@ -56,7 +56,7 @@ public class ChannelMiniInfoItemHolder extends InfoItemHolder {
itemAdditionalDetailView.setText(getDetailLine(item));
}
PicassoHelper.loadAvatar(item.getThumbnails()).into(itemThumbnailView);
CoilHelper.INSTANCE.loadAvatar(itemThumbnailView, item.getThumbnails());
itemView.setOnClickListener(view -> {
if (itemBuilder.getOnChannelSelectedListener() != null) {

View File

@@ -27,8 +27,8 @@ import org.schabi.newpipe.util.DeviceUtils;
import org.schabi.newpipe.util.Localization;
import org.schabi.newpipe.util.NavigationHelper;
import org.schabi.newpipe.util.external_communication.ShareUtils;
import org.schabi.newpipe.util.image.CoilHelper;
import org.schabi.newpipe.util.image.ImageStrategy;
import org.schabi.newpipe.util.image.PicassoHelper;
import org.schabi.newpipe.util.text.TextEllipsizer;
public class CommentInfoItemHolder extends InfoItemHolder {
@@ -82,14 +82,12 @@ public class CommentInfoItemHolder extends InfoItemHolder {
@Override
public void updateFromItem(final InfoItem infoItem,
final HistoryRecordManager historyRecordManager) {
if (!(infoItem instanceof CommentsInfoItem)) {
if (!(infoItem instanceof CommentsInfoItem item)) {
return;
}
final CommentsInfoItem item = (CommentsInfoItem) infoItem;
// load the author avatar
PicassoHelper.loadAvatar(item.getUploaderAvatars()).into(itemThumbnailView);
CoilHelper.INSTANCE.loadAvatar(itemThumbnailView, item.getUploaderAvatars());
if (ImageStrategy.shouldLoadImages()) {
itemThumbnailView.setVisibility(View.VISIBLE);
itemRoot.setPadding(commentVerticalPadding, commentVerticalPadding,

View File

@@ -9,8 +9,8 @@ import org.schabi.newpipe.extractor.InfoItem;
import org.schabi.newpipe.extractor.playlist.PlaylistInfoItem;
import org.schabi.newpipe.info_list.InfoItemBuilder;
import org.schabi.newpipe.local.history.HistoryRecordManager;
import org.schabi.newpipe.util.image.PicassoHelper;
import org.schabi.newpipe.util.Localization;
import org.schabi.newpipe.util.image.CoilHelper;
public class PlaylistMiniInfoItemHolder extends InfoItemHolder {
public final ImageView itemThumbnailView;
@@ -46,7 +46,7 @@ public class PlaylistMiniInfoItemHolder extends InfoItemHolder {
.localizeStreamCountMini(itemStreamCountView.getContext(), item.getStreamCount()));
itemUploaderView.setText(item.getUploaderName());
PicassoHelper.loadPlaylistThumbnail(item.getThumbnails()).into(itemThumbnailView);
CoilHelper.INSTANCE.loadPlaylistThumbnail(itemThumbnailView, item.getThumbnails());
itemView.setOnClickListener(view -> {
if (itemBuilder.getOnPlaylistSelectedListener() != null) {

View File

@@ -16,8 +16,8 @@ import org.schabi.newpipe.ktx.ViewUtils;
import org.schabi.newpipe.local.history.HistoryRecordManager;
import org.schabi.newpipe.util.DependentPreferenceHelper;
import org.schabi.newpipe.util.Localization;
import org.schabi.newpipe.util.image.PicassoHelper;
import org.schabi.newpipe.util.StreamTypeUtil;
import org.schabi.newpipe.util.image.CoilHelper;
import org.schabi.newpipe.views.AnimatedProgressBar;
import java.util.concurrent.TimeUnit;
@@ -87,7 +87,7 @@ public class StreamMiniInfoItemHolder extends InfoItemHolder {
}
// Default thumbnail is shown on error, while loading and if the url is empty
PicassoHelper.loadThumbnail(item.getThumbnails()).into(itemThumbnailView);
CoilHelper.INSTANCE.loadThumbnail(itemThumbnailView, item.getThumbnails());
itemView.setOnClickListener(view -> {
if (itemBuilder.getOnStreamSelectedListener() != null) {

View File

@@ -0,0 +1,13 @@
package org.schabi.newpipe.ktx
import android.graphics.Bitmap
import android.graphics.Rect
import androidx.core.graphics.BitmapCompat
@Suppress("NOTHING_TO_INLINE")
inline fun Bitmap.scale(
width: Int,
height: Int,
srcRect: Rect? = null,
scaleInLinearSpace: Boolean = true
) = BitmapCompat.createScaledBitmap(this, width, height, srcRect, scaleInLinearSpace)

View File

@@ -255,7 +255,7 @@ class FeedFragment : BaseStateFragment<FeedState>() {
viewModel.getShowFutureItemsFromPreferences()
)
AlertDialog.Builder(context!!)
AlertDialog.Builder(requireContext())
.setTitle(R.string.feed_hide_streams_title)
.setMultiChoiceItems(dialogItems, checkedDialogItems) { _, which, isChecked ->
checkedDialogItems[which] = isChecked

View File

@@ -129,8 +129,7 @@ class FeedViewModel(
fun setSaveShowPlayedItems(showPlayedItems: Boolean) {
this.showPlayedItems.onNext(showPlayedItems)
PreferenceManager.getDefaultSharedPreferences(application).edit {
this.putBoolean(application.getString(R.string.feed_show_watched_items_key), showPlayedItems)
this.apply()
putBoolean(application.getString(R.string.feed_show_watched_items_key), showPlayedItems)
}
}
@@ -139,8 +138,7 @@ class FeedViewModel(
fun setSaveShowPartiallyPlayedItems(showPartiallyPlayedItems: Boolean) {
this.showPartiallyPlayedItems.onNext(showPartiallyPlayedItems)
PreferenceManager.getDefaultSharedPreferences(application).edit {
this.putBoolean(application.getString(R.string.feed_show_partially_watched_items_key), showPartiallyPlayedItems)
this.apply()
putBoolean(application.getString(R.string.feed_show_partially_watched_items_key), showPartiallyPlayedItems)
}
}
@@ -149,8 +147,7 @@ class FeedViewModel(
fun setSaveShowFutureItems(showFutureItems: Boolean) {
this.showFutureItems.onNext(showFutureItems)
PreferenceManager.getDefaultSharedPreferences(application).edit {
this.putBoolean(application.getString(R.string.feed_show_future_items_key), showFutureItems)
this.apply()
putBoolean(application.getString(R.string.feed_show_future_items_key), showFutureItems)
}
}
@@ -169,7 +166,7 @@ class FeedViewModel(
fun getFactory(context: Context, groupId: Long) = viewModelFactory {
initializer {
FeedViewModel(
App.getApp(),
App.instance,
groupId,
// Read initial value from preferences
getShowPlayedItemsFromPreferences(context.applicationContext),

View File

@@ -21,7 +21,7 @@ import org.schabi.newpipe.extractor.stream.StreamType.POST_LIVE_STREAM
import org.schabi.newpipe.extractor.stream.StreamType.VIDEO_STREAM
import org.schabi.newpipe.util.Localization
import org.schabi.newpipe.util.StreamTypeUtil
import org.schabi.newpipe.util.image.PicassoHelper
import org.schabi.newpipe.util.image.CoilHelper
data class StreamItem(
val streamWithState: StreamWithState,
@@ -101,7 +101,7 @@ data class StreamItem(
viewBinding.itemProgressView.visibility = View.GONE
}
PicassoHelper.loadThumbnail(stream.thumbnailUrl).into(viewBinding.itemThumbnailView)
CoilHelper.loadThumbnail(viewBinding.itemThumbnailView, stream.thumbnailUrl)
if (itemVersion != ItemVersion.MINI) {
viewBinding.itemAdditionalDetails.text =

View File

@@ -6,7 +6,6 @@ import android.app.PendingIntent
import android.content.Context
import android.content.Intent
import android.graphics.Bitmap
import android.graphics.drawable.Drawable
import android.net.Uri
import android.os.Build
import android.provider.Settings
@@ -17,20 +16,17 @@ import androidx.core.content.ContextCompat
import androidx.core.content.getSystemService
import androidx.core.net.toUri
import androidx.preference.PreferenceManager
import com.squareup.picasso.Picasso
import com.squareup.picasso.Target
import org.schabi.newpipe.R
import org.schabi.newpipe.extractor.stream.StreamInfoItem
import org.schabi.newpipe.local.feed.service.FeedUpdateInfo
import org.schabi.newpipe.util.NavigationHelper
import org.schabi.newpipe.util.image.PicassoHelper
import org.schabi.newpipe.util.image.CoilHelper
/**
* Helper for everything related to show notifications about new streams to the user.
*/
class NotificationHelper(val context: Context) {
private val manager = NotificationManagerCompat.from(context)
private val iconLoadingTargets = ArrayList<Target>()
/**
* Show notifications for new streams from a single channel. The individual notifications are
@@ -71,69 +67,42 @@ class NotificationHelper(val context: Context) {
summaryBuilder.setStyle(style)
// open the channel page when clicking on the summary notification
val intent = NavigationHelper
.getChannelIntent(context, data.serviceId, data.url)
.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
summaryBuilder.setContentIntent(
PendingIntentCompat.getActivity(
context,
data.pseudoId,
NavigationHelper
.getChannelIntent(context, data.serviceId, data.url)
.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK),
0,
false
)
PendingIntentCompat.getActivity(context, data.pseudoId, intent, 0, false)
)
// a Target is like a listener for image loading events
val target = object : Target {
override fun onBitmapLoaded(bitmap: Bitmap, from: Picasso.LoadedFrom) {
// set channel icon only if there is actually one (for Android versions < 7.0)
summaryBuilder.setLargeIcon(bitmap)
val avatarIcon =
CoilHelper.loadBitmapBlocking(context, data.avatarUrl, R.drawable.ic_newpipe_triangle_white)
summaryBuilder.setLargeIcon(avatarIcon)
// Show individual stream notifications, set channel icon only if there is actually
// one
showStreamNotifications(newStreams, data.serviceId, data.url, bitmap)
// Show summary notification
manager.notify(data.pseudoId, summaryBuilder.build())
iconLoadingTargets.remove(this) // allow it to be garbage-collected
}
override fun onBitmapFailed(e: Exception, errorDrawable: Drawable) {
// Show individual stream notifications
showStreamNotifications(newStreams, data.serviceId, data.url, null)
// Show summary notification
manager.notify(data.pseudoId, summaryBuilder.build())
iconLoadingTargets.remove(this) // allow it to be garbage-collected
}
override fun onPrepareLoad(placeHolderDrawable: Drawable) {
// Nothing to do
}
// Show individual stream notifications, set channel icon only if there is actually one
showStreamNotifications(newStreams, data.serviceId, avatarIcon)
// Show summary notification
if (manager.areNotificationsEnabled()) {
manager.notify(data.pseudoId, summaryBuilder.build())
}
// add the target to the list to hold a strong reference and prevent it from being garbage
// collected, since Picasso only holds weak references to targets
iconLoadingTargets.add(target)
PicassoHelper.loadNotificationIcon(data.avatarUrl).into(target)
}
private fun showStreamNotifications(
newStreams: List<StreamInfoItem>,
serviceId: Int,
channelUrl: String,
channelIcon: Bitmap?
) {
for (stream in newStreams) {
val notification = createStreamNotification(stream, serviceId, channelUrl, channelIcon)
manager.notify(stream.url.hashCode(), notification)
if (manager.areNotificationsEnabled()) {
newStreams.forEach { stream ->
val notification =
createStreamNotification(stream, serviceId, channelIcon)
manager.notify(stream.url.hashCode(), notification)
}
}
}
private fun createStreamNotification(
item: StreamInfoItem,
serviceId: Int,
channelUrl: String,
channelIcon: Bitmap?
): Notification {
return NotificationCompat.Builder(
@@ -144,7 +113,7 @@ class NotificationHelper(val context: Context) {
.setLargeIcon(channelIcon)
.setContentTitle(item.name)
.setContentText(item.uploaderName)
.setGroup(channelUrl)
.setGroup(item.uploaderUrl)
.setColor(ContextCompat.getColor(context, R.color.ic_launcher_background))
.setColorized(true)
.setAutoCancel(true)

View File

@@ -111,7 +111,8 @@ class FeedLoadManager(private val context: Context) {
broadcastProgress()
}
.observeOn(Schedulers.io())
.flatMap { Flowable.fromIterable(it) }
// Randomize user subscription ordering to attempt to resist fingerprinting
.flatMap { Flowable.fromIterable(it.shuffled()) }
.takeWhile { !cancelSignal.get() }
.doOnNext { subscriptionEntity ->
// throttle YouTube extractions once every BATCH_SIZE to avoid being rate limited

View File

@@ -185,7 +185,9 @@ class FeedLoadService : Service() {
}
}
notificationManager.notify(NOTIFICATION_ID, notificationBuilder.build())
if (notificationManager.areNotificationsEnabled()) {
notificationManager.notify(NOTIFICATION_ID, notificationBuilder.build())
}
}
// /////////////////////////////////////////////////////////////////////////

View File

@@ -8,8 +8,8 @@ import org.schabi.newpipe.database.playlist.PlaylistDuplicatesEntry;
import org.schabi.newpipe.database.playlist.PlaylistMetadataEntry;
import org.schabi.newpipe.local.LocalItemBuilder;
import org.schabi.newpipe.local.history.HistoryRecordManager;
import org.schabi.newpipe.util.image.PicassoHelper;
import org.schabi.newpipe.util.Localization;
import org.schabi.newpipe.util.image.CoilHelper;
import java.time.format.DateTimeFormatter;
@@ -30,17 +30,16 @@ public class LocalPlaylistItemHolder extends PlaylistItemHolder {
public void updateFromItem(final LocalItem localItem,
final HistoryRecordManager historyRecordManager,
final DateTimeFormatter dateTimeFormatter) {
if (!(localItem instanceof PlaylistMetadataEntry)) {
if (!(localItem instanceof PlaylistMetadataEntry item)) {
return;
}
final PlaylistMetadataEntry item = (PlaylistMetadataEntry) localItem;
itemTitleView.setText(item.getOrderingName());
itemStreamCountView.setText(Localization.localizeStreamCountMini(
itemStreamCountView.getContext(), item.getStreamCount()));
itemUploaderView.setVisibility(View.INVISIBLE);
PicassoHelper.loadPlaylistThumbnail(item.getThumbnailUrl()).into(itemThumbnailView);
CoilHelper.INSTANCE.loadPlaylistThumbnail(itemThumbnailView, item.getThumbnailUrl());
if (item instanceof PlaylistDuplicatesEntry
&& ((PlaylistDuplicatesEntry) item).getTimesStreamIsContained() > 0) {

View File

@@ -16,8 +16,8 @@ import org.schabi.newpipe.local.LocalItemBuilder;
import org.schabi.newpipe.local.history.HistoryRecordManager;
import org.schabi.newpipe.util.DependentPreferenceHelper;
import org.schabi.newpipe.util.Localization;
import org.schabi.newpipe.util.image.PicassoHelper;
import org.schabi.newpipe.util.ServiceHelper;
import org.schabi.newpipe.util.image.CoilHelper;
import org.schabi.newpipe.views.AnimatedProgressBar;
import java.time.format.DateTimeFormatter;
@@ -83,8 +83,8 @@ public class LocalPlaylistStreamItemHolder extends LocalItemHolder {
}
// Default thumbnail is shown on error, while loading and if the url is empty
PicassoHelper.loadThumbnail(item.getStreamEntity().getThumbnailUrl())
.into(itemThumbnailView);
CoilHelper.INSTANCE.loadThumbnail(itemThumbnailView,
item.getStreamEntity().getThumbnailUrl());
itemView.setOnClickListener(view -> {
if (itemBuilder.getOnItemSelectedListener() != null) {

View File

@@ -16,8 +16,8 @@ import org.schabi.newpipe.local.LocalItemBuilder;
import org.schabi.newpipe.local.history.HistoryRecordManager;
import org.schabi.newpipe.util.DependentPreferenceHelper;
import org.schabi.newpipe.util.Localization;
import org.schabi.newpipe.util.image.PicassoHelper;
import org.schabi.newpipe.util.ServiceHelper;
import org.schabi.newpipe.util.image.CoilHelper;
import org.schabi.newpipe.views.AnimatedProgressBar;
import java.time.format.DateTimeFormatter;
@@ -117,8 +117,8 @@ public class LocalStatisticStreamItemHolder extends LocalItemHolder {
}
// Default thumbnail is shown on error, while loading and if the url is empty
PicassoHelper.loadThumbnail(item.getStreamEntity().getThumbnailUrl())
.into(itemThumbnailView);
CoilHelper.INSTANCE.loadThumbnail(itemThumbnailView,
item.getStreamEntity().getThumbnailUrl());
itemView.setOnClickListener(view -> {
if (itemBuilder.getOnItemSelectedListener() != null) {

View File

@@ -8,8 +8,8 @@ import org.schabi.newpipe.database.playlist.model.PlaylistRemoteEntity;
import org.schabi.newpipe.local.LocalItemBuilder;
import org.schabi.newpipe.local.history.HistoryRecordManager;
import org.schabi.newpipe.util.Localization;
import org.schabi.newpipe.util.image.PicassoHelper;
import org.schabi.newpipe.util.ServiceHelper;
import org.schabi.newpipe.util.image.CoilHelper;
import java.time.format.DateTimeFormatter;
@@ -29,10 +29,9 @@ public class RemotePlaylistItemHolder extends PlaylistItemHolder {
public void updateFromItem(final LocalItem localItem,
final HistoryRecordManager historyRecordManager,
final DateTimeFormatter dateTimeFormatter) {
if (!(localItem instanceof PlaylistRemoteEntity)) {
if (!(localItem instanceof PlaylistRemoteEntity item)) {
return;
}
final PlaylistRemoteEntity item = (PlaylistRemoteEntity) localItem;
itemTitleView.setText(item.getOrderingName());
itemStreamCountView.setText(Localization.localizeStreamCountMini(
@@ -45,7 +44,7 @@ public class RemotePlaylistItemHolder extends PlaylistItemHolder {
itemUploaderView.setText(ServiceHelper.getNameOfServiceById(item.getServiceId()));
}
PicassoHelper.loadPlaylistThumbnail(item.getThumbnailUrl()).into(itemThumbnailView);
CoilHelper.INSTANCE.loadPlaylistThumbnail(itemThumbnailView, item.getThumbnailUrl());
super.updateFromItem(localItem, historyRecordManager, dateTimeFormatter);
}

View File

@@ -1,41 +1,63 @@
package org.schabi.newpipe.local.subscription;
import android.app.Dialog;
import android.content.Intent;
import android.os.Bundle;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AlertDialog;
import androidx.core.os.BundleCompat;
import androidx.fragment.app.DialogFragment;
import androidx.fragment.app.Fragment;
import androidx.work.Constraints;
import androidx.work.ExistingWorkPolicy;
import androidx.work.NetworkType;
import androidx.work.OneTimeWorkRequest;
import androidx.work.OutOfQuotaPolicy;
import androidx.work.WorkManager;
import com.livefront.bridge.Bridge;
import org.schabi.newpipe.R;
import org.schabi.newpipe.local.subscription.workers.SubscriptionImportInput;
import org.schabi.newpipe.local.subscription.workers.SubscriptionImportWorker;
public class ImportConfirmationDialog extends DialogFragment {
protected Intent resultServiceIntent;
private static final String EXTRA_RESULT_SERVICE_INTENT = "extra_result_service_intent";
private static final String INPUT = "input";
public static void show(@NonNull final Fragment fragment,
@NonNull final Intent resultServiceIntent) {
final ImportConfirmationDialog confirmationDialog = new ImportConfirmationDialog();
final Bundle args = new Bundle();
args.putParcelable(EXTRA_RESULT_SERVICE_INTENT, resultServiceIntent);
confirmationDialog.setArguments(args);
public static void show(@NonNull final Fragment fragment, final SubscriptionImportInput input) {
final var confirmationDialog = new ImportConfirmationDialog();
final var arguments = new Bundle();
arguments.putParcelable(INPUT, input);
confirmationDialog.setArguments(arguments);
confirmationDialog.show(fragment.getParentFragmentManager(), null);
}
@NonNull
@Override
public Dialog onCreateDialog(@Nullable final Bundle savedInstanceState) {
return new AlertDialog.Builder(requireContext())
final var context = requireContext();
return new AlertDialog.Builder(context)
.setMessage(R.string.import_network_expensive_warning)
.setCancelable(true)
.setNegativeButton(R.string.cancel, null)
.setPositiveButton(R.string.ok, (dialogInterface, i) -> {
requireContext().startService(resultServiceIntent);
final var constraints = new Constraints.Builder()
.setRequiredNetworkType(NetworkType.CONNECTED)
.build();
final var input = BundleCompat.getParcelable(requireArguments(), INPUT,
SubscriptionImportInput.class);
final var req = new OneTimeWorkRequest.Builder(SubscriptionImportWorker.class)
.setInputData(input.toData())
.setExpedited(OutOfQuotaPolicy.RUN_AS_NON_EXPEDITED_WORK_REQUEST)
.setConstraints(constraints)
.build();
WorkManager.getInstance(context)
.enqueueUniqueWork(SubscriptionImportWorker.WORK_NAME,
ExistingWorkPolicy.APPEND_OR_REPLACE, req);
dismiss();
})
.create();
@@ -45,7 +67,7 @@ public class ImportConfirmationDialog extends DialogFragment {
public void onCreate(@Nullable final Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
resultServiceIntent = requireArguments().getParcelable(EXTRA_RESULT_SERVICE_INTENT);
Bridge.restoreInstanceState(this, savedInstanceState);
}
@Override

View File

@@ -1,9 +1,7 @@
package org.schabi.newpipe.local.subscription
import android.app.Activity
import android.content.Context
import android.content.DialogInterface
import android.content.Intent
import android.os.Bundle
import android.os.Parcelable
import android.view.LayoutInflater
@@ -15,8 +13,6 @@ import android.view.View
import android.view.ViewGroup
import android.webkit.MimeTypeMap
import android.widget.Toast
import androidx.activity.result.ActivityResult
import androidx.activity.result.contract.ActivityResultContracts.StartActivityForResult
import androidx.annotation.StringRes
import androidx.appcompat.app.AlertDialog
import androidx.lifecycle.ViewModelProvider
@@ -27,9 +23,6 @@ import com.xwray.groupie.GroupAdapter
import com.xwray.groupie.Section
import com.xwray.groupie.viewbinding.GroupieViewHolder
import io.reactivex.rxjava3.disposables.CompositeDisposable
import java.text.SimpleDateFormat
import java.util.Date
import java.util.Locale
import org.schabi.newpipe.R
import org.schabi.newpipe.database.feed.model.FeedGroupEntity.Companion.GROUP_ALL_ID
import org.schabi.newpipe.databinding.DialogTitleBinding
@@ -53,13 +46,6 @@ import org.schabi.newpipe.local.subscription.item.FeedGroupCarouselItem
import org.schabi.newpipe.local.subscription.item.GroupsHeader
import org.schabi.newpipe.local.subscription.item.Header
import org.schabi.newpipe.local.subscription.item.ImportSubscriptionsHintPlaceholderItem
import org.schabi.newpipe.local.subscription.services.SubscriptionsExportService
import org.schabi.newpipe.local.subscription.services.SubscriptionsImportService
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.streams.io.NoFileManagerSafeGuard
import org.schabi.newpipe.streams.io.StoredFileHelper
import org.schabi.newpipe.util.NavigationHelper
import org.schabi.newpipe.util.OnClickGesture
import org.schabi.newpipe.util.ServiceHelper
@@ -72,6 +58,7 @@ class SubscriptionFragment : BaseStateFragment<SubscriptionState>() {
private lateinit var viewModel: SubscriptionViewModel
private lateinit var subscriptionManager: SubscriptionManager
private lateinit var importExportHelper: SubscriptionsImportExportHelper
private val disposables: CompositeDisposable = CompositeDisposable()
private val groupAdapter = GroupAdapter<GroupieViewHolder<FeedItemCarouselBinding>>()
@@ -80,11 +67,6 @@ class SubscriptionFragment : BaseStateFragment<SubscriptionState>() {
private lateinit var feedGroupsSortMenuItem: GroupsHeader
private val subscriptionsSection = Section()
private val requestExportLauncher =
registerForActivityResult(StartActivityForResult(), this::requestExportResult)
private val requestImportLauncher =
registerForActivityResult(StartActivityForResult(), this::requestImportResult)
@State
@JvmField
var itemsListState: Parcelable? = null
@@ -104,6 +86,7 @@ class SubscriptionFragment : BaseStateFragment<SubscriptionState>() {
override fun onAttach(context: Context) {
super.onAttach(context)
subscriptionManager = SubscriptionManager(requireContext())
importExportHelper = SubscriptionsImportExportHelper(this)
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
@@ -143,7 +126,7 @@ class SubscriptionFragment : BaseStateFragment<SubscriptionState>() {
// -- Import --
val importSubMenu = menu.addSubMenu(R.string.import_from)
addMenuItemToSubmenu(importSubMenu, R.string.previous_export) { onImportPreviousSelected() }
addMenuItemToSubmenu(importSubMenu, R.string.previous_export) { importExportHelper.onImportPreviousSelected() }
.setIcon(R.drawable.ic_backup)
for (service in ServiceList.all()) {
@@ -161,7 +144,7 @@ class SubscriptionFragment : BaseStateFragment<SubscriptionState>() {
// -- Export --
val exportSubMenu = menu.addSubMenu(R.string.export_to)
addMenuItemToSubmenu(exportSubMenu, R.string.file) { onExportSelected() }
addMenuItemToSubmenu(exportSubMenu, R.string.file) { importExportHelper.onExportSelected() }
.setIcon(R.drawable.ic_save)
}
@@ -197,51 +180,10 @@ class SubscriptionFragment : BaseStateFragment<SubscriptionState>() {
NavigationHelper.openSubscriptionsImportFragment(fragmentManager, serviceId)
}
private fun onImportPreviousSelected() {
NoFileManagerSafeGuard.launchSafe(
requestImportLauncher,
StoredFileHelper.getPicker(activity, JSON_MIME_TYPE),
TAG,
requireContext()
)
}
private fun onExportSelected() {
val date = SimpleDateFormat("yyyyMMddHHmm", Locale.ENGLISH).format(Date())
val exportName = "newpipe_subscriptions_$date.json"
NoFileManagerSafeGuard.launchSafe(
requestExportLauncher,
StoredFileHelper.getNewPicker(activity, exportName, JSON_MIME_TYPE, null),
TAG,
requireContext()
)
}
private fun openReorderDialog() {
FeedGroupReorderDialog().show(parentFragmentManager, null)
}
private 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)
)
}
}
private 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)
)
}
}
// ////////////////////////////////////////////////////////////////////////
// Fragment Views
// ////////////////////////////////////////////////////////////////////////

View File

@@ -1,7 +1,6 @@
package org.schabi.newpipe.local.subscription
import android.content.Context
import android.util.Pair
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
import io.reactivex.rxjava3.core.Completable
import io.reactivex.rxjava3.core.Flowable
@@ -51,23 +50,16 @@ class SubscriptionManager(context: Context) {
}
}
fun upsertAll(infoList: List<Pair<ChannelInfo, List<ChannelTabInfo>>>): List<SubscriptionEntity> {
val listEntities = subscriptionTable.upsertAll(
infoList.map { SubscriptionEntity.from(it.first) }
)
fun upsertAll(infoList: List<Pair<ChannelInfo, ChannelTabInfo>>) {
val listEntities = infoList.map { SubscriptionEntity.from(it.first) }
subscriptionTable.upsertAll(listEntities)
database.runInTransaction {
infoList.forEachIndexed { index, info ->
info.second.forEach {
feedDatabaseManager.upsertAll(
listEntities[index].uid,
it.relatedItems.filterIsInstance<StreamInfoItem>()
)
}
val streams = info.second.relatedItems.filterIsInstance<StreamInfoItem>()
feedDatabaseManager.upsertAll(listEntities[index].uid, streams)
}
}
return listEntities
}
fun updateChannelInfo(info: ChannelInfo): Completable = subscriptionTable.getSubscription(info.serviceId, info.url)

View File

@@ -0,0 +1,82 @@
package org.schabi.newpipe.local.subscription
import android.app.Activity
import android.content.Context
import androidx.activity.result.ActivityResult
import androidx.activity.result.contract.ActivityResultContracts.StartActivityForResult
import androidx.fragment.app.Fragment
import java.text.SimpleDateFormat
import java.util.Date
import java.util.Locale
import org.schabi.newpipe.local.subscription.SubscriptionFragment.Companion.JSON_MIME_TYPE
import org.schabi.newpipe.local.subscription.workers.SubscriptionExportWorker
import org.schabi.newpipe.local.subscription.workers.SubscriptionImportInput
import org.schabi.newpipe.streams.io.NoFileManagerSafeGuard
import org.schabi.newpipe.streams.io.StoredFileHelper
/**
* This class has to be created in onAttach() or onCreate().
*
* It contains registerForActivityResult calls and those
* calls are only allowed before a fragment/activity is created.
*/
class SubscriptionsImportExportHelper(
val fragment: Fragment
) {
val context: Context = fragment.requireContext()
companion object {
val TAG: String =
SubscriptionsImportExportHelper::class.java.simpleName + "@" + Integer.toHexString(
hashCode()
)
}
private val requestExportLauncher =
fragment.registerForActivityResult(StartActivityForResult(), this::requestExportResult)
private val requestImportLauncher =
fragment.registerForActivityResult(StartActivityForResult(), this::requestImportResult)
private fun requestExportResult(result: ActivityResult) {
val data = result.data?.data
if (data != null && result.resultCode == Activity.RESULT_OK) {
SubscriptionExportWorker.schedule(context, data)
}
}
private fun requestImportResult(result: ActivityResult) {
val data = result.data?.dataString
if (data != null && result.resultCode == Activity.RESULT_OK) {
ImportConfirmationDialog.show(
fragment,
SubscriptionImportInput.PreviousExportMode(data)
)
}
}
fun onExportSelected() {
val date = SimpleDateFormat("yyyyMMddHHmm", Locale.ENGLISH).format(Date())
val exportName = "newpipe_subscriptions_$date.json"
NoFileManagerSafeGuard.launchSafe(
requestExportLauncher,
StoredFileHelper.getNewPicker(
context,
exportName,
JSON_MIME_TYPE,
null
),
TAG,
context
)
}
fun onImportPreviousSelected() {
NoFileManagerSafeGuard.launchSafe(
requestImportLauncher,
StoredFileHelper.getPicker(context, JSON_MIME_TYPE),
TAG,
context
)
}
}

View File

@@ -1,10 +1,6 @@
package org.schabi.newpipe.local.subscription;
import static org.schabi.newpipe.extractor.subscription.SubscriptionExtractor.ContentSource.CHANNEL_URL;
import static org.schabi.newpipe.local.subscription.services.SubscriptionsImportService.CHANNEL_URL_MODE;
import static org.schabi.newpipe.local.subscription.services.SubscriptionsImportService.INPUT_STREAM_MODE;
import static org.schabi.newpipe.local.subscription.services.SubscriptionsImportService.KEY_MODE;
import static org.schabi.newpipe.local.subscription.services.SubscriptionsImportService.KEY_VALUE;
import android.app.Activity;
import android.content.Intent;
@@ -37,7 +33,7 @@ import org.schabi.newpipe.error.UserAction;
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.local.subscription.workers.SubscriptionImportInput;
import org.schabi.newpipe.streams.io.NoFileManagerSafeGuard;
import org.schabi.newpipe.streams.io.StoredFileHelper;
import org.schabi.newpipe.util.Constants;
@@ -168,10 +164,8 @@ public class SubscriptionsImportFragment extends BaseFragment {
}
public void onImportUrl(final String value) {
ImportConfirmationDialog.show(this, new Intent(activity, SubscriptionsImportService.class)
.putExtra(KEY_MODE, CHANNEL_URL_MODE)
.putExtra(KEY_VALUE, value)
.putExtra(Constants.KEY_SERVICE_ID, currentServiceId));
ImportConfirmationDialog.show(this,
new SubscriptionImportInput.ChannelUrlMode(currentServiceId, value));
}
public void onImportFile() {
@@ -186,16 +180,10 @@ public class SubscriptionsImportFragment extends BaseFragment {
}
private void requestImportFileResult(final ActivityResult result) {
if (result.getData() == null) {
return;
}
if (result.getResultCode() == Activity.RESULT_OK && result.getData().getData() != null) {
final String data = result.getData() != null ? result.getData().getDataString() : null;
if (result.getResultCode() == Activity.RESULT_OK && data != null) {
ImportConfirmationDialog.show(this,
new Intent(activity, SubscriptionsImportService.class)
.putExtra(KEY_MODE, INPUT_STREAM_MODE)
.putExtra(KEY_VALUE, result.getData().getData())
.putExtra(Constants.KEY_SERVICE_ID, currentServiceId));
new SubscriptionImportInput.InputStreamMode(currentServiceId, data));
}
}

View File

@@ -327,7 +327,7 @@ class FeedGroupDialog : DialogFragment(), BackPressable {
groupIcon = feedGroupEntity?.icon
groupSortOrder = feedGroupEntity?.sortOrder ?: -1
val feedGroupIcon = if (selectedIcon == null) icon else selectedIcon!!
val feedGroupIcon = selectedIcon ?: icon
feedGroupCreateBinding.iconPreview.setImageResource(feedGroupIcon.getDrawableRes())
if (feedGroupCreateBinding.groupNameInput.text.isNullOrBlank()) {
@@ -506,7 +506,7 @@ class FeedGroupDialog : DialogFragment(), BackPressable {
private fun hideKeyboardSearch() {
inputMethodManager.hideSoftInputFromWindow(
searchLayoutBinding.toolbarSearchEditText.windowToken,
InputMethodManager.RESULT_UNCHANGED_SHOWN
InputMethodManager.HIDE_NOT_ALWAYS
)
searchLayoutBinding.toolbarSearchEditText.clearFocus()
}
@@ -523,7 +523,7 @@ class FeedGroupDialog : DialogFragment(), BackPressable {
private fun hideKeyboard() {
inputMethodManager.hideSoftInputFromWindow(
feedGroupCreateBinding.groupNameInput.windowToken,
InputMethodManager.RESULT_UNCHANGED_SHOWN
InputMethodManager.HIDE_NOT_ALWAYS
)
feedGroupCreateBinding.groupNameInput.clearFocus()
}

View File

@@ -9,7 +9,7 @@ import org.schabi.newpipe.R
import org.schabi.newpipe.extractor.channel.ChannelInfoItem
import org.schabi.newpipe.util.Localization
import org.schabi.newpipe.util.OnClickGesture
import org.schabi.newpipe.util.image.PicassoHelper
import org.schabi.newpipe.util.image.CoilHelper
class ChannelItem(
private val infoItem: ChannelInfoItem,
@@ -39,7 +39,7 @@ class ChannelItem(
itemChannelDescriptionView.text = infoItem.description
}
PicassoHelper.loadAvatar(infoItem.thumbnails).into(itemThumbnailView)
CoilHelper.loadAvatar(itemThumbnailView, infoItem.thumbnails)
gesturesListener?.run {
viewHolder.root.setOnClickListener { selected(infoItem) }

View File

@@ -10,7 +10,7 @@ import org.schabi.newpipe.database.subscription.SubscriptionEntity
import org.schabi.newpipe.databinding.PickerSubscriptionItemBinding
import org.schabi.newpipe.ktx.AnimationType
import org.schabi.newpipe.ktx.animate
import org.schabi.newpipe.util.image.PicassoHelper
import org.schabi.newpipe.util.image.CoilHelper
data class PickerSubscriptionItem(
val subscriptionEntity: SubscriptionEntity,
@@ -21,7 +21,7 @@ data class PickerSubscriptionItem(
override fun getSpanSize(spanCount: Int, position: Int): Int = 1
override fun bind(viewBinding: PickerSubscriptionItemBinding, position: Int) {
PicassoHelper.loadAvatar(subscriptionEntity.avatarUrl).into(viewBinding.thumbnailView)
CoilHelper.loadAvatar(viewBinding.thumbnailView, subscriptionEntity.avatarUrl)
viewBinding.titleView.text = subscriptionEntity.name
viewBinding.selectedHighlight.isVisible = isSelected
}

View File

@@ -1,233 +0,0 @@
/*
* Copyright 2018 Mauricio Colli <mauriciocolli@outlook.com>
* BaseImportExportService.java is part of NewPipe
*
* License: GPL-3.0+
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.schabi.newpipe.local.subscription.services;
import android.app.Service;
import android.content.Intent;
import android.os.Build;
import android.os.IBinder;
import android.text.TextUtils;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.StringRes;
import androidx.core.app.NotificationCompat;
import androidx.core.app.NotificationManagerCompat;
import androidx.core.app.ServiceCompat;
import org.reactivestreams.Publisher;
import org.schabi.newpipe.R;
import org.schabi.newpipe.error.ErrorInfo;
import org.schabi.newpipe.error.ErrorUtil;
import org.schabi.newpipe.error.UserAction;
import org.schabi.newpipe.extractor.subscription.SubscriptionExtractor;
import org.schabi.newpipe.ktx.ExceptionUtils;
import org.schabi.newpipe.local.subscription.SubscriptionManager;
import java.io.FileNotFoundException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
import io.reactivex.rxjava3.core.Flowable;
import io.reactivex.rxjava3.disposables.CompositeDisposable;
import io.reactivex.rxjava3.functions.Function;
import io.reactivex.rxjava3.processors.PublishProcessor;
public abstract class BaseImportExportService extends Service {
protected final String TAG = this.getClass().getSimpleName();
protected final CompositeDisposable disposables = new CompositeDisposable();
protected final PublishProcessor<String> notificationUpdater = PublishProcessor.create();
protected NotificationManagerCompat notificationManager;
protected NotificationCompat.Builder notificationBuilder;
protected SubscriptionManager subscriptionManager;
private static final int NOTIFICATION_SAMPLING_PERIOD = 2500;
protected final AtomicInteger currentProgress = new AtomicInteger(-1);
protected final AtomicInteger maxProgress = new AtomicInteger(-1);
protected final ImportExportEventListener eventListener = new ImportExportEventListener() {
@Override
public void onSizeReceived(final int size) {
maxProgress.set(size);
currentProgress.set(0);
}
@Override
public void onItemCompleted(final String itemName) {
currentProgress.incrementAndGet();
notificationUpdater.onNext(itemName);
}
};
protected Toast toast;
@Nullable
@Override
public IBinder onBind(final Intent intent) {
return null;
}
@Override
public void onCreate() {
super.onCreate();
subscriptionManager = new SubscriptionManager(this);
setupNotification();
}
@Override
public void onDestroy() {
super.onDestroy();
disposeAll();
}
protected void disposeAll() {
disposables.clear();
}
/*//////////////////////////////////////////////////////////////////////////
// Notification Impl
//////////////////////////////////////////////////////////////////////////*/
protected abstract int getNotificationId();
@StringRes
public abstract int getTitle();
protected void setupNotification() {
notificationManager = NotificationManagerCompat.from(this);
notificationBuilder = createNotification();
startForeground(getNotificationId(), notificationBuilder.build());
final Function<Flowable<String>, Publisher<String>> throttleAfterFirstEmission = flow ->
flow.take(1).concatWith(flow.skip(1)
.throttleLast(NOTIFICATION_SAMPLING_PERIOD, TimeUnit.MILLISECONDS));
disposables.add(notificationUpdater
.filter(s -> !s.isEmpty())
.publish(throttleAfterFirstEmission)
.observeOn(AndroidSchedulers.mainThread())
.subscribe(this::updateNotification));
}
protected void updateNotification(final String text) {
notificationBuilder
.setProgress(maxProgress.get(), currentProgress.get(), maxProgress.get() == -1);
final String progressText = currentProgress + "/" + maxProgress;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
if (!TextUtils.isEmpty(text)) {
notificationBuilder.setContentText(text + " (" + progressText + ")");
}
} else {
notificationBuilder.setContentInfo(progressText);
notificationBuilder.setContentText(text);
}
notificationManager.notify(getNotificationId(), notificationBuilder.build());
}
protected void stopService() {
postErrorResult(null, null);
}
protected void stopAndReportError(final Throwable throwable, final String request) {
stopService();
ErrorUtil.createNotification(this, new ErrorInfo(
throwable, UserAction.SUBSCRIPTION_IMPORT_EXPORT, request));
}
protected void postErrorResult(final String title, final String text) {
disposeAll();
ServiceCompat.stopForeground(this, ServiceCompat.STOP_FOREGROUND_REMOVE);
stopSelf();
if (title == null) {
return;
}
final String textOrEmpty = text == null ? "" : text;
notificationBuilder = new NotificationCompat
.Builder(this, getString(R.string.notification_channel_id))
.setSmallIcon(R.drawable.ic_newpipe_triangle_white)
.setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
.setContentTitle(title)
.setStyle(new NotificationCompat.BigTextStyle().bigText(textOrEmpty))
.setContentText(textOrEmpty);
notificationManager.notify(getNotificationId(), notificationBuilder.build());
}
protected NotificationCompat.Builder createNotification() {
return new NotificationCompat.Builder(this, getString(R.string.notification_channel_id))
.setOngoing(true)
.setProgress(-1, -1, true)
.setSmallIcon(R.drawable.ic_newpipe_triangle_white)
.setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
.setContentTitle(getString(getTitle()));
}
/*//////////////////////////////////////////////////////////////////////////
// Toast
//////////////////////////////////////////////////////////////////////////*/
protected void showToast(@StringRes final int message) {
showToast(getString(message));
}
protected void showToast(final String message) {
if (toast != null) {
toast.cancel();
}
toast = Toast.makeText(this, message, Toast.LENGTH_SHORT);
toast.show();
}
/*//////////////////////////////////////////////////////////////////////////
// Error handling
//////////////////////////////////////////////////////////////////////////*/
protected void handleError(@StringRes final int errorTitle, @NonNull final Throwable error) {
String message = getErrorMessage(error);
if (TextUtils.isEmpty(message)) {
final String errorClassName = error.getClass().getName();
message = getString(R.string.error_occurred_detail, errorClassName);
}
showToast(errorTitle);
postErrorResult(getString(errorTitle), message);
}
protected String getErrorMessage(final Throwable error) {
String message = null;
if (error instanceof SubscriptionExtractor.InvalidSourceException) {
message = getString(R.string.invalid_source);
} else if (error instanceof FileNotFoundException) {
message = getString(R.string.invalid_file);
} else if (ExceptionUtils.isNetworkRelated(error)) {
message = getString(R.string.network_error);
}
return message;
}
}

View File

@@ -1,17 +0,0 @@
package org.schabi.newpipe.local.subscription.services;
public interface ImportExportEventListener {
/**
* Called when the size has been resolved.
*
* @param size how many items there are to import/export
*/
void onSizeReceived(int size);
/**
* Called every time an item has been parsed/resolved.
*
* @param itemName the name of the subscription item
*/
void onItemCompleted(String itemName);
}

View File

@@ -1,158 +0,0 @@
/*
* Copyright 2018 Mauricio Colli <mauriciocolli@outlook.com>
* ImportExportJsonHelper.java is part of NewPipe
*
* License: GPL-3.0+
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.schabi.newpipe.local.subscription.services;
import androidx.annotation.Nullable;
import com.grack.nanojson.JsonAppendableWriter;
import com.grack.nanojson.JsonArray;
import com.grack.nanojson.JsonObject;
import com.grack.nanojson.JsonParser;
import com.grack.nanojson.JsonWriter;
import org.schabi.newpipe.BuildConfig;
import org.schabi.newpipe.extractor.subscription.SubscriptionExtractor.InvalidSourceException;
import org.schabi.newpipe.extractor.subscription.SubscriptionItem;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.List;
/**
* A JSON implementation capable of importing and exporting subscriptions, it has the advantage
* of being able to transfer subscriptions to any device.
*/
public final class ImportExportJsonHelper {
/*//////////////////////////////////////////////////////////////////////////
// Json implementation
//////////////////////////////////////////////////////////////////////////*/
private static final String JSON_APP_VERSION_KEY = "app_version";
private static final String JSON_APP_VERSION_INT_KEY = "app_version_int";
private static final String JSON_SUBSCRIPTIONS_ARRAY_KEY = "subscriptions";
private static final String JSON_SERVICE_ID_KEY = "service_id";
private static final String JSON_URL_KEY = "url";
private static final String JSON_NAME_KEY = "name";
private ImportExportJsonHelper() { }
/**
* Read a JSON source through the input stream.
*
* @param in the input stream (e.g. a file)
* @param eventListener listener for the events generated
* @return the parsed subscription items
*/
public static List<SubscriptionItem> readFrom(
final InputStream in, @Nullable final ImportExportEventListener eventListener)
throws InvalidSourceException {
if (in == null) {
throw new InvalidSourceException("input is null");
}
final List<SubscriptionItem> channels = new ArrayList<>();
try {
final JsonObject parentObject = JsonParser.object().from(in);
if (!parentObject.has(JSON_SUBSCRIPTIONS_ARRAY_KEY)) {
throw new InvalidSourceException("Channels array is null");
}
final JsonArray channelsArray = parentObject.getArray(JSON_SUBSCRIPTIONS_ARRAY_KEY);
if (eventListener != null) {
eventListener.onSizeReceived(channelsArray.size());
}
for (final Object o : channelsArray) {
if (o instanceof JsonObject) {
final JsonObject itemObject = (JsonObject) o;
final int serviceId = itemObject.getInt(JSON_SERVICE_ID_KEY, 0);
final String url = itemObject.getString(JSON_URL_KEY);
final String name = itemObject.getString(JSON_NAME_KEY);
if (url != null && name != null && !url.isEmpty() && !name.isEmpty()) {
channels.add(new SubscriptionItem(serviceId, url, name));
if (eventListener != null) {
eventListener.onItemCompleted(name);
}
}
}
}
} catch (final Throwable e) {
throw new InvalidSourceException("Couldn't parse json", e);
}
return channels;
}
/**
* Write the subscriptions items list as JSON to the output.
*
* @param items the list of subscriptions items
* @param out the output stream (e.g. a file)
* @param eventListener listener for the events generated
*/
public static void writeTo(final List<SubscriptionItem> items, final OutputStream out,
@Nullable final ImportExportEventListener eventListener) {
final JsonAppendableWriter writer = JsonWriter.on(out);
writeTo(items, writer, eventListener);
writer.done();
}
/**
* @see #writeTo(List, OutputStream, ImportExportEventListener)
* @param items the list of subscriptions items
* @param writer the output {@link JsonAppendableWriter}
* @param eventListener listener for the events generated
*/
public static void writeTo(final List<SubscriptionItem> items,
final JsonAppendableWriter writer,
@Nullable final ImportExportEventListener eventListener) {
if (eventListener != null) {
eventListener.onSizeReceived(items.size());
}
writer.object();
writer.value(JSON_APP_VERSION_KEY, BuildConfig.VERSION_NAME);
writer.value(JSON_APP_VERSION_INT_KEY, BuildConfig.VERSION_CODE);
writer.array(JSON_SUBSCRIPTIONS_ARRAY_KEY);
for (final SubscriptionItem item : items) {
writer.object();
writer.value(JSON_SERVICE_ID_KEY, item.getServiceId());
writer.value(JSON_URL_KEY, item.getUrl());
writer.value(JSON_NAME_KEY, item.getName());
writer.end();
if (eventListener != null) {
eventListener.onItemCompleted(item.getName());
}
}
writer.end();
writer.end();
}
}

View File

@@ -1,171 +0,0 @@
/*
* Copyright 2018 Mauricio Colli <mauriciocolli@outlook.com>
* SubscriptionsExportService.java is part of NewPipe
*
* License: GPL-3.0+
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.schabi.newpipe.local.subscription.services;
import static org.schabi.newpipe.MainActivity.DEBUG;
import android.content.Intent;
import android.net.Uri;
import android.util.Log;
import androidx.core.content.IntentCompat;
import androidx.localbroadcastmanager.content.LocalBroadcastManager;
import org.reactivestreams.Subscriber;
import org.reactivestreams.Subscription;
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.IOException;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.List;
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
import io.reactivex.rxjava3.functions.Function;
import io.reactivex.rxjava3.schedulers.Schedulers;
public class SubscriptionsExportService extends BaseImportExportService {
public static final String KEY_FILE_PATH = "key_file_path";
/**
* A {@link LocalBroadcastManager local broadcast} will be made with this action
* when the export is successfully completed.
*/
public static final String EXPORT_COMPLETE_ACTION = App.PACKAGE_NAME + ".local.subscription"
+ ".services.SubscriptionsExportService.EXPORT_COMPLETE";
private Subscription subscription;
private StoredFileHelper outFile;
private OutputStream outputStream;
@Override
public int onStartCommand(final Intent intent, final int flags, final int startId) {
if (intent == null || subscription != null) {
return START_NOT_STICKY;
}
final Uri path = IntentCompat.getParcelableExtra(intent, KEY_FILE_PATH, Uri.class);
if (path == null) {
stopAndReportError(new IllegalStateException(
"Exporting to a file, but the path is null"),
"Exporting subscriptions");
return START_NOT_STICKY;
}
try {
outFile = new StoredFileHelper(this, path, "application/json");
// truncate the file before writing to it, otherwise if the new content is smaller than
// the previous file size, the file will retain part of the previous content and be
// corrupted
outputStream = new SharpOutputStream(outFile.openAndTruncateStream());
} catch (final IOException e) {
handleError(e);
return START_NOT_STICKY;
}
startExport();
return START_NOT_STICKY;
}
@Override
protected int getNotificationId() {
return 4567;
}
@Override
public int getTitle() {
return R.string.export_ongoing;
}
@Override
protected void disposeAll() {
super.disposeAll();
if (subscription != null) {
subscription.cancel();
}
}
private void startExport() {
showToast(R.string.export_ongoing);
subscriptionManager.subscriptionTable().getAll().take(1)
.map(subscriptionEntities -> {
final List<SubscriptionItem> result =
new ArrayList<>(subscriptionEntities.size());
for (final SubscriptionEntity entity : subscriptionEntities) {
result.add(new SubscriptionItem(entity.getServiceId(), entity.getUrl(),
entity.getName()));
}
return result;
})
.map(exportToFile())
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(getSubscriber());
}
private Subscriber<StoredFileHelper> getSubscriber() {
return new Subscriber<StoredFileHelper>() {
@Override
public void onSubscribe(final Subscription s) {
subscription = s;
s.request(1);
}
@Override
public void onNext(final StoredFileHelper file) {
if (DEBUG) {
Log.d(TAG, "startExport() success: file = " + file);
}
}
@Override
public void onError(final Throwable error) {
Log.e(TAG, "onError() called with: error = [" + error + "]", error);
handleError(error);
}
@Override
public void onComplete() {
LocalBroadcastManager.getInstance(SubscriptionsExportService.this)
.sendBroadcast(new Intent(EXPORT_COMPLETE_ACTION));
showToast(R.string.export_complete_toast);
stopService();
}
};
}
private Function<List<SubscriptionItem>, StoredFileHelper> exportToFile() {
return subscriptionItems -> {
ImportExportJsonHelper.writeTo(subscriptionItems, outputStream, eventListener);
return outFile;
};
}
protected void handleError(final Throwable error) {
super.handleError(R.string.subscriptions_export_unsuccessful, error);
}
}

View File

@@ -1,327 +0,0 @@
/*
* Copyright 2018 Mauricio Colli <mauriciocolli@outlook.com>
* SubscriptionsImportService.java is part of NewPipe
*
* License: GPL-3.0+
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.schabi.newpipe.local.subscription.services;
import static org.schabi.newpipe.MainActivity.DEBUG;
import static org.schabi.newpipe.streams.io.StoredFileHelper.DEFAULT_MIME;
import android.content.Intent;
import android.net.Uri;
import android.text.TextUtils;
import android.util.Log;
import android.util.Pair;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.core.content.IntentCompat;
import androidx.localbroadcastmanager.content.LocalBroadcastManager;
import org.reactivestreams.Subscriber;
import org.reactivestreams.Subscription;
import org.schabi.newpipe.App;
import org.schabi.newpipe.R;
import org.schabi.newpipe.database.subscription.SubscriptionEntity;
import org.schabi.newpipe.extractor.NewPipe;
import org.schabi.newpipe.extractor.channel.ChannelInfo;
import org.schabi.newpipe.extractor.channel.tabs.ChannelTabInfo;
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.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
import io.reactivex.rxjava3.core.Flowable;
import io.reactivex.rxjava3.core.Notification;
import io.reactivex.rxjava3.functions.Consumer;
import io.reactivex.rxjava3.functions.Function;
import io.reactivex.rxjava3.schedulers.Schedulers;
public class SubscriptionsImportService extends BaseImportExportService {
public static final int CHANNEL_URL_MODE = 0;
public static final int INPUT_STREAM_MODE = 1;
public static final int PREVIOUS_EXPORT_MODE = 2;
public static final String KEY_MODE = "key_mode";
public static final String KEY_VALUE = "key_value";
/**
* A {@link LocalBroadcastManager local broadcast} will be made with this action
* when the import is successfully completed.
*/
public static final String IMPORT_COMPLETE_ACTION = App.PACKAGE_NAME + ".local.subscription"
+ ".services.SubscriptionsImportService.IMPORT_COMPLETE";
/**
* How many extractions running in parallel.
*/
public static final int PARALLEL_EXTRACTIONS = 8;
/**
* Number of items to buffer to mass-insert in the subscriptions table,
* this leads to a better performance as we can then use db transactions.
*/
public static final int BUFFER_COUNT_BEFORE_INSERT = 50;
private Subscription subscription;
private int currentMode;
private int currentServiceId;
@Nullable
private String channelUrl;
@Nullable
private InputStream inputStream;
@Nullable
private String inputStreamType;
@Override
public int onStartCommand(final Intent intent, final int flags, final int startId) {
if (intent == null || subscription != null) {
return START_NOT_STICKY;
}
currentMode = intent.getIntExtra(KEY_MODE, -1);
currentServiceId = intent.getIntExtra(Constants.KEY_SERVICE_ID, Constants.NO_SERVICE_ID);
if (currentMode == CHANNEL_URL_MODE) {
channelUrl = intent.getStringExtra(KEY_VALUE);
} else {
final Uri uri = IntentCompat.getParcelableExtra(intent, KEY_VALUE, Uri.class);
if (uri == null) {
stopAndReportError(new IllegalStateException(
"Importing from input stream, but file path is null"),
"Importing subscriptions");
return START_NOT_STICKY;
}
try {
final StoredFileHelper fileHelper = new StoredFileHelper(this, uri, DEFAULT_MIME);
inputStream = new SharpInputStream(fileHelper.getStream());
inputStreamType = fileHelper.getType();
if (inputStreamType == null || inputStreamType.equals(DEFAULT_MIME)) {
// mime type could not be determined, just take file extension
final String name = fileHelper.getName();
final int pointIndex = name.lastIndexOf('.');
if (pointIndex == -1 || pointIndex >= name.length() - 1) {
inputStreamType = DEFAULT_MIME; // no extension, will fail in the extractor
} else {
inputStreamType = name.substring(pointIndex + 1);
}
}
} catch (final IOException e) {
handleError(e);
return START_NOT_STICKY;
}
}
if (currentMode == -1 || currentMode == CHANNEL_URL_MODE && channelUrl == null) {
final String errorDescription = "Some important field is null or in illegal state: "
+ "currentMode=[" + currentMode + "], "
+ "channelUrl=[" + channelUrl + "], "
+ "inputStream=[" + inputStream + "]";
stopAndReportError(new IllegalStateException(errorDescription),
"Importing subscriptions");
return START_NOT_STICKY;
}
startImport();
return START_NOT_STICKY;
}
@Override
protected int getNotificationId() {
return 4568;
}
@Override
public int getTitle() {
return R.string.import_ongoing;
}
@Override
protected void disposeAll() {
super.disposeAll();
if (subscription != null) {
subscription.cancel();
}
}
/*//////////////////////////////////////////////////////////////////////////
// Imports
//////////////////////////////////////////////////////////////////////////*/
private void startImport() {
showToast(R.string.import_ongoing);
Flowable<List<SubscriptionItem>> flowable = null;
switch (currentMode) {
case CHANNEL_URL_MODE:
flowable = importFromChannelUrl();
break;
case INPUT_STREAM_MODE:
flowable = importFromInputStream();
break;
case PREVIOUS_EXPORT_MODE:
flowable = importFromPreviousExport();
break;
}
if (flowable == null) {
final String message = "Flowable given by \"importFrom\" is null "
+ "(current mode: " + currentMode + ")";
stopAndReportError(new IllegalStateException(message), "Importing subscriptions");
return;
}
flowable.doOnNext(subscriptionItems ->
eventListener.onSizeReceived(subscriptionItems.size()))
.flatMap(Flowable::fromIterable)
.parallel(PARALLEL_EXTRACTIONS)
.runOn(Schedulers.io())
.map((Function<SubscriptionItem, Notification<Pair<ChannelInfo,
List<ChannelTabInfo>>>>) subscriptionItem -> {
try {
final ChannelInfo channelInfo = ExtractorHelper
.getChannelInfo(subscriptionItem.getServiceId(),
subscriptionItem.getUrl(), true)
.blockingGet();
return Notification.createOnNext(new Pair<>(channelInfo,
Collections.singletonList(
ExtractorHelper.getChannelTab(
subscriptionItem.getServiceId(),
channelInfo.getTabs().get(0), true).blockingGet()
)));
} catch (final Throwable e) {
return Notification.createOnError(e);
}
})
.sequential()
.observeOn(Schedulers.io())
.doOnNext(getNotificationsConsumer())
.buffer(BUFFER_COUNT_BEFORE_INSERT)
.map(upsertBatch())
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(getSubscriber());
}
private Subscriber<List<SubscriptionEntity>> getSubscriber() {
return new Subscriber<>() {
@Override
public void onSubscribe(final Subscription s) {
subscription = s;
s.request(Long.MAX_VALUE);
}
@Override
public void onNext(final List<SubscriptionEntity> successfulInserted) {
if (DEBUG) {
Log.d(TAG, "startImport() " + successfulInserted.size()
+ " items successfully inserted into the database");
}
}
@Override
public void onError(final Throwable error) {
Log.e(TAG, "Got an error!", error);
handleError(error);
}
@Override
public void onComplete() {
LocalBroadcastManager.getInstance(SubscriptionsImportService.this)
.sendBroadcast(new Intent(IMPORT_COMPLETE_ACTION));
showToast(R.string.import_complete_toast);
stopService();
}
};
}
private Consumer<Notification<Pair<ChannelInfo,
List<ChannelTabInfo>>>> getNotificationsConsumer() {
return notification -> {
if (notification.isOnNext()) {
final String name = notification.getValue().first.getName();
eventListener.onItemCompleted(!TextUtils.isEmpty(name) ? name : "");
} else if (notification.isOnError()) {
final Throwable error = notification.getError();
final Throwable cause = error.getCause();
if (error instanceof IOException) {
throw error;
} else if (cause instanceof IOException) {
throw cause;
} else if (ExceptionUtils.isNetworkRelated(error)) {
throw new IOException(error);
}
eventListener.onItemCompleted("");
}
};
}
private Function<List<Notification<Pair<ChannelInfo, List<ChannelTabInfo>>>>,
List<SubscriptionEntity>> upsertBatch() {
return notificationList -> {
final List<Pair<ChannelInfo, List<ChannelTabInfo>>> infoList =
new ArrayList<>(notificationList.size());
for (final Notification<Pair<ChannelInfo, List<ChannelTabInfo>>> n : notificationList) {
if (n.isOnNext()) {
infoList.add(n.getValue());
}
}
return subscriptionManager.upsertAll(infoList);
};
}
private Flowable<List<SubscriptionItem>> importFromChannelUrl() {
return Flowable.fromCallable(() -> NewPipe.getService(currentServiceId)
.getSubscriptionExtractor()
.fromChannelUrl(channelUrl));
}
private Flowable<List<SubscriptionItem>> importFromInputStream() {
Objects.requireNonNull(inputStream);
Objects.requireNonNull(inputStreamType);
return Flowable.fromCallable(() -> NewPipe.getService(currentServiceId)
.getSubscriptionExtractor()
.fromInputStream(inputStream, inputStreamType));
}
private Flowable<List<SubscriptionItem>> importFromPreviousExport() {
return Flowable.fromCallable(() -> ImportExportJsonHelper.readFrom(inputStream, null));
}
protected void handleError(@NonNull final Throwable error) {
super.handleError(R.string.subscriptions_import_unsuccessful, error);
}
}

View File

@@ -0,0 +1,72 @@
/*
* Copyright 2018 Mauricio Colli <mauriciocolli@outlook.com>
* ImportExportJsonHelper.java is part of NewPipe
*
* License: GPL-3.0+
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.schabi.newpipe.local.subscription.workers
import java.io.InputStream
import java.io.OutputStream
import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.decodeFromStream
import kotlinx.serialization.json.encodeToStream
import org.schabi.newpipe.extractor.subscription.SubscriptionExtractor.InvalidSourceException
/**
* A JSON implementation capable of importing and exporting subscriptions, it has the advantage
* of being able to transfer subscriptions to any device.
*/
object ImportExportJsonHelper {
private val json = Json { encodeDefaults = true }
/**
* Read a JSON source through the input stream.
*
* @param in the input stream (e.g. a file)
* @return the parsed subscription items
*/
@JvmStatic
@Throws(InvalidSourceException::class)
fun readFrom(`in`: InputStream?): List<SubscriptionItem> {
if (`in` == null) {
throw InvalidSourceException("input is null")
}
try {
@OptIn(ExperimentalSerializationApi::class)
return json.decodeFromStream<SubscriptionData>(`in`).subscriptions
} catch (e: Throwable) {
throw InvalidSourceException("Couldn't parse json", e)
}
}
/**
* Write the subscriptions items list as JSON to the output.
*
* @param items the list of subscriptions items
* @param out the output stream (e.g. a file)
*/
@OptIn(ExperimentalSerializationApi::class)
@JvmStatic
fun writeTo(
items: List<SubscriptionItem>,
out: OutputStream
) {
json.encodeToStream(SubscriptionData(items), out)
}
}

View File

@@ -0,0 +1,24 @@
package org.schabi.newpipe.local.subscription.workers
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import org.schabi.newpipe.BuildConfig
@Serializable
class SubscriptionData(
val subscriptions: List<SubscriptionItem>
) {
@SerialName("app_version")
private val appVersion = BuildConfig.VERSION_NAME
@SerialName("app_version_int")
private val appVersionInt = BuildConfig.VERSION_CODE
}
@Serializable
data class SubscriptionItem(
@SerialName("service_id")
val serviceId: Int,
val url: String,
val name: String
)

View File

@@ -0,0 +1,119 @@
package org.schabi.newpipe.local.subscription.workers
import android.content.Context
import android.content.pm.ServiceInfo
import android.net.Uri
import android.os.Build
import android.util.Log
import android.widget.Toast
import androidx.core.app.NotificationCompat
import androidx.core.net.toUri
import androidx.work.CoroutineWorker
import androidx.work.ExistingWorkPolicy
import androidx.work.ForegroundInfo
import androidx.work.OneTimeWorkRequestBuilder
import androidx.work.OutOfQuotaPolicy
import androidx.work.WorkManager
import androidx.work.WorkerParameters
import androidx.work.workDataOf
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.reactive.awaitFirst
import kotlinx.coroutines.withContext
import org.schabi.newpipe.BuildConfig
import org.schabi.newpipe.NewPipeDatabase
import org.schabi.newpipe.R
class SubscriptionExportWorker(
appContext: Context,
params: WorkerParameters
) : CoroutineWorker(appContext, params) {
// This is needed for API levels < 31 (Android S).
override suspend fun getForegroundInfo(): ForegroundInfo {
return createForegroundInfo(applicationContext.getString(R.string.export_ongoing))
}
override suspend fun doWork(): Result {
return try {
val uri = inputData.getString(EXPORT_PATH)!!.toUri()
val table = NewPipeDatabase.getInstance(applicationContext).subscriptionDAO()
val subscriptions =
table.getAll()
.awaitFirst()
.map { SubscriptionItem(it.serviceId, it.url ?: "", it.name ?: "") }
val qty = subscriptions.size
val title = applicationContext.resources.getQuantityString(R.plurals.export_subscriptions, qty, qty)
setForeground(createForegroundInfo(title))
withContext(Dispatchers.IO) {
// Truncate file if it already exists
applicationContext.contentResolver.openOutputStream(uri, "wt")?.use {
ImportExportJsonHelper.writeTo(subscriptions, it)
}
}
if (BuildConfig.DEBUG) {
Log.i(TAG, "Exported $qty subscriptions")
}
withContext(Dispatchers.Main) {
Toast
.makeText(applicationContext, R.string.export_complete_toast, Toast.LENGTH_SHORT)
.show()
}
Result.success()
} catch (e: Exception) {
if (BuildConfig.DEBUG) {
Log.e(TAG, "Error while exporting subscriptions", e)
}
withContext(Dispatchers.Main) {
Toast
.makeText(applicationContext, R.string.subscriptions_export_unsuccessful, Toast.LENGTH_SHORT)
.show()
}
return Result.failure()
}
}
private fun createForegroundInfo(title: String): ForegroundInfo {
val notification =
NotificationCompat
.Builder(applicationContext, NOTIFICATION_CHANNEL_ID)
.setSmallIcon(R.drawable.ic_newpipe_triangle_white)
.setOngoing(true)
.setProgress(-1, -1, true)
.setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
.setForegroundServiceBehavior(NotificationCompat.FOREGROUND_SERVICE_IMMEDIATE)
.setContentTitle(title)
.build()
val serviceType = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC else 0
return ForegroundInfo(NOTIFICATION_ID, notification, serviceType)
}
companion object {
private const val TAG = "SubscriptionExportWork"
private const val NOTIFICATION_ID = 4567
private const val NOTIFICATION_CHANNEL_ID = "newpipe"
private const val WORK_NAME = "exportSubscriptions"
private const val EXPORT_PATH = "exportPath"
fun schedule(
context: Context,
uri: Uri
) {
val data = workDataOf(EXPORT_PATH to uri.toString())
val workRequest =
OneTimeWorkRequestBuilder<SubscriptionExportWorker>()
.setInputData(data)
.setExpedited(OutOfQuotaPolicy.RUN_AS_NON_EXPEDITED_WORK_REQUEST)
.build()
WorkManager
.getInstance(context)
.enqueueUniqueWork(WORK_NAME, ExistingWorkPolicy.APPEND_OR_REPLACE, workRequest)
}
}
}

View File

@@ -0,0 +1,242 @@
package org.schabi.newpipe.local.subscription.workers
import android.content.Context
import android.content.pm.ServiceInfo
import android.os.Build
import android.os.Parcelable
import android.util.Log
import android.webkit.MimeTypeMap
import android.widget.Toast
import androidx.core.app.NotificationCompat
import androidx.core.net.toUri
import androidx.work.CoroutineWorker
import androidx.work.Data
import androidx.work.ForegroundInfo
import androidx.work.WorkManager
import androidx.work.WorkerParameters
import androidx.work.workDataOf
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.rx3.await
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import kotlinx.coroutines.withContext
import kotlinx.parcelize.Parcelize
import org.schabi.newpipe.BuildConfig
import org.schabi.newpipe.R
import org.schabi.newpipe.extractor.NewPipe
import org.schabi.newpipe.local.subscription.SubscriptionManager
import org.schabi.newpipe.util.ExtractorHelper
class SubscriptionImportWorker(
appContext: Context,
params: WorkerParameters
) : CoroutineWorker(appContext, params) {
// This is needed for API levels < 31 (Android S).
override suspend fun getForegroundInfo(): ForegroundInfo {
return createForegroundInfo(applicationContext.getString(R.string.import_ongoing), null, 0, 0)
}
override suspend fun doWork(): Result {
val subscriptions =
try {
loadSubscriptionsFromInput(SubscriptionImportInput.fromData(inputData))
} catch (e: Exception) {
if (BuildConfig.DEBUG) {
Log.e(TAG, "Error while loading subscriptions from path", e)
}
withContext(Dispatchers.Main) {
Toast
.makeText(applicationContext, R.string.subscriptions_import_unsuccessful, Toast.LENGTH_SHORT)
.show()
}
return Result.failure()
}
val mutex = Mutex()
var index = 1
val qty = subscriptions.size
var title =
applicationContext.resources.getQuantityString(R.plurals.load_subscriptions, qty, qty)
val channelInfoList =
try {
withContext(Dispatchers.IO.limitedParallelism(PARALLEL_EXTRACTIONS)) {
subscriptions
.map {
async {
val channelInfo =
ExtractorHelper.getChannelInfo(it.serviceId, it.url, true).await()
val channelTab =
ExtractorHelper.getChannelTab(it.serviceId, channelInfo.tabs[0], true).await()
val currentIndex = mutex.withLock { index++ }
setForeground(createForegroundInfo(title, channelInfo.name, currentIndex, qty))
channelInfo to channelTab
}
}.awaitAll()
}
} catch (e: Exception) {
if (BuildConfig.DEBUG) {
Log.e(TAG, "Error while loading subscription data", e)
}
withContext(Dispatchers.Main) {
Toast.makeText(applicationContext, R.string.subscriptions_import_unsuccessful, Toast.LENGTH_SHORT)
.show()
}
return Result.failure()
}
title = applicationContext.resources.getQuantityString(R.plurals.import_subscriptions, qty, qty)
setForeground(createForegroundInfo(title, null, 0, 0))
index = 0
val subscriptionManager = SubscriptionManager(applicationContext)
for (chunk in channelInfoList.chunked(BUFFER_COUNT_BEFORE_INSERT)) {
withContext(Dispatchers.IO) {
subscriptionManager.upsertAll(chunk)
}
index += chunk.size
setForeground(createForegroundInfo(title, null, index, qty))
}
withContext(Dispatchers.Main) {
Toast.makeText(applicationContext, R.string.import_complete_toast, Toast.LENGTH_SHORT)
.show()
}
return Result.success()
}
private suspend fun loadSubscriptionsFromInput(input: SubscriptionImportInput): List<SubscriptionItem> {
return withContext(Dispatchers.IO) {
when (input) {
is SubscriptionImportInput.ChannelUrlMode ->
NewPipe.getService(input.serviceId).subscriptionExtractor
.fromChannelUrl(input.url)
.map { SubscriptionItem(it.serviceId, it.url, it.name) }
is SubscriptionImportInput.InputStreamMode ->
applicationContext.contentResolver.openInputStream(input.url.toUri())?.use {
val contentType =
MimeTypeMap.getFileExtensionFromUrl(input.url).ifEmpty { DEFAULT_MIME }
NewPipe.getService(input.serviceId).subscriptionExtractor
.fromInputStream(it, contentType)
.map { SubscriptionItem(it.serviceId, it.url, it.name) }
}
is SubscriptionImportInput.PreviousExportMode ->
applicationContext.contentResolver.openInputStream(input.url.toUri())?.use {
ImportExportJsonHelper.readFrom(it)
}
} ?: emptyList()
}
}
private fun createForegroundInfo(
title: String,
text: String?,
currentProgress: Int,
maxProgress: Int
): ForegroundInfo {
val notification =
NotificationCompat
.Builder(applicationContext, NOTIFICATION_CHANNEL_ID)
.setSmallIcon(R.drawable.ic_newpipe_triangle_white)
.setOngoing(true)
.setProgress(maxProgress, currentProgress, currentProgress == 0)
.setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
.setForegroundServiceBehavior(NotificationCompat.FOREGROUND_SERVICE_IMMEDIATE)
.setContentTitle(title)
.setContentText(text)
.addAction(
R.drawable.ic_close,
applicationContext.getString(R.string.cancel),
WorkManager.getInstance(applicationContext).createCancelPendingIntent(id)
).apply {
if (currentProgress > 0 && maxProgress > 0) {
val progressText = "$currentProgress/$maxProgress"
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
setSubText(progressText)
} else {
setContentInfo(progressText)
}
}
}.build()
val serviceType = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC else 0
return ForegroundInfo(NOTIFICATION_ID, notification, serviceType)
}
companion object {
// Log tag length is limited to 23 characters on API levels < 24.
private const val TAG = "SubscriptionImport"
private const val NOTIFICATION_ID = 4568
private const val NOTIFICATION_CHANNEL_ID = "newpipe"
private const val DEFAULT_MIME = "application/octet-stream"
private const val PARALLEL_EXTRACTIONS = 8
private const val BUFFER_COUNT_BEFORE_INSERT = 50
const val WORK_NAME = "SubscriptionImportWorker"
}
}
sealed class SubscriptionImportInput : Parcelable {
@Parcelize
data class ChannelUrlMode(val serviceId: Int, val url: String) : SubscriptionImportInput()
@Parcelize
data class InputStreamMode(val serviceId: Int, val url: String) : SubscriptionImportInput()
@Parcelize
data class PreviousExportMode(val url: String) : SubscriptionImportInput()
fun toData(): Data {
val (mode, serviceId, url) = when (this) {
is ChannelUrlMode -> Triple(CHANNEL_URL_MODE, serviceId, url)
is InputStreamMode -> Triple(INPUT_STREAM_MODE, serviceId, url)
is PreviousExportMode -> Triple(PREVIOUS_EXPORT_MODE, null, url)
}
return workDataOf("mode" to mode, "service_id" to serviceId, "url" to url)
}
companion object {
private const val CHANNEL_URL_MODE = 0
private const val INPUT_STREAM_MODE = 1
private const val PREVIOUS_EXPORT_MODE = 2
fun fromData(data: Data): SubscriptionImportInput {
val mode = data.getInt("mode", PREVIOUS_EXPORT_MODE)
when (mode) {
CHANNEL_URL_MODE -> {
val serviceId = data.getInt("service_id", -1)
if (serviceId == -1) {
throw IllegalArgumentException("No service id provided")
}
val url = data.getString("url")!!
return ChannelUrlMode(serviceId, url)
}
INPUT_STREAM_MODE -> {
val serviceId = data.getInt("service_id", -1)
if (serviceId == -1) {
throw IllegalArgumentException("No service id provided")
}
val url = data.getString("url")!!
return InputStreamMode(serviceId, url)
}
PREVIOUS_EXPORT_MODE -> {
val url = data.getString("url")!!
return PreviousExportMode(url)
}
else -> throw IllegalArgumentException("Unknown mode: $mode")
}
}
}
}

View File

@@ -127,39 +127,39 @@ public final class PlayQueueActivity extends AppCompatActivity
@Override
public boolean onOptionsItemSelected(final MenuItem item) {
switch (item.getItemId()) {
case android.R.id.home:
finish();
return true;
case R.id.action_settings:
NavigationHelper.openSettings(this);
return true;
case R.id.action_append_playlist:
PlaylistDialog.showForPlayQueue(player, getSupportFragmentManager());
return true;
case R.id.action_playback_speed:
openPlaybackParameterDialog();
return true;
case R.id.action_mute:
player.toggleMute();
return true;
case R.id.action_system_audio:
startActivity(new Intent(Settings.ACTION_SOUND_SETTINGS));
return true;
case R.id.action_switch_main:
final int itemId = item.getItemId();
if (itemId == android.R.id.home) {
finish();
return true;
} else if (itemId == R.id.action_settings) {
NavigationHelper.openSettings(this);
return true;
} else if (itemId == R.id.action_append_playlist) {
PlaylistDialog.showForPlayQueue(player, getSupportFragmentManager());
return true;
} else if (itemId == R.id.action_playback_speed) {
openPlaybackParameterDialog();
return true;
} else if (itemId == R.id.action_mute) {
player.toggleMute();
return true;
} else if (itemId == R.id.action_system_audio) {
startActivity(new Intent(Settings.ACTION_SOUND_SETTINGS));
return true;
} else if (itemId == R.id.action_switch_main) {
this.player.setRecovery();
NavigationHelper.playOnMainPlayer(this, player.getPlayQueue(), true);
return true;
} else if (itemId == R.id.action_switch_popup) {
if (PermissionHelper.isPopupEnabledElseAsk(this)) {
this.player.setRecovery();
NavigationHelper.playOnMainPlayer(this, player.getPlayQueue(), true);
return true;
case R.id.action_switch_popup:
if (PermissionHelper.isPopupEnabledElseAsk(this)) {
this.player.setRecovery();
NavigationHelper.playOnPopupPlayer(this, player.getPlayQueue(), true);
}
return true;
case R.id.action_switch_background:
this.player.setRecovery();
NavigationHelper.playOnBackgroundPlayer(this, player.getPlayQueue(), true);
return true;
NavigationHelper.playOnPopupPlayer(this, player.getPlayQueue(), true);
}
return true;
} else if (itemId == R.id.action_switch_background) {
this.player.setRecovery();
NavigationHelper.playOnBackgroundPlayer(this, player.getPlayQueue(), true);
return true;
}
if (item.getGroupId() == MENU_ID_AUDIO_TRACK) {

View File

@@ -46,13 +46,14 @@ import static org.schabi.newpipe.util.ListHelper.getPopupResolutionIndex;
import static org.schabi.newpipe.util.ListHelper.getResolutionIndex;
import static java.util.concurrent.TimeUnit.MILLISECONDS;
import static coil3.Image_androidKt.toBitmap;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.SharedPreferences;
import android.graphics.Bitmap;
import android.graphics.drawable.Drawable;
import android.media.AudioManager;
import android.support.v4.media.session.MediaSessionCompat;
import android.util.Log;
@@ -80,8 +81,6 @@ import com.google.android.exoplayer2.trackselection.DefaultTrackSelector;
import com.google.android.exoplayer2.trackselection.MappingTrackSelector;
import com.google.android.exoplayer2.upstream.DefaultBandwidthMeter;
import com.google.android.exoplayer2.video.VideoSize;
import com.squareup.picasso.Picasso;
import com.squareup.picasso.Target;
import org.schabi.newpipe.MainActivity;
import org.schabi.newpipe.R;
@@ -126,13 +125,14 @@ import org.schabi.newpipe.util.ListHelper;
import org.schabi.newpipe.util.NavigationHelper;
import org.schabi.newpipe.util.SerializedCache;
import org.schabi.newpipe.util.StreamTypeUtil;
import org.schabi.newpipe.util.image.PicassoHelper;
import org.schabi.newpipe.util.image.CoilHelper;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.stream.IntStream;
import coil3.target.Target;
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
import io.reactivex.rxjava3.core.Observable;
import io.reactivex.rxjava3.core.Single;
@@ -181,7 +181,6 @@ public final class Player implements PlaybackListener, Listener {
//////////////////////////////////////////////////////////////////////////*/
public static final int RENDERER_UNAVAILABLE = -1;
private static final String PICASSO_PLAYER_THUMBNAIL_TAG = "PICASSO_PLAYER_THUMBNAIL_TAG";
/*//////////////////////////////////////////////////////////////////////////
// Playback
@@ -200,6 +199,8 @@ public final class Player implements PlaybackListener, Listener {
private MediaItemTag currentMetadata;
@Nullable
private Bitmap currentThumbnail;
@Nullable
private coil3.request.Disposable thumbnailDisposable;
/*//////////////////////////////////////////////////////////////////////////
// Player
@@ -255,12 +256,6 @@ public final class Player implements PlaybackListener, Listener {
@NonNull
private final CompositeDisposable streamItemDisposable = new CompositeDisposable();
// This is the only listener we need for thumbnail loading, since there is always at most only
// one thumbnail being loaded at a time. This field is also here to maintain a strong reference,
// which would otherwise be garbage collected since Picasso holds weak references to targets.
@NonNull
private final Target currentThumbnailTarget;
/*//////////////////////////////////////////////////////////////////////////
// Utils
//////////////////////////////////////////////////////////////////////////*/
@@ -314,8 +309,6 @@ public final class Player implements PlaybackListener, Listener {
videoResolver = new VideoPlaybackResolver(context, dataSource, getQualityResolver());
audioResolver = new AudioPlaybackResolver(context, dataSource);
currentThumbnailTarget = getCurrentThumbnailTarget();
// The UIs added here should always be present. They will be initialized when the player
// reaches the initialization step. Make sure the media session ui is before the
// notification ui in the UIs list, since the notification depends on the media session in
@@ -704,7 +697,6 @@ public final class Player implements PlaybackListener, Listener {
databaseUpdateDisposable.clear();
progressUpdateDisposable.set(null);
streamItemDisposable.clear();
cancelLoadingCurrentThumbnail();
UIs.destroyAll(Object.class); // destroy every UI: obviously every UI extends Object
}
@@ -884,67 +876,58 @@ public final class Player implements PlaybackListener, Listener {
//////////////////////////////////////////////////////////////////////////*/
//region Thumbnail loading
private Target getCurrentThumbnailTarget() {
// a Picasso target is just a listener for thumbnail loading events
return new Target() {
@Override
public void onBitmapLoaded(final Bitmap bitmap, final Picasso.LoadedFrom from) {
if (DEBUG) {
Log.d(TAG, "Thumbnail - onBitmapLoaded() called with: bitmap = [" + bitmap
+ " -> " + bitmap.getWidth() + "x" + bitmap.getHeight() + "], from = ["
+ from + "]");
}
// there is a new thumbnail, so e.g. the end screen thumbnail needs to change, too.
onThumbnailLoaded(bitmap);
}
@Override
public void onBitmapFailed(final Exception e, final Drawable errorDrawable) {
Log.e(TAG, "Thumbnail - onBitmapFailed() called", e);
// there is a new thumbnail, so e.g. the end screen thumbnail needs to change, too.
onThumbnailLoaded(null);
}
@Override
public void onPrepareLoad(final Drawable placeHolderDrawable) {
if (DEBUG) {
Log.d(TAG, "Thumbnail - onPrepareLoad() called");
}
}
};
}
private void loadCurrentThumbnail(final List<Image> thumbnails) {
if (DEBUG) {
Log.d(TAG, "Thumbnail - loadCurrentThumbnail() called with thumbnails = ["
+ thumbnails.size() + "]");
}
// first cancel any previous loading
cancelLoadingCurrentThumbnail();
// Cancel any ongoing image loading
if (thumbnailDisposable != null) {
thumbnailDisposable.dispose();
}
// Unset currentThumbnail, since it is now outdated. This ensures it is not used in media
// session metadata while the new thumbnail is being loaded by Picasso.
// session metadata while the new thumbnail is being loaded by Coil.
onThumbnailLoaded(null);
if (thumbnails.isEmpty()) {
return;
}
// scale down the notification thumbnail for performance
PicassoHelper.loadScaledDownThumbnail(context, thumbnails)
.tag(PICASSO_PLAYER_THUMBNAIL_TAG)
.into(currentThumbnailTarget);
final var thumbnailTarget = new Target() {
@Override
public void onError(@Nullable final coil3.Image error) {
Log.e(TAG, "Thumbnail - onError() called");
// there is a new thumbnail, so e.g. the end screen thumbnail needs to change, too.
onThumbnailLoaded(null);
}
@Override
public void onStart(@Nullable final coil3.Image placeholder) {
if (DEBUG) {
Log.d(TAG, "Thumbnail - onStart() called");
}
}
@Override
public void onSuccess(@NonNull final coil3.Image result) {
if (DEBUG) {
Log.d(TAG, "Thumbnail - onSuccess() called with: drawable = [" + result + "]");
}
// there is a new thumbnail, so e.g. the end screen thumbnail needs to change, too.
onThumbnailLoaded(toBitmap(result));
}
};
thumbnailDisposable = CoilHelper.INSTANCE
.loadScaledDownThumbnail(context, thumbnails, thumbnailTarget);
}
private void cancelLoadingCurrentThumbnail() {
// cancel the Picasso job associated with the player thumbnail, if any
PicassoHelper.cancelTag(PICASSO_PLAYER_THUMBNAIL_TAG);
}
private void onThumbnailLoaded(@Nullable final Bitmap bitmap) {
// Avoid useless thumbnail updates, if the thumbnail has not actually changed. Based on the
// thumbnail loading code, this if would be skipped only when both bitmaps are `null`, since
// onThumbnailLoaded won't be called twice with the same nonnull bitmap by Picasso's target.
// onThumbnailLoaded won't be called twice with the same nonnull bitmap by Coil's target.
if (currentThumbnail != bitmap) {
currentThumbnail = bitmap;
UIs.call(playerUi -> playerUi.onThumbnailLoaded(bitmap));

View File

@@ -47,6 +47,9 @@ abstract class BasePlayerGestureListener(
startMultiDoubleTap(event)
} else if (portion === DisplayPortion.MIDDLE) {
player.playPause()
if (player.isPlaying) {
playerUi.hideControls(0, 0)
}
}
}

View File

@@ -49,12 +49,12 @@ import java.text.DecimalFormatSymbols;
import java.text.NumberFormat;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
public final class PlayerHelper {
private static final FormattersProvider FORMATTERS_PROVIDER = new FormattersProvider();
@@ -87,11 +87,11 @@ public final class PlayerHelper {
}
@NonNull
public static String getTimeString(final int milliSeconds) {
final int seconds = (milliSeconds % 60000) / 1000;
final int minutes = (milliSeconds % 3600000) / 60000;
final int hours = (milliSeconds % 86400000) / 3600000;
final int days = (milliSeconds % (86400000 * 7)) / 86400000;
public static String getTimeString(final long milliSeconds) {
final long seconds = (milliSeconds % 60000) / 1000;
final long minutes = (milliSeconds % 3600000) / 60000;
final long hours = (milliSeconds % 86400000) / 3600000;
final long days = (milliSeconds % (86400000 * 7)) / 86400000;
final Formatters formatters = FORMATTERS_PROVIDER.formatters();
if (days > 0) {
@@ -174,10 +174,9 @@ public final class PlayerHelper {
@Nullable
public static PlayQueue autoQueueOf(@NonNull final StreamInfo info,
@NonNull final List<PlayQueueItem> existingItems) {
final Set<String> urls = new HashSet<>(existingItems.size());
for (final PlayQueueItem item : existingItems) {
urls.add(item.getUrl());
}
final Set<String> urls = existingItems.stream()
.map(PlayQueueItem::getUrl)
.collect(Collectors.toUnmodifiableSet());
final List<InfoItem> relatedItems = info.getRelatedItems();
if (Utils.isNullOrEmpty(relatedItems)) {

View File

@@ -117,7 +117,7 @@ public final class PlayerHolder {
// 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();
return App.getInstance();
}
public void startService(final boolean playAfterConnect,

View File

@@ -22,7 +22,7 @@ internal fun infoItemTypeToString(type: InfoType): String {
InfoType.STREAM -> ID_STREAM
InfoType.PLAYLIST -> ID_PLAYLIST
InfoType.CHANNEL -> ID_CHANNEL
else -> throw IllegalStateException("Unexpected value: $type")
else -> error("Unexpected value: $type")
}
}
@@ -31,7 +31,7 @@ internal fun infoItemTypeFromString(type: String): InfoType {
ID_STREAM -> InfoType.STREAM
ID_PLAYLIST -> InfoType.PLAYLIST
ID_CHANNEL -> InfoType.CHANNEL
else -> throw IllegalStateException("Unexpected value: $type")
else -> error("Unexpected value: $type")
}
}

View File

@@ -49,7 +49,7 @@ import org.schabi.newpipe.util.NavigationHelper
*/
class MediaBrowserPlaybackPreparer(
private val context: Context,
private val setMediaSessionError: BiConsumer<String, Int>, // error string, error code
private val setMediaSessionError: BiConsumer<CharSequence, Int>, // error string, error code
private val clearMediaSessionError: Runnable,
private val onPrepare: Consumer<Boolean>
) : PlaybackPreparer {
@@ -118,7 +118,7 @@ class MediaBrowserPlaybackPreparer(
private fun onPrepareError(throwable: Throwable) {
setMediaSessionError.accept(
ErrorInfo.getMessage(throwable, null, null).getString(context),
ErrorInfo.getMessage(throwable, null, null).getText(context),
PlaybackStateCompat.ERROR_CODE_APP_ERROR
)
}

View File

@@ -82,11 +82,11 @@ internal class PackageValidator(context: Context) {
// Build the caller info for the rest of the checks here.
val callerPackageInfo = buildCallerInfo(callingPackage)
?: throw IllegalStateException("Caller wasn't found in the system?")
?: error("Caller wasn't found in the system?")
// Verify that things aren't ... broken. (This test should always pass.)
if (callerPackageInfo.uid != callingUid) {
throw IllegalStateException("Caller's package UID doesn't match caller's actual UID?")
check(callerPackageInfo.uid == callingUid) {
"Caller's package UID doesn't match caller's actual UID?"
}
val callerSignature = callerPackageInfo.signature
@@ -202,7 +202,7 @@ internal class PackageValidator(context: Context) {
*/
private fun getSystemSignature(): String = getPackageInfo(ANDROID_PLATFORM)?.let { platformInfo ->
getSignature(platformInfo)
} ?: throw IllegalStateException("Platform signature not found")
} ?: error("Platform signature not found")
/**
* Creates a SHA-256 signature given a certificate byte array.

View File

@@ -72,7 +72,9 @@ public final class NotificationUtil {
notificationBuilder = createNotification();
}
updateNotification();
notificationManager.notify(NOTIFICATION_ID, notificationBuilder.build());
if (notificationManager.areNotificationsEnabled()) {
notificationManager.notify(NOTIFICATION_ID, notificationBuilder.build());
}
}
public synchronized void updateThumbnail() {
@@ -84,7 +86,9 @@ public final class NotificationUtil {
}
setLargeIcon(notificationBuilder);
notificationManager.notify(NOTIFICATION_ID, notificationBuilder.build());
if (notificationManager.areNotificationsEnabled()) {
notificationManager.notify(NOTIFICATION_ID, notificationBuilder.build());
}
}
}

View File

@@ -6,8 +6,8 @@ import android.view.MotionEvent;
import android.view.View;
import org.schabi.newpipe.util.Localization;
import org.schabi.newpipe.util.image.PicassoHelper;
import org.schabi.newpipe.util.ServiceHelper;
import org.schabi.newpipe.util.image.CoilHelper;
public class PlayQueueItemBuilder {
private static final String TAG = PlayQueueItemBuilder.class.toString();
@@ -33,7 +33,7 @@ public class PlayQueueItemBuilder {
holder.itemDurationView.setVisibility(View.GONE);
}
PicassoHelper.loadThumbnail(item.getThumbnails()).into(holder.itemThumbnailView);
CoilHelper.INSTANCE.loadThumbnail(holder.itemThumbnailView, item.getThumbnails());
holder.itemRoot.setOnClickListener(view -> {
if (onItemClickListener != null) {

View File

@@ -5,8 +5,8 @@ import androidx.annotation.NonNull;
import org.schabi.newpipe.extractor.stream.StreamInfo;
import org.schabi.newpipe.extractor.stream.StreamInfoItem;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;
public final class SinglePlayQueue extends PlayQueue {
public SinglePlayQueue(final StreamInfoItem item) {
@@ -29,11 +29,7 @@ public final class SinglePlayQueue extends PlayQueue {
}
private static List<PlayQueueItem> playQueueItemsOf(@NonNull final List<StreamInfoItem> items) {
final List<PlayQueueItem> playQueueItems = new ArrayList<>(items.size());
for (final StreamInfoItem item : items) {
playQueueItems.add(new PlayQueueItem(item));
}
return playQueueItems;
return items.stream().map(PlayQueueItem::new).collect(Collectors.toList());
}
@Override

View File

@@ -13,8 +13,9 @@ import androidx.collection.SparseArrayCompat;
import com.google.common.base.Stopwatch;
import org.schabi.newpipe.App;
import org.schabi.newpipe.extractor.stream.Frameset;
import org.schabi.newpipe.util.image.PicassoHelper;
import org.schabi.newpipe.util.image.CoilHelper;
import java.util.Comparator;
import java.util.List;
@@ -207,8 +208,8 @@ public class SeekbarPreviewThumbnailHolder {
Log.d(TAG, "Downloading bitmap for seekbarPreview from '" + url + "'");
// Gets the bitmap within the timeout of 15 seconds imposed by default by OkHttpClient
// Ensure that your are not running on the main-Thread this will otherwise hang
final Bitmap bitmap = PicassoHelper.loadSeekbarThumbnailPreview(url).get();
// Ensure that you are not running on the main thread, otherwise this will hang
final var bitmap = CoilHelper.INSTANCE.loadBitmapBlocking(App.getInstance(), url);
if (sw != null) {
Log.d(TAG, "Download of bitmap for seekbarPreview from '" + url + "' took "

View File

@@ -77,6 +77,7 @@ import java.util.Collections;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.stream.Collectors;
public final class MainPlayerUi extends VideoPlayerUi implements View.OnLayoutChangeListener {
private static final String TAG = MainPlayerUi.class.getSimpleName();
@@ -749,13 +750,13 @@ public final class MainPlayerUi extends VideoPlayerUi implements View.OnLayoutCh
}
private int getNearestStreamSegmentPosition(final long playbackPosition) {
int nearestPosition = 0;
final List<StreamSegment> segments = player.getCurrentStreamInfo()
.map(StreamInfo::getStreamSegments)
.orElse(Collections.emptyList());
for (int i = 0; i < segments.size(); i++) {
if (segments.get(i).getStartTimeSeconds() * 1000L > playbackPosition) {
int nearestPosition = 0;
for (final var segment : segments) {
if (segment.getStartTimeSeconds() * 1000L > playbackPosition) {
break;
}
nearestPosition++;
@@ -816,22 +817,13 @@ public final class MainPlayerUi extends VideoPlayerUi implements View.OnLayoutCh
}
final int currentStream = playQueue.getIndex();
int before = 0;
int after = 0;
final List<PlayQueueItem> streams = playQueue.getStreams();
final int nStreams = streams.size();
for (int i = 0; i < nStreams; i++) {
if (i < currentStream) {
before += streams.get(i).getDuration();
} else {
after += streams.get(i).getDuration();
}
}
final long before = streams.subList(0, currentStream).stream()
.collect(Collectors.summingLong(PlayQueueItem::getDuration)) * 1000;
before *= 1000;
after *= 1000;
final long after = streams.subList(currentStream, streams.size()).stream()
.collect(Collectors.summingLong(PlayQueueItem::getDuration)) * 1000;
binding.itemsListHeaderDuration.setText(
String.format("%s/%s",

View File

@@ -16,7 +16,6 @@ import androidx.activity.result.ActivityResultLauncher;
import androidx.activity.result.contract.ActivityResultContracts;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.core.content.ContextCompat;
import androidx.preference.Preference;
import androidx.preference.PreferenceManager;
@@ -27,6 +26,7 @@ import org.schabi.newpipe.R;
import org.schabi.newpipe.error.ErrorInfo;
import org.schabi.newpipe.error.ErrorUtil;
import org.schabi.newpipe.error.UserAction;
import org.schabi.newpipe.local.subscription.SubscriptionsImportExportHelper;
import org.schabi.newpipe.settings.export.BackupFileLocator;
import org.schabi.newpipe.settings.export.ImportExportManager;
import org.schabi.newpipe.streams.io.NoFileManagerSafeGuard;
@@ -34,12 +34,10 @@ 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 java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
@@ -57,18 +55,22 @@ public class BackupRestoreSettingsFragment extends BasePreferenceFragment {
private final ActivityResultLauncher<Intent> requestExportPathLauncher =
registerForActivityResult(new ActivityResultContracts.StartActivityForResult(),
this::requestExportPathResult);
private SubscriptionsImportExportHelper importExportHelper;
@Override
public void onAttach(@NonNull final Context context) {
super.onAttach(context);
importExportHelper = new SubscriptionsImportExportHelper(this);
}
@Override
public void onCreatePreferences(@Nullable final Bundle savedInstanceState,
@Nullable final String rootKey) {
final File homeDir = ContextCompat.getDataDir(requireContext());
Objects.requireNonNull(homeDir);
manager = new ImportExportManager(new BackupFileLocator(homeDir));
manager = new ImportExportManager(new BackupFileLocator(requireContext()));
importExportDataPathKey = getString(R.string.import_export_data_path);
addPreferencesFromResourceRegistry();
final Preference importDataPreference = requirePreference(R.string.import_data);
@@ -123,6 +125,21 @@ public class BackupRestoreSettingsFragment extends BasePreferenceFragment {
alertDialog.show();
return true;
});
final Preference exportSubsPreference =
requirePreference(R.string.export_subscriptions_key);
exportSubsPreference.setOnPreferenceClickListener(reference -> {
importExportHelper.onExportSelected();
return true;
});
final Preference importSubsPreference =
requirePreference(R.string.import_subscriptions_key);
importSubsPreference.setOnPreferenceClickListener(preference -> {
importExportHelper.onImportPreviousSelected();
return true;
});
}
private void requestExportPathResult(final ActivityResult result) {
@@ -181,9 +198,7 @@ public class BackupRestoreSettingsFragment extends BasePreferenceFragment {
}
try {
if (!manager.ensureDbDirectoryExists()) {
throw new IOException("Could not create databases dir");
}
manager.ensureDbDirectoryExists();
// replace the current database
if (!manager.extractDb(file)) {

View File

@@ -19,12 +19,12 @@ import org.schabi.newpipe.extractor.NewPipe;
import org.schabi.newpipe.player.helper.PlayerHelper;
import org.schabi.newpipe.util.Localization;
import org.schabi.newpipe.util.image.ImageStrategy;
import org.schabi.newpipe.util.image.PicassoHelper;
import org.schabi.newpipe.util.image.PreferredImageQuality;
import java.io.IOException;
import java.util.Locale;
import coil3.SingletonImageLoader;
public class ContentSettingsFragment extends BasePreferenceFragment {
private String youtubeRestrictedModeEnabledKey;
@@ -74,14 +74,12 @@ public class ContentSettingsFragment extends BasePreferenceFragment {
(preference, newValue) -> {
ImageStrategy.setPreferredImageQuality(PreferredImageQuality
.fromPreferenceKey(requireContext(), (String) newValue));
try {
PicassoHelper.clearCache(preference.getContext());
Toast.makeText(preference.getContext(),
R.string.thumbnail_cache_wipe_complete_notice, Toast.LENGTH_SHORT)
final var loader = SingletonImageLoader.get(preference.getContext());
loader.getMemoryCache().clear();
loader.getDiskCache().clear();
Toast.makeText(preference.getContext(),
R.string.thumbnail_cache_wipe_complete_notice, Toast.LENGTH_SHORT)
.show();
} catch (final IOException e) {
Log.e(TAG, "Unable to clear Picasso cache", e);
}
return true;
});
}

View File

@@ -10,7 +10,6 @@ import org.schabi.newpipe.error.ErrorInfo;
import org.schabi.newpipe.error.ErrorUtil;
import org.schabi.newpipe.error.UserAction;
import org.schabi.newpipe.local.feed.notifications.NotificationWorker;
import org.schabi.newpipe.util.image.PicassoHelper;
import java.util.Optional;
@@ -25,8 +24,6 @@ public class DebugSettingsFragment extends BasePreferenceFragment {
requirePreference(R.string.allow_heap_dumping_key);
final Preference showMemoryLeaksPreference =
requirePreference(R.string.show_memory_leaks_key);
final Preference showImageIndicatorsPreference =
requirePreference(R.string.show_image_indicators_key);
final Preference checkNewStreamsPreference =
requirePreference(R.string.check_new_streams_key);
final Preference crashTheAppPreference =
@@ -54,11 +51,6 @@ public class DebugSettingsFragment extends BasePreferenceFragment {
showMemoryLeaksPreference.setSummary(R.string.leak_canary_not_available);
}
showImageIndicatorsPreference.setOnPreferenceChangeListener((preference, newValue) -> {
PicassoHelper.setIndicatorsEnabled((Boolean) newValue);
return true;
});
checkNewStreamsPreference.setOnPreferenceClickListener(preference -> {
NotificationWorker.runNow(preference.getContext());
return true;

View File

@@ -157,7 +157,7 @@ public final class NewPipeSettings {
prefs.getInt(disabledTunnelingAutomaticallyKey, -1) == 0
&& !prefs.getBoolean(disabledTunnelingKey, false);
if (App.getApp().isFirstRun()
if (App.getInstance().isFirstRun()
|| (wasDeviceBlacklistUpdated && !wasMediaTunnelingEnabledByUser)) {
setMediaTunneling(context);
}

View File

@@ -19,8 +19,8 @@ import org.schabi.newpipe.R;
import org.schabi.newpipe.database.subscription.SubscriptionEntity;
import org.schabi.newpipe.error.ErrorUtil;
import org.schabi.newpipe.local.subscription.SubscriptionManager;
import org.schabi.newpipe.util.image.PicassoHelper;
import org.schabi.newpipe.util.ThemeHelper;
import org.schabi.newpipe.util.image.CoilHelper;
import java.util.List;
import java.util.Vector;
@@ -190,7 +190,7 @@ public class SelectChannelFragment extends DialogFragment {
final SubscriptionEntity entry = subscriptions.get(position);
holder.titleView.setText(entry.getName());
holder.view.setOnClickListener(view -> clickedItem(position));
PicassoHelper.loadAvatar(entry.getAvatarUrl()).into(holder.thumbnailView);
CoilHelper.INSTANCE.loadAvatar(holder.thumbnailView, entry.getAvatarUrl());
}
@Override

View File

@@ -27,7 +27,7 @@ import org.schabi.newpipe.error.ErrorUtil;
import org.schabi.newpipe.error.UserAction;
import org.schabi.newpipe.local.playlist.LocalPlaylistManager;
import org.schabi.newpipe.local.playlist.RemotePlaylistManager;
import org.schabi.newpipe.util.image.PicassoHelper;
import org.schabi.newpipe.util.image.CoilHelper;
import java.util.List;
import java.util.Vector;
@@ -154,21 +154,17 @@ public class SelectPlaylistFragment extends DialogFragment {
final int position) {
final PlaylistLocalItem selectedItem = playlists.get(position);
if (selectedItem instanceof PlaylistMetadataEntry) {
final PlaylistMetadataEntry entry = ((PlaylistMetadataEntry) selectedItem);
if (selectedItem instanceof PlaylistMetadataEntry entry) {
holder.titleView.setText(entry.getOrderingName());
holder.view.setOnClickListener(view -> clickedItem(position));
PicassoHelper.loadPlaylistThumbnail(entry.getThumbnailUrl())
.into(holder.thumbnailView);
} else if (selectedItem instanceof PlaylistRemoteEntity) {
final PlaylistRemoteEntity entry = ((PlaylistRemoteEntity) selectedItem);
CoilHelper.INSTANCE.loadPlaylistThumbnail(holder.thumbnailView,
entry.getThumbnailUrl());
} else if (selectedItem instanceof PlaylistRemoteEntity entry) {
holder.titleView.setText(entry.getOrderingName());
holder.view.setOnClickListener(view -> clickedItem(position));
PicassoHelper.loadPlaylistThumbnail(entry.getThumbnailUrl())
.into(holder.thumbnailView);
CoilHelper.INSTANCE.loadPlaylistThumbnail(holder.thumbnailView,
entry.getThumbnailUrl());
}
}

View File

@@ -1,11 +1,13 @@
package org.schabi.newpipe.settings.export
import java.io.File
import android.content.Context
import java.nio.file.Path
import kotlin.io.path.div
/**
* Locates specific files of NewPipe based on the home directory of the app.
*/
class BackupFileLocator(private val homeDir: File) {
class BackupFileLocator(context: Context) {
companion object {
const val FILE_NAME_DB = "newpipe.db"
@@ -17,13 +19,8 @@ class BackupFileLocator(private val homeDir: File) {
const val FILE_NAME_JSON_PREFS = "preferences.json"
}
val dbDir by lazy { File(homeDir, "/databases") }
val db by lazy { File(dbDir, FILE_NAME_DB) }
val dbJournal by lazy { File(dbDir, "$FILE_NAME_DB-journal") }
val dbShm by lazy { File(dbDir, "$FILE_NAME_DB-shm") }
val dbWal by lazy { File(dbDir, "$FILE_NAME_DB-wal") }
val db: Path = context.getDatabasePath(FILE_NAME_DB).toPath()
val dbJournal: Path = db.resolveSibling("$FILE_NAME_DB-journal")
val dbShm: Path = db.resolveSibling("$FILE_NAME_DB-shm")
val dbWal: Path = db.resolveSibling("$FILE_NAME_DB-wal")
}

View File

@@ -9,6 +9,8 @@ import java.io.FileNotFoundException
import java.io.IOException
import java.io.ObjectOutputStream
import java.util.zip.ZipOutputStream
import kotlin.io.path.createParentDirectories
import kotlin.io.path.deleteIfExists
import org.schabi.newpipe.streams.io.SharpOutputStream
import org.schabi.newpipe.streams.io.StoredFileHelper
import org.schabi.newpipe.util.ZipHelper
@@ -28,11 +30,8 @@ class ImportExportManager(private val fileLocator: BackupFileLocator) {
// previous file size, the file will retain part of the previous content and be corrupted
ZipOutputStream(SharpOutputStream(file.openAndTruncateStream()).buffered()).use { outZip ->
// add the database
ZipHelper.addFileToZip(
outZip,
BackupFileLocator.FILE_NAME_DB,
fileLocator.db.path
)
val name = BackupFileLocator.FILE_NAME_DB
ZipHelper.addFileToZip(outZip, name, fileLocator.db)
// add the legacy vulnerable serialized preferences (will be removed in the future)
ZipHelper.addFileToZip(
@@ -61,11 +60,10 @@ class ImportExportManager(private val fileLocator: BackupFileLocator) {
/**
* Tries to create database directory if it does not exist.
*
* @return Whether the directory exists afterwards.
*/
fun ensureDbDirectoryExists(): Boolean {
return fileLocator.dbDir.exists() || fileLocator.dbDir.mkdir()
@Throws(IOException::class)
fun ensureDbDirectoryExists() {
fileLocator.db.createParentDirectories()
}
/**
@@ -75,16 +73,13 @@ class ImportExportManager(private val fileLocator: BackupFileLocator) {
* @return true if the database was successfully extracted, false otherwise
*/
fun extractDb(file: StoredFileHelper): Boolean {
val success = ZipHelper.extractFileFromZip(
file,
BackupFileLocator.FILE_NAME_DB,
fileLocator.db.path
)
val name = BackupFileLocator.FILE_NAME_DB
val success = ZipHelper.extractFileFromZip(file, name, fileLocator.db)
if (success) {
fileLocator.dbJournal.delete()
fileLocator.dbWal.delete()
fileLocator.dbShm.delete()
fileLocator.dbJournal.deleteIfExists()
fileLocator.dbWal.deleteIfExists()
fileLocator.dbShm.deleteIfExists()
}
return success

View File

@@ -1,58 +0,0 @@
package org.schabi.newpipe.settings.export;
import java.io.IOException;
import java.io.InputStream;
import java.io.ObjectInputStream;
import java.io.ObjectStreamClass;
import java.util.Set;
/**
* An {@link ObjectInputStream} that only allows preferences-related types to be deserialized, to
* prevent injections. The only allowed types are: all primitive types, all boxed primitive types,
* null, strings. HashMap, HashSet and arrays of previously defined types are also allowed. Sources:
* <a href="https://wiki.sei.cmu.edu/confluence/display/java/SER00-J.+Enable+serialization+compatibility+during+class+evolution">
* cmu.edu
* </a>,
* <a href="https://cheatsheetseries.owasp.org/cheatsheets/Deserialization_Cheat_Sheet.html#harden-your-own-javaioobjectinputstream">
* OWASP cheatsheet
* </a>,
* <a href="https://commons.apache.org/proper/commons-io/apidocs/src-html/org/apache/commons/io/serialization/ValidatingObjectInputStream.html#line-118">
* Apache's {@code ValidatingObjectInputStream}
* </a>
*/
public class PreferencesObjectInputStream extends ObjectInputStream {
/**
* Primitive types, strings and other built-in types do not pass through resolveClass() but
* instead have a custom encoding; see
* <a href="https://docs.oracle.com/javase/6/docs/platform/serialization/spec/protocol.html#10152">
* official docs</a>.
*/
private static final Set<String> CLASS_WHITELIST = Set.of(
"java.lang.Boolean",
"java.lang.Byte",
"java.lang.Character",
"java.lang.Short",
"java.lang.Integer",
"java.lang.Long",
"java.lang.Float",
"java.lang.Double",
"java.lang.Void",
"java.util.HashMap",
"java.util.HashSet"
);
public PreferencesObjectInputStream(final InputStream in) throws IOException {
super(in);
}
@Override
protected Class<?> resolveClass(final ObjectStreamClass desc)
throws ClassNotFoundException, IOException {
if (CLASS_WHITELIST.contains(desc.getName())) {
return super.resolveClass(desc);
} else {
throw new ClassNotFoundException("Class not allowed: " + desc.getName());
}
}
}

View File

@@ -0,0 +1,52 @@
/*
* SPDX-FileCopyrightText: 2024-2026 NewPipe contributors <https://newpipe.net>
* SPDX-License-Identifier: GPL-3.0-or-later
*/
package org.schabi.newpipe.settings.export
import java.io.IOException
import java.io.InputStream
import java.io.ObjectInputStream
import java.io.ObjectStreamClass
/**
* An [ObjectInputStream] that only allows preferences-related types to be deserialized, to
* prevent injections. The only allowed types are: all primitive types, all boxed primitive types,
* null, strings. HashMap, HashSet and arrays of previously defined types are also allowed. Sources:
* [cmu.edu](https://wiki.sei.cmu.edu/confluence/display/java/SER00-J.+Enable+serialization+compatibility+during+class+evolution) * ,
* [OWASP cheatsheet](https://cheatsheetseries.owasp.org/cheatsheets/Deserialization_Cheat_Sheet.html#harden-your-own-javaioobjectinputstream) * ,
* [Apache's `ValidatingObjectInputStream`](https://commons.apache.org/proper/commons-io/apidocs/src-html/org/apache/commons/io/serialization/ValidatingObjectInputStream.html#line-118) *
*/
class PreferencesObjectInputStream(stream: InputStream) : ObjectInputStream(stream) {
@Throws(ClassNotFoundException::class, IOException::class)
override fun resolveClass(desc: ObjectStreamClass): Class<*> {
if (desc.name in CLASS_WHITELIST) {
return super.resolveClass(desc)
} else {
throw ClassNotFoundException("Class not allowed: $desc.name")
}
}
companion object {
/**
* Primitive types, strings and other built-in types do not pass through resolveClass() but
* instead have a custom encoding; see
* [
* official docs](https://docs.oracle.com/javase/6/docs/platform/serialization/spec/protocol.html#10152).
*/
private val CLASS_WHITELIST = setOf<String>(
"java.lang.Boolean",
"java.lang.Byte",
"java.lang.Character",
"java.lang.Short",
"java.lang.Integer",
"java.lang.Long",
"java.lang.Float",
"java.lang.Double",
"java.lang.Void",
"java.util.HashMap",
"java.util.HashSet"
)
}
}

View File

@@ -251,7 +251,7 @@ public final class SettingMigrations {
final int lastPrefVersion = sp.getInt(lastPrefVersionKey, 0);
// no migration to run, already up to date
if (App.getApp().isFirstRun()) {
if (App.getInstance().isFirstRun()) {
sp.edit().putInt(lastPrefVersionKey, VERSION).apply();
return;
} else if (lastPrefVersion == VERSION) {

View File

@@ -26,14 +26,13 @@ data class PreferenceSearchItem(
val breadcrumbs: String,
@XmlRes val searchIndexItemResId: Int
) {
val allRelevantSearchFields: List<String>
get() = listOf(title, summary, entries, breadcrumbs)
fun hasData(): Boolean {
return !key.isEmpty() && !title.isEmpty()
}
fun getAllRelevantSearchFields(): MutableList<String?> {
return mutableListOf(title, summary, entries, breadcrumbs)
}
override fun toString(): String {
return "PreferenceItem: $title $summary $key"
}

View File

@@ -9,8 +9,9 @@ import com.grack.nanojson.JsonParserException;
import com.grack.nanojson.JsonStringWriter;
import com.grack.nanojson.JsonWriter;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.stream.Collectors;
/**
* Class to get a JSON representation of a list of tabs, and the other way around.
@@ -44,39 +45,25 @@ public final class TabsJsonHelper {
return getDefaultTabs();
}
final List<Tab> returnTabs = new ArrayList<>();
final JsonObject outerJsonObject;
try {
outerJsonObject = JsonParser.object().from(tabsJson);
final JsonObject outerJsonObject = JsonParser.object().from(tabsJson);
if (!outerJsonObject.has(JSON_TABS_ARRAY_KEY)) {
throw new InvalidJsonException("JSON doesn't contain \"" + JSON_TABS_ARRAY_KEY
+ "\" array");
}
final JsonArray tabsArray = outerJsonObject.getArray(JSON_TABS_ARRAY_KEY);
final JsonArray tabsArray = outerJsonObject.getArray(JSON_TABS_ARRAY_KEY, null);
for (final Object o : tabsArray) {
if (!(o instanceof JsonObject)) {
continue;
}
final var returnTabs = tabsArray.streamAsJsonObjects()
.map(Tab::from)
.filter(Objects::nonNull)
.collect(Collectors.toUnmodifiableList());
final Tab tab = Tab.from((JsonObject) o);
if (tab != null) {
returnTabs.add(tab);
}
}
return returnTabs.isEmpty() ? getDefaultTabs() : returnTabs;
} catch (final JsonParserException e) {
throw new InvalidJsonException(e);
}
if (returnTabs.isEmpty()) {
return getDefaultTabs();
}
return returnTabs;
}
/**

View File

@@ -1,51 +0,0 @@
package org.schabi.newpipe.util;
import android.content.Context;
import android.content.SharedPreferences;
import androidx.preference.PreferenceManager;
import org.schabi.newpipe.R;
/**
* For preferences with dependencies and multiple use case,
* this class can be used to reduce the lines of code.
*/
public final class DependentPreferenceHelper {
private DependentPreferenceHelper() {
// no instance
}
/**
* Option `Resume playback` depends on `Watch history`, this method can be used to retrieve if
* `Resume playback` and its dependencies are all enabled.
*
* @param context the Android context
* @return returns true if `Resume playback` and `Watch history` are both enabled
*/
public static boolean getResumePlaybackEnabled(final Context context) {
final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
return prefs.getBoolean(context.getString(
R.string.enable_watch_history_key), true)
&& prefs.getBoolean(context.getString(
R.string.enable_playback_resume_key), true);
}
/**
* Option `Position in lists` depends on `Watch history`, this method can be used to retrieve if
* `Position in lists` and its dependencies are all enabled.
*
* @param context the Android context
* @return returns true if `Positions in lists` and `Watch history` are both enabled
*/
public static boolean getPositionsInListsEnabled(final Context context) {
final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
return prefs.getBoolean(context.getString(
R.string.enable_watch_history_key), true)
&& prefs.getBoolean(context.getString(
R.string.enable_playback_state_lists_key), true);
}
}

View File

@@ -0,0 +1,46 @@
/*
* SPDX-FileCopyrightText: 2023-2026 NewPipe contributors <https://newpipe.net>
* SPDX-License-Identifier: GPL-3.0-or-later
*/
package org.schabi.newpipe.util
import android.content.Context
import androidx.preference.PreferenceManager
import org.schabi.newpipe.R
/**
* For preferences with dependencies and multiple use case,
* this class can be used to reduce the lines of code.
*/
object DependentPreferenceHelper {
/**
* Option `Resume playback` depends on `Watch history`, this method can be used to retrieve if
* `Resume playback` and its dependencies are all enabled.
*
* @param context the Android context
* @return returns true if `Resume playback` and `Watch history` are both enabled
*/
@JvmStatic
fun getResumePlaybackEnabled(context: Context): Boolean {
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
return prefs.getBoolean(context.getString(R.string.enable_watch_history_key), true) &&
prefs.getBoolean(context.getString(R.string.enable_playback_resume_key), true)
}
/**
* Option `Position in lists` depends on `Watch history`, this method can be used to retrieve if
* `Position in lists` and its dependencies are all enabled.
*
* @param context the Android context
* @return returns true if `Positions in lists` and `Watch history` are both enabled
*/
@JvmStatic
fun getPositionsInListsEnabled(context: Context): Boolean {
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
return prefs.getBoolean(context.getString(R.string.enable_watch_history_key), true) &&
prefs.getBoolean(context.getString(R.string.enable_playback_state_lists_key), true)
}
}

View File

@@ -131,7 +131,7 @@ public final class DeviceUtils {
}
isFireTV =
App.getApp().getPackageManager().hasSystemFeature(AMAZON_FEATURE_FIRE_TV);
App.getInstance().getPackageManager().hasSystemFeature(AMAZON_FEATURE_FIRE_TV);
return isFireTV;
}
@@ -140,7 +140,7 @@ public final class DeviceUtils {
return isTV;
}
final PackageManager pm = App.getApp().getPackageManager();
final PackageManager pm = App.getInstance().getPackageManager();
// from doc: https://developer.android.com/training/tv/start/hardware.html#runtime-check
boolean isTv = ContextCompat.getSystemService(context, UiModeManager.class)

View File

@@ -48,7 +48,7 @@ public final class KeyboardUtil {
final InputMethodManager imm = ContextCompat.getSystemService(activity,
InputMethodManager.class);
imm.hideSoftInputFromWindow(editText.getWindowToken(),
InputMethodManager.RESULT_UNCHANGED_SHOWN);
InputMethodManager.HIDE_NOT_ALWAYS);
editText.clearFocus();
}

View File

@@ -426,12 +426,24 @@ public final class Localization {
return new BigDecimal(value).setScale(scale, RoundingMode.HALF_UP).doubleValue();
}
/**
* A wrapper around {@code context.getResources().getQuantityString()} with some safeguard.
*
* @param context the Android context
* @param pluralId the ID of the plural resource
* @param zeroCaseStringId the resource ID of the string to use in case {@code count=0},
* or 0 if the plural resource should be used in the zero case too
* @param count the number that should be used to pick the correct plural form
* @param formattedCount the formatting parameter to substitute inside the plural resource,
* ideally just {@code count} converted to string
* @return the formatted string with the correct pluralization
*/
private static String getQuantity(@NonNull final Context context,
@PluralsRes final int pluralId,
@StringRes final int zeroCaseStringId,
final long count,
final String formattedCount) {
if (count == 0) {
if (count == 0 && zeroCaseStringId != 0) {
return context.getString(zeroCaseStringId);
}

View File

@@ -501,6 +501,7 @@ public final class NavigationHelper {
public static void openCommentRepliesFragment(@NonNull final FragmentActivity activity,
@NonNull final CommentsInfoItem comment) {
closeCommentRepliesFragments(activity);
defaultTransaction(activity.getSupportFragmentManager())
.replace(R.id.fragment_holder, new CommentRepliesFragment(comment),
CommentRepliesFragment.TAG)
@@ -508,6 +509,41 @@ public final class NavigationHelper {
.commit();
}
/**
* Closes all open {@link CommentRepliesFragment}s in {@code activity},
* including those that are not at the top of the back stack.
* This is needed to prevent multiple open CommentRepliesFragments
* Ideally there should only be one since we remove existing before opening a new one.
* @param activity the activity in which to close the CommentRepliesFragments
*/
public static void closeCommentRepliesFragments(@NonNull final FragmentActivity activity) {
final FragmentManager fm = activity.getSupportFragmentManager();
// Remove all existing fragment instances tagged as CommentRepliesFragment
final FragmentTransaction tx = defaultTransaction(fm);
boolean removed = false;
for (final Fragment fragment : fm.getFragments()) {
if (fragment != null && CommentRepliesFragment.TAG.equals(fragment.getTag())) {
tx.remove(fragment);
removed = true;
}
}
if (removed) {
tx.commit();
}
// Only pop back stack entries named CommentRepliesFragment.TAG if they are at the top.
while (fm.getBackStackEntryCount() > 0
&& CommentRepliesFragment.TAG.equals(
fm.getBackStackEntryAt(fm.getBackStackEntryCount() - 1).getName()
)
) {
fm.popBackStackImmediate(CommentRepliesFragment.TAG,
FragmentManager.POP_BACK_STACK_INCLUSIVE);
}
}
public static void openPlaylistFragment(final FragmentManager fragmentManager,
final int serviceId, final String url,
@NonNull final String name) {

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