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

Compare commits

...

54 Commits
v0.28.3 ... dev

Author SHA1 Message Date
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
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
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
37 changed files with 911 additions and 1159 deletions

View File

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

View File

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

View File

@@ -3,6 +3,8 @@
* SPDX-License-Identifier: GPL-3.0-or-later * SPDX-License-Identifier: GPL-3.0-or-later
*/ */
import com.android.build.api.dsl.ApplicationExtension
plugins { plugins {
alias(libs.plugins.android.application) alias(libs.plugins.android.application)
alias(libs.plugins.jetbrains.kotlin.android) alias(libs.plugins.jetbrains.kotlin.android)
@@ -32,7 +34,7 @@ kotlin {
} }
} }
android { configure<ApplicationExtension> {
compileSdk = 36 compileSdk = 36
namespace = "org.schabi.newpipe" namespace = "org.schabi.newpipe"
@@ -78,7 +80,10 @@ android {
} }
isMinifyEnabled = true isMinifyEnabled = true
isShrinkResources = false // disabled to fix F-Droid"s reproducible build isShrinkResources = false // disabled to fix F-Droid"s reproducible build
proguardFiles(getDefaultProguardFile("proguard-android.txt"), "proguard-rules.pro") proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro"
)
} }
} }
@@ -100,7 +105,7 @@ android {
sourceSets { sourceSets {
getByName("androidTest") { getByName("androidTest") {
assets.srcDir("$projectDir/schemas") assets.directories += "$projectDir/schemas"
} }
} }
@@ -111,6 +116,7 @@ android {
buildFeatures { buildFeatures {
viewBinding = true viewBinding = true
buildConfig = true buildConfig = true
resValues = true
} }
packaging { packaging {

View File

@@ -39,3 +39,8 @@
## For some reason NotificationModeConfigFragment wasn't kept (only referenced in a preference xml) ## For some reason NotificationModeConfigFragment wasn't kept (only referenced in a preference xml)
-keep class org.schabi.newpipe.settings.notifications.** { *; } -keep class org.schabi.newpipe.settings.notifications.** { *; }
# Prevent R8 from stripping or renaming Protobuf internal fields
-keepclassmembers class * extends com.google.protobuf.GeneratedMessageLite {
<fields>;
}

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

@@ -92,7 +92,7 @@ class AboutActivity : AppCompatActivity() {
return when (position) { return when (position) {
posAbout -> AboutFragment() posAbout -> AboutFragment()
posLicense -> LicenseFragment.newInstance(SOFTWARE_COMPONENTS) 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) { return when (position) {
posAbout -> R.string.tab_about posAbout -> R.string.tab_about
posLicense -> R.string.tab_licenses posLicense -> R.string.tab_licenses
else -> throw IllegalArgumentException("Unknown position for ViewPager2") else -> error("Unknown position for ViewPager2")
} }
} }
} }

View File

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

View File

@@ -87,7 +87,7 @@ abstract class StreamDAO : BasicDAO<StreamEntity> {
private fun compareAndUpdateStream(newerStream: StreamEntity) { private fun compareAndUpdateStream(newerStream: StreamEntity) {
val existentMinimalStream = getMinimalStreamForCompare(newerStream.serviceId, newerStream.url) 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 newerStream.uid = existentMinimalStream.uid
if (!StreamTypeUtil.isLiveStream(newerStream.streamType)) { if (!StreamTypeUtil.isLiveStream(newerStream.streamType)) {

View File

@@ -100,7 +100,7 @@ abstract class SubscriptionDAO : BasicDAO<SubscriptionEntity> {
entity.uid = uidFromInsert entity.uid = uidFromInsert
} else { } else {
val subscriptionIdFromDb = getSubscriptionIdInternal(entity.serviceId, entity.url!!) 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 entity.uid = subscriptionIdFromDb
update(entity) update(entity)

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,280 @@
/*
* 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
/**
* 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.text = 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)
.replace("\\n", "\n")
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.isNotEmpty()) {
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.isNotEmpty()) {
append(index + 1)
}
append("</b>")
append("</summary><p>\n")
append("\n```\n${stacktrace}\n```\n")
append("</details>\n")
}
// make sure to close everything
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

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

@@ -129,8 +129,7 @@ class FeedViewModel(
fun setSaveShowPlayedItems(showPlayedItems: Boolean) { fun setSaveShowPlayedItems(showPlayedItems: Boolean) {
this.showPlayedItems.onNext(showPlayedItems) this.showPlayedItems.onNext(showPlayedItems)
PreferenceManager.getDefaultSharedPreferences(application).edit { PreferenceManager.getDefaultSharedPreferences(application).edit {
this.putBoolean(application.getString(R.string.feed_show_watched_items_key), showPlayedItems) putBoolean(application.getString(R.string.feed_show_watched_items_key), showPlayedItems)
this.apply()
} }
} }
@@ -139,8 +138,7 @@ class FeedViewModel(
fun setSaveShowPartiallyPlayedItems(showPartiallyPlayedItems: Boolean) { fun setSaveShowPartiallyPlayedItems(showPartiallyPlayedItems: Boolean) {
this.showPartiallyPlayedItems.onNext(showPartiallyPlayedItems) this.showPartiallyPlayedItems.onNext(showPartiallyPlayedItems)
PreferenceManager.getDefaultSharedPreferences(application).edit { PreferenceManager.getDefaultSharedPreferences(application).edit {
this.putBoolean(application.getString(R.string.feed_show_partially_watched_items_key), showPartiallyPlayedItems) putBoolean(application.getString(R.string.feed_show_partially_watched_items_key), showPartiallyPlayedItems)
this.apply()
} }
} }
@@ -149,8 +147,7 @@ class FeedViewModel(
fun setSaveShowFutureItems(showFutureItems: Boolean) { fun setSaveShowFutureItems(showFutureItems: Boolean) {
this.showFutureItems.onNext(showFutureItems) this.showFutureItems.onNext(showFutureItems)
PreferenceManager.getDefaultSharedPreferences(application).edit { PreferenceManager.getDefaultSharedPreferences(application).edit {
this.putBoolean(application.getString(R.string.feed_show_future_items_key), showFutureItems) putBoolean(application.getString(R.string.feed_show_future_items_key), showFutureItems)
this.apply()
} }
} }

View File

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

View File

@@ -327,7 +327,7 @@ class FeedGroupDialog : DialogFragment(), BackPressable {
groupIcon = feedGroupEntity?.icon groupIcon = feedGroupEntity?.icon
groupSortOrder = feedGroupEntity?.sortOrder ?: -1 groupSortOrder = feedGroupEntity?.sortOrder ?: -1
val feedGroupIcon = if (selectedIcon == null) icon else selectedIcon!! val feedGroupIcon = selectedIcon ?: icon
feedGroupCreateBinding.iconPreview.setImageResource(feedGroupIcon.getDrawableRes()) feedGroupCreateBinding.iconPreview.setImageResource(feedGroupIcon.getDrawableRes())
if (feedGroupCreateBinding.groupNameInput.text.isNullOrBlank()) { if (feedGroupCreateBinding.groupNameInput.text.isNullOrBlank()) {

View File

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

View File

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

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

@@ -26,14 +26,13 @@ data class PreferenceSearchItem(
val breadcrumbs: String, val breadcrumbs: String,
@XmlRes val searchIndexItemResId: Int @XmlRes val searchIndexItemResId: Int
) { ) {
val allRelevantSearchFields: List<String>
get() = listOf(title, summary, entries, breadcrumbs)
fun hasData(): Boolean { fun hasData(): Boolean {
return !key.isEmpty() && !title.isEmpty() return !key.isEmpty() && !title.isEmpty()
} }
fun getAllRelevantSearchFields(): MutableList<String?> {
return mutableListOf(title, summary, entries, breadcrumbs)
}
override fun toString(): String { override fun toString(): String {
return "PreferenceItem: $title $summary $key" 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.JsonStringWriter;
import com.grack.nanojson.JsonWriter; import com.grack.nanojson.JsonWriter;
import java.util.ArrayList;
import java.util.List; 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. * 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(); return getDefaultTabs();
} }
final List<Tab> returnTabs = new ArrayList<>();
final JsonObject outerJsonObject;
try { try {
outerJsonObject = JsonParser.object().from(tabsJson); final JsonObject outerJsonObject = JsonParser.object().from(tabsJson);
if (!outerJsonObject.has(JSON_TABS_ARRAY_KEY)) { if (!outerJsonObject.has(JSON_TABS_ARRAY_KEY)) {
throw new InvalidJsonException("JSON doesn't contain \"" + JSON_TABS_ARRAY_KEY throw new InvalidJsonException("JSON doesn't contain \"" + JSON_TABS_ARRAY_KEY
+ "\" array"); + "\" 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) { final var returnTabs = tabsArray.streamAsJsonObjects()
if (!(o instanceof JsonObject)) { .map(Tab::from)
continue; .filter(Objects::nonNull)
} .collect(Collectors.toUnmodifiableList());
final Tab tab = Tab.from((JsonObject) o); return returnTabs.isEmpty() ? getDefaultTabs() : returnTabs;
if (tab != null) {
returnTabs.add(tab);
}
}
} catch (final JsonParserException e) { } catch (final JsonParserException e) {
throw new InvalidJsonException(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

@@ -1,61 +0,0 @@
package org.schabi.newpipe.util;
import android.content.Context;
import android.text.Selection;
import android.text.Spannable;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import org.schabi.newpipe.util.external_communication.ShareUtils;
import org.schabi.newpipe.views.NewPipeEditText;
import org.schabi.newpipe.views.NewPipeTextView;
public final class NewPipeTextViewHelper {
private NewPipeTextViewHelper() {
}
/**
* Share the selected text of {@link NewPipeTextView NewPipeTextViews} and
* {@link NewPipeEditText NewPipeEditTexts} with
* {@link ShareUtils#shareText(Context, String, String)}.
*
* <p>
* This allows EMUI users to get the Android share sheet instead of the EMUI share sheet when
* using the {@code Share} command of the popup menu which appears when selecting text.
* </p>
*
* @param textView the {@link TextView} on which sharing the selected text. It should be a
* {@link NewPipeTextView} or a {@link NewPipeEditText} (even if
* {@link TextView standard TextViews} are supported).
*/
public static void shareSelectedTextWithShareUtils(@NonNull final TextView textView) {
final CharSequence textViewText = textView.getText();
shareSelectedTextIfNotNullAndNotEmpty(textView, getSelectedText(textView, textViewText));
if (textViewText instanceof Spannable) {
Selection.setSelection((Spannable) textViewText, textView.getSelectionEnd());
}
}
@Nullable
private static CharSequence getSelectedText(@NonNull final TextView textView,
@Nullable final CharSequence text) {
if (!textView.hasSelection() || text == null) {
return null;
}
final int start = textView.getSelectionStart();
final int end = textView.getSelectionEnd();
return String.valueOf(start > end ? text.subSequence(end, start)
: text.subSequence(start, end));
}
private static void shareSelectedTextIfNotNullAndNotEmpty(
@NonNull final TextView textView,
@Nullable final CharSequence selectedText) {
if (selectedText != null && selectedText.length() != 0) {
ShareUtils.shareText(textView.getContext(), "", selectedText.toString());
}
}
}

View File

@@ -0,0 +1,60 @@
/*
* SPDX-FileCopyrightText: 2021-2026 NewPipe contributors <https://newpipe.net>
* SPDX-License-Identifier: GPL-3.0-or-later
*/
package org.schabi.newpipe.util
import android.text.Selection
import android.text.Spannable
import android.widget.TextView
import org.schabi.newpipe.util.external_communication.ShareUtils
object NewPipeTextViewHelper {
/**
* Share the selected text of [NewPipeTextViews][org.schabi.newpipe.views.NewPipeTextView] and
* [NewPipeEditTexts][org.schabi.newpipe.views.NewPipeEditText] with
* [ShareUtils.shareText].
*
*
*
* This allows EMUI users to get the Android share sheet instead of the EMUI share sheet when
* using the `Share` command of the popup menu which appears when selecting text.
*
*
* @param textView the [TextView] on which sharing the selected text. It should be a
* [org.schabi.newpipe.views.NewPipeTextView] or a [org.schabi.newpipe.views.NewPipeEditText]
* (even if [standard TextViews][TextView] are supported).
*/
@JvmStatic
fun shareSelectedTextWithShareUtils(textView: TextView) {
val textViewText = textView.getText()
shareSelectedTextIfNotNullAndNotEmpty(textView, getSelectedText(textView, textViewText))
if (textViewText is Spannable) {
Selection.setSelection(textViewText, textView.selectionEnd)
}
}
private fun getSelectedText(textView: TextView, text: CharSequence?): CharSequence? {
if (!textView.hasSelection() || text == null) {
return null
}
val start = textView.selectionStart
val end = textView.selectionEnd
return if (start > end) {
text.subSequence(end, start)
} else {
text.subSequence(start, end)
}
}
private fun shareSelectedTextIfNotNullAndNotEmpty(
textView: TextView,
selectedText: CharSequence?
) {
if (!selectedText.isNullOrEmpty()) {
ShareUtils.shareText(textView.context, "", selectedText.toString())
}
}
}

View File

@@ -1,69 +0,0 @@
package org.schabi.newpipe.util;
import android.content.Context;
import android.content.SharedPreferences;
import androidx.preference.PreferenceManager;
import com.grack.nanojson.JsonArray;
import com.grack.nanojson.JsonObject;
import com.grack.nanojson.JsonParser;
import com.grack.nanojson.JsonParserException;
import com.grack.nanojson.JsonStringWriter;
import com.grack.nanojson.JsonWriter;
import org.schabi.newpipe.R;
import org.schabi.newpipe.extractor.ServiceList;
import org.schabi.newpipe.extractor.services.peertube.PeertubeInstance;
import java.util.ArrayList;
import java.util.List;
public final class PeertubeHelper {
private PeertubeHelper() { }
public static List<PeertubeInstance> getInstanceList(final Context context) {
final SharedPreferences sharedPreferences = PreferenceManager
.getDefaultSharedPreferences(context);
final String savedInstanceListKey = context.getString(R.string.peertube_instance_list_key);
final String savedJson = sharedPreferences.getString(savedInstanceListKey, null);
if (null == savedJson) {
return List.of(getCurrentInstance());
}
try {
final JsonArray array = JsonParser.object().from(savedJson).getArray("instances");
final List<PeertubeInstance> result = new ArrayList<>();
for (final Object o : array) {
if (o instanceof JsonObject) {
final JsonObject instance = (JsonObject) o;
final String name = instance.getString("name");
final String url = instance.getString("url");
result.add(new PeertubeInstance(url, name));
}
}
return result;
} catch (final JsonParserException e) {
return List.of(getCurrentInstance());
}
}
public static PeertubeInstance selectInstance(final PeertubeInstance instance,
final Context context) {
final SharedPreferences sharedPreferences = PreferenceManager
.getDefaultSharedPreferences(context);
final String selectedInstanceKey =
context.getString(R.string.peertube_selected_instance_key);
final JsonStringWriter jsonWriter = JsonWriter.string().object();
jsonWriter.value("name", instance.getName());
jsonWriter.value("url", instance.getUrl());
final String jsonToSave = jsonWriter.end().done();
sharedPreferences.edit().putString(selectedInstanceKey, jsonToSave).apply();
ServiceList.PeerTube.setInstance(instance);
return instance;
}
public static PeertubeInstance getCurrentInstance() {
return ServiceList.PeerTube.getInstance();
}
}

View File

@@ -0,0 +1,52 @@
/*
* SPDX-FileCopyrightText: 2019-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.core.content.edit
import androidx.preference.PreferenceManager
import com.grack.nanojson.JsonObject
import com.grack.nanojson.JsonParser
import com.grack.nanojson.JsonWriter
import org.schabi.newpipe.R
import org.schabi.newpipe.extractor.ServiceList
import org.schabi.newpipe.extractor.services.peertube.PeertubeInstance
object PeertubeHelper {
@JvmStatic
val currentInstance: PeertubeInstance
get() = ServiceList.PeerTube.instance
@JvmStatic
fun getInstanceList(context: Context): List<PeertubeInstance> {
val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context)
val savedInstanceListKey = context.getString(R.string.peertube_instance_list_key)
val savedJson = sharedPreferences.getString(savedInstanceListKey, null)
?: return listOf(currentInstance)
return runCatching {
JsonParser.`object`().from(savedJson).getArray("instances")
.filterIsInstance<JsonObject>()
.map { PeertubeInstance(it.getString("url"), it.getString("name")) }
}.getOrDefault(listOf(currentInstance))
}
@JvmStatic
fun selectInstance(instance: PeertubeInstance, context: Context): PeertubeInstance {
val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context)
val selectedInstanceKey = context.getString(R.string.peertube_selected_instance_key)
val jsonWriter = JsonWriter.string().`object`()
jsonWriter.value("name", instance.name)
jsonWriter.value("url", instance.url)
val jsonToSave = jsonWriter.end().done()
sharedPreferences.edit { putString(selectedInstanceKey, jsonToSave) }
ServiceList.PeerTube.instance = instance
return instance
}
}

View File

@@ -1,94 +0,0 @@
package org.schabi.newpipe.util;
import android.content.Context;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AppCompatActivity;
import androidx.preference.PreferenceManager;
import org.schabi.newpipe.R;
import org.schabi.newpipe.databinding.PlaylistControlBinding;
import org.schabi.newpipe.fragments.list.playlist.PlaylistControlViewHolder;
import org.schabi.newpipe.player.PlayerType;
/**
* Utility class for play buttons and their respective click listeners.
*/
public final class PlayButtonHelper {
private PlayButtonHelper() {
// utility class
}
/**
* Initialize {@link android.view.View.OnClickListener OnClickListener}
* and {@link android.view.View.OnLongClickListener OnLongClickListener} for playlist control
* buttons defined in {@link R.layout#playlist_control}.
*
* @param activity The activity to use for the {@link android.widget.Toast Toast}.
* @param playlistControlBinding The binding of the
* {@link R.layout#playlist_control playlist control layout}.
* @param fragment The fragment to get the play queue from.
*/
public static void initPlaylistControlClickListener(
@NonNull final AppCompatActivity activity,
@NonNull final PlaylistControlBinding playlistControlBinding,
@NonNull final PlaylistControlViewHolder fragment) {
// click listener
playlistControlBinding.playlistCtrlPlayAllButton.setOnClickListener(view -> {
NavigationHelper.playOnMainPlayer(activity, fragment.getPlayQueue());
showHoldToAppendToastIfNeeded(activity);
});
playlistControlBinding.playlistCtrlPlayPopupButton.setOnClickListener(view -> {
NavigationHelper.playOnPopupPlayer(activity, fragment.getPlayQueue(), false);
showHoldToAppendToastIfNeeded(activity);
});
playlistControlBinding.playlistCtrlPlayBgButton.setOnClickListener(view -> {
NavigationHelper.playOnBackgroundPlayer(activity, fragment.getPlayQueue(), false);
showHoldToAppendToastIfNeeded(activity);
});
// long click listener
playlistControlBinding.playlistCtrlPlayAllButton.setOnLongClickListener(view -> {
NavigationHelper.enqueueOnPlayer(activity, fragment.getPlayQueue(), PlayerType.MAIN);
return true;
});
playlistControlBinding.playlistCtrlPlayPopupButton.setOnLongClickListener(view -> {
NavigationHelper.enqueueOnPlayer(activity, fragment.getPlayQueue(), PlayerType.POPUP);
return true;
});
playlistControlBinding.playlistCtrlPlayBgButton.setOnLongClickListener(view -> {
NavigationHelper.enqueueOnPlayer(activity, fragment.getPlayQueue(), PlayerType.AUDIO);
return true;
});
}
/**
* Show the "hold to append" toast if the corresponding preference is enabled.
*
* @param context The context to show the toast.
*/
private static void showHoldToAppendToastIfNeeded(@NonNull final Context context) {
if (shouldShowHoldToAppendTip(context)) {
Toast.makeText(context, R.string.hold_to_append, Toast.LENGTH_SHORT).show();
}
}
/**
* Check if the "hold to append" toast should be shown.
*
* <p>
* The tip is shown if the corresponding preference is enabled.
* This is the default behaviour.
* </p>
*
* @param context The context to get the preference.
* @return {@code true} if the tip should be shown, {@code false} otherwise.
*/
public static boolean shouldShowHoldToAppendTip(@NonNull final Context context) {
return PreferenceManager.getDefaultSharedPreferences(context)
.getBoolean(context.getString(R.string.show_hold_to_append_key), true);
}
}

View File

@@ -0,0 +1,96 @@
/*
* 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 android.view.View
import android.view.View.OnLongClickListener
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
import androidx.preference.PreferenceManager
import org.schabi.newpipe.R
import org.schabi.newpipe.databinding.PlaylistControlBinding
import org.schabi.newpipe.fragments.list.playlist.PlaylistControlViewHolder
import org.schabi.newpipe.player.PlayerType
/**
* Utility class for play buttons and their respective click listeners.
*/
object PlayButtonHelper {
/**
* Initialize [OnClickListener][View.OnClickListener]
* and [OnLongClickListener][OnLongClickListener] for playlist control
* buttons defined in [R.layout.playlist_control].
*
* @param activity The activity to use for the [Toast][Toast].
* @param playlistControlBinding The binding of the
* [playlist control layout][R.layout.playlist_control].
* @param fragment The fragment to get the play queue from.
*/
@JvmStatic
fun initPlaylistControlClickListener(
activity: AppCompatActivity,
playlistControlBinding: PlaylistControlBinding,
fragment: PlaylistControlViewHolder
) {
// click listener
playlistControlBinding.playlistCtrlPlayAllButton.setOnClickListener {
NavigationHelper.playOnMainPlayer(activity, fragment.getPlayQueue())
showHoldToAppendToastIfNeeded(activity)
}
playlistControlBinding.playlistCtrlPlayPopupButton.setOnClickListener {
NavigationHelper.playOnPopupPlayer(activity, fragment.getPlayQueue(), false)
showHoldToAppendToastIfNeeded(activity)
}
playlistControlBinding.playlistCtrlPlayBgButton.setOnClickListener {
NavigationHelper.playOnBackgroundPlayer(activity, fragment.getPlayQueue(), false)
showHoldToAppendToastIfNeeded(activity)
}
// long click listener
playlistControlBinding.playlistCtrlPlayAllButton.setOnLongClickListener {
NavigationHelper.enqueueOnPlayer(activity, fragment.getPlayQueue(), PlayerType.MAIN)
true
}
playlistControlBinding.playlistCtrlPlayPopupButton.setOnLongClickListener {
NavigationHelper.enqueueOnPlayer(activity, fragment.getPlayQueue(), PlayerType.POPUP)
true
}
playlistControlBinding.playlistCtrlPlayBgButton.setOnLongClickListener {
NavigationHelper.enqueueOnPlayer(activity, fragment.getPlayQueue(), PlayerType.AUDIO)
true
}
}
/**
* Show the "hold to append" toast if the corresponding preference is enabled.
*
* @param context The context to show the toast.
*/
private fun showHoldToAppendToastIfNeeded(context: Context) {
if (shouldShowHoldToAppendTip(context)) {
Toast.makeText(context, R.string.hold_to_append, Toast.LENGTH_SHORT).show()
}
}
/**
* Check if the "hold to append" toast should be shown.
*
*
*
* The tip is shown if the corresponding preference is enabled.
* This is the default behaviour.
*
*
* @param context The context to get the preference.
* @return `true` if the tip should be shown, `false` otherwise.
*/
@JvmStatic
fun shouldShowHoldToAppendTip(context: Context): Boolean {
return PreferenceManager.getDefaultSharedPreferences(context)
.getBoolean(context.getString(R.string.show_hold_to_append_key), true)
}
}

View File

@@ -1,213 +0,0 @@
package org.schabi.newpipe.util;
import static org.schabi.newpipe.extractor.ServiceList.SoundCloud;
import android.content.Context;
import android.content.SharedPreferences;
import androidx.annotation.DrawableRes;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.StringRes;
import androidx.preference.PreferenceManager;
import com.grack.nanojson.JsonObject;
import com.grack.nanojson.JsonParser;
import com.grack.nanojson.JsonParserException;
import org.schabi.newpipe.R;
import org.schabi.newpipe.extractor.NewPipe;
import org.schabi.newpipe.extractor.ServiceList;
import org.schabi.newpipe.extractor.StreamingService;
import org.schabi.newpipe.extractor.exceptions.ExtractionException;
import org.schabi.newpipe.extractor.services.peertube.PeertubeInstance;
import java.util.Optional;
import java.util.concurrent.TimeUnit;
public final class ServiceHelper {
private static final StreamingService DEFAULT_FALLBACK_SERVICE = ServiceList.YouTube;
private ServiceHelper() { }
@DrawableRes
public static int getIcon(final int serviceId) {
switch (serviceId) {
case 0:
return R.drawable.ic_smart_display;
case 1:
return R.drawable.ic_cloud;
case 2:
return R.drawable.ic_placeholder_media_ccc;
case 3:
return R.drawable.ic_placeholder_peertube;
case 4:
return R.drawable.ic_placeholder_bandcamp;
default:
return R.drawable.ic_circle;
}
}
public static String getTranslatedFilterString(final String filter, final Context c) {
switch (filter) {
case "all":
return c.getString(R.string.all);
case "videos":
case "sepia_videos":
case "music_videos":
return c.getString(R.string.videos_string);
case "channels":
return c.getString(R.string.channels);
case "playlists":
case "music_playlists":
return c.getString(R.string.playlists);
case "tracks":
return c.getString(R.string.tracks);
case "users":
return c.getString(R.string.users);
case "conferences":
return c.getString(R.string.conferences);
case "events":
return c.getString(R.string.events);
case "music_songs":
return c.getString(R.string.songs);
case "music_albums":
return c.getString(R.string.albums);
case "music_artists":
return c.getString(R.string.artists);
default:
return filter;
}
}
/**
* Get a resource string with instructions for importing subscriptions for each service.
*
* @param serviceId service to get the instructions for
* @return the string resource containing the instructions or -1 if the service don't support it
*/
@StringRes
public static int getImportInstructions(final int serviceId) {
switch (serviceId) {
case 0:
return R.string.import_youtube_instructions;
case 1:
return R.string.import_soundcloud_instructions;
default:
return -1;
}
}
/**
* For services that support importing from a channel url, return a hint that will
* be used in the EditText that the user will type in his channel url.
*
* @param serviceId service to get the hint for
* @return the hint's string resource or -1 if the service don't support it
*/
@StringRes
public static int getImportInstructionsHint(final int serviceId) {
switch (serviceId) {
case 1:
return R.string.import_soundcloud_instructions_hint;
default:
return -1;
}
}
public static int getSelectedServiceId(final Context context) {
return Optional.ofNullable(getSelectedService(context))
.orElse(DEFAULT_FALLBACK_SERVICE)
.getServiceId();
}
@Nullable
public static StreamingService getSelectedService(final Context context) {
final String serviceName = PreferenceManager.getDefaultSharedPreferences(context)
.getString(context.getString(R.string.current_service_key),
context.getString(R.string.default_service_value));
try {
return NewPipe.getService(serviceName);
} catch (final ExtractionException e) {
return null;
}
}
@NonNull
public static String getNameOfServiceById(final int serviceId) {
return ServiceList.all().stream()
.filter(s -> s.getServiceId() == serviceId)
.findFirst()
.map(StreamingService::getServiceInfo)
.map(StreamingService.ServiceInfo::getName)
.orElse("<unknown>");
}
/**
* @param serviceId the id of the service
* @return the service corresponding to the provided id
* @throws java.util.NoSuchElementException if there is no service with the provided id
*/
@NonNull
public static StreamingService getServiceById(final int serviceId) {
return ServiceList.all().stream()
.filter(s -> s.getServiceId() == serviceId)
.findFirst()
.orElseThrow();
}
public static void setSelectedServiceId(final Context context, final int serviceId) {
String serviceName;
try {
serviceName = NewPipe.getService(serviceId).getServiceInfo().getName();
} catch (final ExtractionException e) {
serviceName = DEFAULT_FALLBACK_SERVICE.getServiceInfo().getName();
}
setSelectedServicePreferences(context, serviceName);
}
private static void setSelectedServicePreferences(final Context context,
final String serviceName) {
PreferenceManager.getDefaultSharedPreferences(context).edit().
putString(context.getString(R.string.current_service_key), serviceName).apply();
}
public static long getCacheExpirationMillis(final int serviceId) {
if (serviceId == SoundCloud.getServiceId()) {
return TimeUnit.MILLISECONDS.convert(5, TimeUnit.MINUTES);
} else {
return TimeUnit.MILLISECONDS.convert(1, TimeUnit.HOURS);
}
}
public static void initService(final Context context, final int serviceId) {
if (serviceId == ServiceList.PeerTube.getServiceId()) {
final SharedPreferences sharedPreferences = PreferenceManager
.getDefaultSharedPreferences(context);
final String json = sharedPreferences.getString(context.getString(
R.string.peertube_selected_instance_key), null);
if (null == json) {
return;
}
final JsonObject jsonObject;
try {
jsonObject = JsonParser.object().from(json);
} catch (final JsonParserException e) {
return;
}
final String name = jsonObject.getString("name");
final String url = jsonObject.getString("url");
final PeertubeInstance instance = new PeertubeInstance(url, name);
ServiceList.PeerTube.setInstance(instance);
}
}
public static void initServices(final Context context) {
for (final StreamingService s : ServiceList.all()) {
initService(context, s.getServiceId());
}
}
}

View File

@@ -0,0 +1,168 @@
/*
* SPDX-FileCopyrightText: 2018-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.annotation.DrawableRes
import androidx.annotation.StringRes
import androidx.core.content.edit
import androidx.preference.PreferenceManager
import com.grack.nanojson.JsonParser
import java.util.concurrent.TimeUnit
import org.schabi.newpipe.R
import org.schabi.newpipe.extractor.NewPipe
import org.schabi.newpipe.extractor.ServiceList
import org.schabi.newpipe.extractor.StreamingService
import org.schabi.newpipe.extractor.services.peertube.PeertubeInstance
import org.schabi.newpipe.ktx.getStringSafe
object ServiceHelper {
private val DEFAULT_FALLBACK_SERVICE: StreamingService = ServiceList.YouTube
@JvmStatic
@DrawableRes
fun getIcon(serviceId: Int): Int {
return when (serviceId) {
0 -> R.drawable.ic_smart_display
1 -> R.drawable.ic_cloud
2 -> R.drawable.ic_placeholder_media_ccc
3 -> R.drawable.ic_placeholder_peertube
4 -> R.drawable.ic_placeholder_bandcamp
else -> R.drawable.ic_circle
}
}
@JvmStatic
fun getTranslatedFilterString(filter: String, context: Context): String {
return when (filter) {
"all" -> context.getString(R.string.all)
"videos", "sepia_videos", "music_videos" -> context.getString(R.string.videos_string)
"channels" -> context.getString(R.string.channels)
"playlists", "music_playlists" -> context.getString(R.string.playlists)
"tracks" -> context.getString(R.string.tracks)
"users" -> context.getString(R.string.users)
"conferences" -> context.getString(R.string.conferences)
"events" -> context.getString(R.string.events)
"music_songs" -> context.getString(R.string.songs)
"music_albums" -> context.getString(R.string.albums)
"music_artists" -> context.getString(R.string.artists)
else -> filter
}
}
/**
* Get a resource string with instructions for importing subscriptions for each service.
*
* @param serviceId service to get the instructions for
* @return the string resource containing the instructions or -1 if the service don't support it
*/
@JvmStatic
@StringRes
fun getImportInstructions(serviceId: Int): Int {
return when (serviceId) {
0 -> R.string.import_youtube_instructions
1 -> R.string.import_soundcloud_instructions
else -> -1
}
}
/**
* For services that support importing from a channel url, return a hint that will
* be used in the EditText that the user will type in his channel url.
*
* @param serviceId service to get the hint for
* @return the hint's string resource or -1 if the service don't support it
*/
@JvmStatic
@StringRes
fun getImportInstructionsHint(serviceId: Int): Int {
return when (serviceId) {
1 -> R.string.import_soundcloud_instructions_hint
else -> -1
}
}
@JvmStatic
fun getSelectedServiceId(context: Context): Int {
return (getSelectedService(context) ?: DEFAULT_FALLBACK_SERVICE).serviceId
}
@JvmStatic
fun getSelectedService(context: Context): StreamingService? {
val serviceName: String = PreferenceManager.getDefaultSharedPreferences(context)
.getStringSafe(
context.getString(R.string.current_service_key),
context.getString(R.string.default_service_value)
)
return runCatching { NewPipe.getService(serviceName) }.getOrNull()
}
@JvmStatic
fun getNameOfServiceById(serviceId: Int): String {
return ServiceList.all().stream()
.filter { it.serviceId == serviceId }
.findFirst()
.map(StreamingService::getServiceInfo)
.map(StreamingService.ServiceInfo::getName)
.orElse("<unknown>")
}
/**
* @param serviceId the id of the service
* @return the service corresponding to the provided id
* @throws java.util.NoSuchElementException if there is no service with the provided id
*/
@JvmStatic
fun getServiceById(serviceId: Int): StreamingService {
return ServiceList.all().firstNotNullOf { it.takeIf { it.serviceId == serviceId } }
}
@JvmStatic
fun setSelectedServiceId(context: Context, serviceId: Int) {
val serviceName = runCatching { NewPipe.getService(serviceId).serviceInfo.name }
.getOrDefault(DEFAULT_FALLBACK_SERVICE.serviceInfo.name)
setSelectedServicePreferences(context, serviceName)
}
private fun setSelectedServicePreferences(context: Context, serviceName: String?) {
val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context)
sharedPreferences.edit { putString(context.getString(R.string.current_service_key), serviceName) }
}
@JvmStatic
fun getCacheExpirationMillis(serviceId: Int): Long {
return if (serviceId == ServiceList.SoundCloud.serviceId) {
TimeUnit.MILLISECONDS.convert(5, TimeUnit.MINUTES)
} else {
TimeUnit.MILLISECONDS.convert(1, TimeUnit.HOURS)
}
}
fun initService(context: Context, serviceId: Int) {
if (serviceId == ServiceList.PeerTube.serviceId) {
val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context)
val json = sharedPreferences.getString(
context.getString(R.string.peertube_selected_instance_key),
null
) ?: return
val jsonObject = runCatching { JsonParser.`object`().from(json) }
.getOrElse { return@initService }
ServiceList.PeerTube.instance = PeertubeInstance(
jsonObject.getString("url"),
jsonObject.getString("name")
)
}
}
@JvmStatic
fun initServices(context: Context) {
ServiceList.all().forEach { initService(context, it.serviceId) }
}
}

View File

@@ -1,50 +0,0 @@
package org.schabi.newpipe.util;
import org.schabi.newpipe.extractor.stream.StreamType;
/**
* Utility class for {@link StreamType}.
*/
public final class StreamTypeUtil {
private StreamTypeUtil() {
// No impl pls
}
/**
* Check if the {@link StreamType} of a stream is a livestream.
*
* @param streamType the stream type of the stream
* @return whether the stream type is {@link StreamType#AUDIO_STREAM},
* {@link StreamType#AUDIO_LIVE_STREAM} or {@link StreamType#POST_LIVE_AUDIO_STREAM}
*/
public static boolean isAudio(final StreamType streamType) {
return streamType == StreamType.AUDIO_STREAM
|| streamType == StreamType.AUDIO_LIVE_STREAM
|| streamType == StreamType.POST_LIVE_AUDIO_STREAM;
}
/**
* Check if the {@link StreamType} of a stream is a livestream.
*
* @param streamType the stream type of the stream
* @return whether the stream type is {@link StreamType#VIDEO_STREAM},
* {@link StreamType#LIVE_STREAM} or {@link StreamType#POST_LIVE_STREAM}
*/
public static boolean isVideo(final StreamType streamType) {
return streamType == StreamType.VIDEO_STREAM
|| streamType == StreamType.LIVE_STREAM
|| streamType == StreamType.POST_LIVE_STREAM;
}
/**
* Check if the {@link StreamType} of a stream is a livestream.
*
* @param streamType the stream type of the stream
* @return whether the stream type is {@link StreamType#LIVE_STREAM} or
* {@link StreamType#AUDIO_LIVE_STREAM}
*/
public static boolean isLiveStream(final StreamType streamType) {
return streamType == StreamType.LIVE_STREAM
|| streamType == StreamType.AUDIO_LIVE_STREAM;
}
}

View File

@@ -0,0 +1,54 @@
/*
* SPDX-FileCopyrightText: 2021-2026 NewPipe contributors <https://newpipe.net>
* SPDX-License-Identifier: GPL-3.0-or-later
*/
package org.schabi.newpipe.util
import org.schabi.newpipe.extractor.stream.StreamType
/**
* Utility class for [StreamType].
*/
object StreamTypeUtil {
/**
* Check if the [StreamType] of a stream is a livestream.
*
* @param streamType the stream type of the stream
* @return whether the stream type is [StreamType.AUDIO_STREAM],
* [StreamType.AUDIO_LIVE_STREAM] or [StreamType.POST_LIVE_AUDIO_STREAM]
*/
@JvmStatic
fun isAudio(streamType: StreamType): Boolean {
return streamType == StreamType.AUDIO_STREAM ||
streamType == StreamType.AUDIO_LIVE_STREAM ||
streamType == StreamType.POST_LIVE_AUDIO_STREAM
}
/**
* Check if the [StreamType] of a stream is a livestream.
*
* @param streamType the stream type of the stream
* @return whether the stream type is [StreamType.VIDEO_STREAM],
* [StreamType.LIVE_STREAM] or [StreamType.POST_LIVE_STREAM]
*/
@JvmStatic
fun isVideo(streamType: StreamType): Boolean {
return streamType == StreamType.VIDEO_STREAM ||
streamType == StreamType.LIVE_STREAM ||
streamType == StreamType.POST_LIVE_STREAM
}
/**
* Check if the [StreamType] of a stream is a livestream.
*
* @param streamType the stream type of the stream
* @return whether the stream type is [StreamType.LIVE_STREAM] or
* [StreamType.AUDIO_LIVE_STREAM]
*/
@JvmStatic
fun isLiveStream(streamType: StreamType): Boolean {
return streamType == StreamType.LIVE_STREAM ||
streamType == StreamType.AUDIO_LIVE_STREAM
}
}

View File

@@ -1,6 +1,4 @@
android.enableJetifier=false
android.nonFinalResIds=false android.nonFinalResIds=false
android.nonTransitiveRClass=true
android.useAndroidX=true android.useAndroidX=true
org.gradle.jvmargs=-Xmx2048M --add-opens jdk.compiler/com.sun.tools.javac.model=ALL-UNNAMED org.gradle.jvmargs=-Xmx2048M --add-opens jdk.compiler/com.sun.tools.javac.model=ALL-UNNAMED
systemProp.file.encoding=utf-8 systemProp.file.encoding=utf-8

View File

@@ -23,8 +23,8 @@ groupie = "2.10.1"
jsoup = "1.22.1" jsoup = "1.22.1"
junit = "4.13.2" junit = "4.13.2"
junit-ext = "1.3.0" junit-ext = "1.3.0"
kotlin = "2.2.21" kotlin = "2.3.0"
ksp = "2.3.4" ksp = "2.3.5"
ktlint = "1.8.0" ktlint = "1.8.0"
leakcanary = "2.14" leakcanary = "2.14"
lifecycle = "2.9.4" # Newer versions require minSdk >= 23 lifecycle = "2.9.4" # Newer versions require minSdk >= 23