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

Compare commits

..

1 Commits

Author SHA1 Message Date
TobiGr
27b2d5de70 [AndroidTV] Fix selecting PeerTube instance in navigation drawer
Fixes #10020
2023-07-15 04:38:36 +02:00
418 changed files with 3779 additions and 13124 deletions

View File

@@ -14,7 +14,7 @@ body:
attributes:
label: "Checklist"
options:
- label: "I am able to reproduce the bug with the latest version given here: [CLICK THIS LINK](https://github.com/TeamNewPipe/NewPipe/releases/latest)."
- label: "I am able to reproduce the bug with the [latest version](https://github.com/TeamNewPipe/NewPipe/releases/latest)."
required: true
- label: "I made sure that there are *no existing issues* - [open](https://github.com/TeamNewPipe/NewPipe/issues) or [closed](https://github.com/TeamNewPipe/NewPipe/issues?q=is%3Aissue+is%3Aclosed) - which I could contribute my information to."
required: true

View File

@@ -1,17 +0,0 @@
# Add 'size/small' label to any changes with less than 50 lines
size/small:
max: 49
# Add 'size/medium' label to any changes between 50 and 249 lines
size/medium:
min: 50
max: 249
# Add 'size/large' label to any changes between 250 and 749 lines
size/large:
min: 250
max: 749
# Add 'size/giant' label to any changes for more than 749 lines
size/giant:
min: 750

View File

@@ -17,8 +17,6 @@ module.exports = async ({github, context}) => {
initialBody = context.payload.comment.body;
} else if (context.eventName == 'issues') {
initialBody = context.payload.issue.body;
} else if (context.eventName == 'pull_request') {
initialBody = context.payload.pull_request.body;
} else {
console.log('Aborting: No body found');
return;
@@ -76,17 +74,9 @@ module.exports = async ({github, context}) => {
repo: context.repo.repo,
body: newBody
});
} else if (context.eventName == 'pull_request') {
console.log('Updating pull request', context.payload.pull_request.number);
await github.rest.pulls.update({
pull_number: context.payload.pull_request.number,
owner: context.repo.owner,
repo: context.repo.repo,
body: newBody
});
}
// Async replace function from https://stackoverflow.com/a/48032528
// Asnyc replace function from https://stackoverflow.com/a/48032528
async function replaceAsync(str, regex, asyncFn) {
const promises = [];
str.replace(regex, (match, ...args) => {
@@ -138,7 +128,7 @@ module.exports = async ({github, context}) => {
if (shouldModify) {
wasMatchModified = true;
console.log(`Modifying match '${match}'`);
return `<img alt="${g1}" src="${g2}" width=${Math.min(600, Math.floor(IMG_MAX_HEIGHT_PX * probeAspectRatio))} />`;
return `<img alt="${g1}" src="${g2}" width=${Math.min(600, (IMG_MAX_HEIGHT_PX * probeAspectRatio).toFixed(0))} />`;
}
console.log(`Match '${match}' is ok/will not be modified`);

View File

@@ -5,8 +5,6 @@ on:
types: [created, edited]
issues:
types: [opened, edited]
pull_request:
types: [opened, edited]
permissions:
issues: write

View File

@@ -1,18 +0,0 @@
name: "PR size labeler"
on: [pull_request]
permissions:
contents: read
pull-requests: write
jobs:
changed-lines-count-labeler:
runs-on: ubuntu-latest
name: Automatically labelling pull requests based on the changed lines count
permissions:
pull-requests: write
steps:
- name: Set a label
uses: TeamNewPipe/changed-lines-count-labeler@main
with:
repo-token: ${{ secrets.GITHUB_TOKEN }}
configuration-path: .github/changed-lines-count-labeler.yml

View File

@@ -13,14 +13,14 @@
<a href="https://github.com/TeamNewPipe/NewPipe/actions" alt="Build Status"><img src="https://github.com/TeamNewPipe/NewPipe/workflows/CI/badge.svg?branch=dev&event=push"></a>
<a href="https://hosted.weblate.org/engage/newpipe/" alt="Translation Status"><img src="https://hosted.weblate.org/widgets/newpipe/-/svg-badge.svg"></a>
<a href="https://web.libera.chat/#newpipe" alt="IRC channel: #newpipe"><img src="https://img.shields.io/badge/IRC%20chat-%23newpipe-brightgreen.svg"></a>
<a href="https://matrix.to/#/#newpipe:libera.chat" alt="Matrix channel: #newpipe"><img src="https://img.shields.io/badge/Matrix%20chat-%23newpipe-blue"></a>
<a href="https://www.bountysource.com/teams/newpipe" alt="Bountysource bounties"><img src="https://img.shields.io/bountysource/team/newpipe/activity.svg?colorB=cd201f"></a>
</p>
<hr>
<p align="center"><a href="#screenshots">Screenshots</a> &bull; <a href="#supported-services">Supported Services</a> &bull; <a href="#description">Description</a> &bull; <a href="#features">Features</a> &bull; <a href="#installation-and-updates">Installation and updates</a> &bull; <a href="#contribution">Contribution</a> &bull; <a href="#donate">Donate</a> &bull; <a href="#license">License</a></p>
<p align="center"><a href="https://newpipe.net">Website</a> &bull; <a href="https://newpipe.net/blog/">Blog</a> &bull; <a href="https://newpipe.net/FAQ/">FAQ</a> &bull; <a href="https://newpipe.net/press/">Press</a></p>
<hr>
*Read this document in other languages: [Deutsch](doc/README.de.md), [English](README.md), [Español](doc/README.es.md), [Français](doc/README.fr.md), [हिन्दी](doc/README.hi.md), [Italiano](doc/README.it.md), [한국어](doc/README.ko.md), [Português Brasil](doc/README.pt_BR.md), [Polski](doc/README.pl.md), [ਪੰਜਾਬੀ ](doc/README.pa.md), [日本語](doc/README.ja.md), [Română](doc/README.ro.md), [Soomaali](doc/README.so.md), [Türkçe](doc/README.tr.md), [正體中文](doc/README.zh_TW.md), [অসমীয়া](doc/README.asm.md), [Српски](doc/README.sr.md)*
*Read this in other languages: [English](README.md), [Español](doc/README.es.md), [हिन्दी](doc/README.hi.md), [한국어](doc/README.ko.md), [Soomaali](doc/README.so.md), [Português Brasil](doc/README.pt_BR.md), [Polski](doc/README.pl.md), [日本語](doc/README.ja.md), [Română](doc/README.ro.md), [Türkçe](doc/README.tr.md), [正體中文](doc/README.zh_TW.md).*
<b>WARNING: THIS APP IS IN BETA, SO YOU MAY ENCOUNTER BUGS. IF YOU DO, OPEN AN ISSUE IN OUR GITHUB REPOSITORY BY FILLING OUT THE ISSUE TEMPLATE.</b>
@@ -126,6 +126,16 @@ If you like NewPipe, you're welcome to send a donation. We prefer Liberapay, as
<td><a href="https://liberapay.com/TeamNewPipe/"><img src="assets/liberapay_qr_code.png" alt="Visit NewPipe at liberapay.com" width="100px"></a></td>
<td><a href="https://liberapay.com/TeamNewPipe/donate"><img src="assets/liberapay_donate_button.svg" alt="Donate via Liberapay" height="35px"></a></td>
</tr>
<tr>
<td><img src="https://bitcoin.org/img/icons/logotop.svg" alt="Bitcoin"></td>
<td><img src="assets/bitcoin_qr_code.png" alt="Bitcoin QR code" width="100px"></td>
<td><samp>16A9J59ahMRqkLSZjhYj33n9j3fMztFxnh</samp></td>
</tr>
<tr>
<td><a href="https://www.bountysource.com/teams/newpipe"><img src="https://upload.wikimedia.org/wikipedia/commons/thumb/2/22/Bountysource.png/320px-Bountysource.png" alt="Bountysource" width="190px"></a></td>
<td><a href="https://www.bountysource.com/teams/newpipe"><img src="assets/bountysource_qr_code.png" alt="Visit NewPipe at bountysource.com" width="100px"></a></td>
<td><a href="https://www.bountysource.com/teams/newpipe/issues"><img src="https://img.shields.io/bountysource/team/newpipe/activity.svg?colorB=cd201f" height="30px" alt="Check out how many bounties you can earn."></a></td>
</tr>
</table>
## Privacy Policy

View File

@@ -20,8 +20,8 @@ android {
resValue "string", "app_name", "NewPipe"
minSdk 21
targetSdk 33
versionCode 996
versionName "0.26.1"
versionCode 993
versionName "0.25.1"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
@@ -50,6 +50,9 @@ android {
}
}
// Keep the release build type at the end of the list to override 'archivesBaseName' of
// debug build. This seems to be a Gradle bug, therefore
// TODO: update Gradle version
release {
if (System.properties.containsKey('packageSuffix')) {
applicationIdSuffix System.getProperty('packageSuffix')
@@ -107,16 +110,16 @@ ext {
checkstyleVersion = '10.12.1'
androidxLifecycleVersion = '2.5.1'
androidxRoomVersion = '2.5.2'
androidxRoomVersion = '2.4.3'
androidxWorkVersion = '2.7.1'
icepickVersion = '3.2.0'
exoPlayerVersion = '2.18.7'
googleAutoServiceVersion = '1.1.1'
googleAutoServiceVersion = '1.0.1'
groupieVersion = '2.10.1'
markwonVersion = '4.6.2'
leakCanaryVersion = '2.12'
leakCanaryVersion = '2.9.1'
stethoVersion = '1.6.0'
mockitoVersion = '4.0.0'
}
@@ -189,7 +192,7 @@ sonar {
dependencies {
/** Desugaring **/
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs_nio:2.0.3'
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:2.0.3'
/** NewPipe libraries **/
// You can use a local version by uncommenting a few lines in settings.gradle
@@ -197,7 +200,7 @@ dependencies {
// name and the commit hash with the commit hash of the (pushed) commit you want to test
// This works thanks to JitPack: https://jitpack.io/
implementation 'com.github.TeamNewPipe:nanojson:1d9e1aea9049fc9f85e68b43ba39fe7be1c1f751'
implementation 'com.github.TeamNewPipe:NewPipeExtractor:v0.23.1'
implementation 'com.github.TeamNewPipe:NewPipeExtractor:8495ad619e'
implementation 'com.github.TeamNewPipe:NoNonsense-FilePicker:5.0.0'
/** Checkstyle **/
@@ -205,7 +208,7 @@ dependencies {
ktlint 'com.pinterest:ktlint:0.45.2'
/** Kotlin **/
implementation "org.jetbrains.kotlin:kotlin-stdlib:${kotlin_version}"
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:${kotlin_version}"
/** AndroidX **/
implementation 'androidx.appcompat:appcompat:1.5.1'
@@ -229,7 +232,7 @@ dependencies {
implementation 'androidx.viewpager2:viewpager2:1.1.0-beta01'
implementation "androidx.work:work-runtime-ktx:${androidxWorkVersion}"
implementation "androidx.work:work-rxjava3:${androidxWorkVersion}"
implementation 'com.google.android.material:material:1.9.0'
implementation 'com.google.android.material:material:1.6.1'
/** Third-party libraries **/
// Instance state boilerplate elimination
@@ -241,9 +244,6 @@ dependencies {
// HTTP client
implementation "com.squareup.okhttp3:okhttp:4.11.0"
// okhttp3:4.11.0 introduces a vulnerability from com.squareup.okio:okio@3.3.0,
// remove com.squareup.okio:okio when updating okhttp
implementation "com.squareup.okio:okio:3.4.0"
// Media player
implementation "com.google.android.exoplayer:exoplayer-core:${exoPlayerVersion}"
@@ -288,9 +288,9 @@ dependencies {
/** Debugging **/
// Memory leak detection
debugImplementation "com.squareup.leakcanary:leakcanary-object-watcher-android:${leakCanaryVersion}"
debugImplementation "com.squareup.leakcanary:plumber-android:${leakCanaryVersion}"
debugImplementation "com.squareup.leakcanary:leakcanary-android-core:${leakCanaryVersion}"
implementation "com.squareup.leakcanary:leakcanary-object-watcher-android:${leakCanaryVersion}"
implementation "com.squareup.leakcanary:plumber-android:${leakCanaryVersion}"
debugImplementation "com.squareup.leakcanary:leakcanary-android:${leakCanaryVersion}"
// Debug bridge for Android
debugImplementation "com.facebook.stetho:stetho:${stethoVersion}"
debugImplementation "com.facebook.stetho:stetho-okhttp3:${stethoVersion}"

View File

@@ -4,6 +4,7 @@ import android.content.ContentValues
import android.database.sqlite.SQLiteDatabase
import androidx.room.Room
import androidx.room.testing.MigrationTestHelper
import androidx.sqlite.db.framework.FrameworkSQLiteOpenHelperFactory
import androidx.test.core.app.ApplicationProvider
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.platform.app.InstrumentationRegistry
@@ -32,7 +33,8 @@ class DatabaseMigrationTest {
@get:Rule
val testHelper = MigrationTestHelper(
InstrumentationRegistry.getInstrumentation(),
AppDatabase::class.java
AppDatabase::class.java.canonicalName,
FrameworkSQLiteOpenHelperFactory()
)
@Test

View File

@@ -1,124 +0,0 @@
package org.schabi.newpipe.database
import android.content.Context
import androidx.room.Room
import androidx.test.core.app.ApplicationProvider
import io.reactivex.rxjava3.core.Single
import org.junit.After
import org.junit.Assert.assertEquals
import org.junit.Assert.assertNotNull
import org.junit.Before
import org.junit.Test
import org.schabi.newpipe.database.feed.dao.FeedDAO
import org.schabi.newpipe.database.feed.model.FeedEntity
import org.schabi.newpipe.database.feed.model.FeedGroupEntity
import org.schabi.newpipe.database.stream.StreamWithState
import org.schabi.newpipe.database.stream.dao.StreamDAO
import org.schabi.newpipe.database.stream.model.StreamEntity
import org.schabi.newpipe.database.subscription.SubscriptionDAO
import org.schabi.newpipe.database.subscription.SubscriptionEntity
import org.schabi.newpipe.extractor.ServiceList
import org.schabi.newpipe.extractor.channel.ChannelInfo
import org.schabi.newpipe.extractor.stream.StreamType
import java.io.IOException
import java.time.OffsetDateTime
import kotlin.streams.toList
class FeedDAOTest {
private lateinit var db: AppDatabase
private lateinit var feedDAO: FeedDAO
private lateinit var streamDAO: StreamDAO
private lateinit var subscriptionDAO: SubscriptionDAO
private val serviceId = ServiceList.YouTube.serviceId
private val stream1 = StreamEntity(1, serviceId, "https://youtube.com/watch?v=1", "stream 1", StreamType.VIDEO_STREAM, 1000, "channel-1", "https://youtube.com/channel/1", "https://i.ytimg.com/vi/1/hqdefault.jpg", 100, "2023-01-01", OffsetDateTime.parse("2023-01-01T00:00:00Z"))
private val stream2 = StreamEntity(2, serviceId, "https://youtube.com/watch?v=2", "stream 2", StreamType.VIDEO_STREAM, 1000, "channel-1", "https://youtube.com/channel/1", "https://i.ytimg.com/vi/1/hqdefault.jpg", 100, "2023-01-02", OffsetDateTime.parse("2023-01-02T00:00:00Z"))
private val stream3 = StreamEntity(3, serviceId, "https://youtube.com/watch?v=3", "stream 3", StreamType.LIVE_STREAM, 1000, "channel-1", "https://youtube.com/channel/1", "https://i.ytimg.com/vi/1/hqdefault.jpg", 100, "2023-01-03", OffsetDateTime.parse("2023-01-03T00:00:00Z"))
private val stream4 = StreamEntity(4, serviceId, "https://youtube.com/watch?v=4", "stream 4", StreamType.VIDEO_STREAM, 1000, "channel-2", "https://youtube.com/channel/2", "https://i.ytimg.com/vi/1/hqdefault.jpg", 100, "2023-08-10", OffsetDateTime.parse("2023-08-10T00:00:00Z"))
private val stream5 = StreamEntity(5, serviceId, "https://youtube.com/watch?v=5", "stream 5", StreamType.VIDEO_STREAM, 1000, "channel-2", "https://youtube.com/channel/2", "https://i.ytimg.com/vi/1/hqdefault.jpg", 100, "2023-08-20", OffsetDateTime.parse("2023-08-20T00:00:00Z"))
private val stream6 = StreamEntity(6, serviceId, "https://youtube.com/watch?v=6", "stream 6", StreamType.VIDEO_STREAM, 1000, "channel-3", "https://youtube.com/channel/3", "https://i.ytimg.com/vi/1/hqdefault.jpg", 100, "2023-09-01", OffsetDateTime.parse("2023-09-01T00:00:00Z"))
private val stream7 = StreamEntity(7, serviceId, "https://youtube.com/watch?v=7", "stream 7", StreamType.VIDEO_STREAM, 1000, "channel-4", "https://youtube.com/channel/4", "https://i.ytimg.com/vi/1/hqdefault.jpg", 100, "2023-08-10", OffsetDateTime.parse("2023-08-10T00:00:00Z"))
private val allStreams = listOf(
stream1, stream2, stream3, stream4, stream5, stream6, stream7
)
@Before
fun createDb() {
val context = ApplicationProvider.getApplicationContext<Context>()
db = Room.inMemoryDatabaseBuilder(
context, AppDatabase::class.java
).build()
feedDAO = db.feedDAO()
streamDAO = db.streamDAO()
subscriptionDAO = db.subscriptionDAO()
}
@After
@Throws(IOException::class)
fun closeDb() {
db.close()
}
@Test
fun testUnlinkStreamsOlderThan_KeepOne() {
setupUnlinkDelete("2023-08-15T00:00:00Z")
val streams = feedDAO.getStreams(
FeedGroupEntity.GROUP_ALL_ID, includePlayed = true, includePartiallyPlayed = true, null
)
.blockingGet()
val allowedStreams = listOf(stream3, stream5, stream6, stream7)
assertEqual(streams, allowedStreams)
}
@Test
fun testUnlinkStreamsOlderThan_KeepMultiple() {
setupUnlinkDelete("2023-08-01T00:00:00Z")
val streams = feedDAO.getStreams(
FeedGroupEntity.GROUP_ALL_ID, includePlayed = true, includePartiallyPlayed = true, null
)
.blockingGet()
val allowedStreams = listOf(stream3, stream4, stream5, stream6, stream7)
assertEqual(streams, allowedStreams)
}
private fun assertEqual(streams: List<StreamWithState>?, allowedStreams: List<StreamEntity>) {
assertNotNull(streams)
assertEquals(allowedStreams, streams!!.stream().map { it.stream }.toList().sortedBy { it.uid })
}
private fun setupUnlinkDelete(time: String) {
clearAndFillTables()
Single.fromCallable {
feedDAO.unlinkStreamsOlderThan(OffsetDateTime.parse(time))
}.blockingSubscribe()
Single.fromCallable {
streamDAO.deleteOrphans()
}.blockingSubscribe()
}
private fun clearAndFillTables() {
db.clearAllTables()
streamDAO.insertAll(allStreams)
subscriptionDAO.insertAll(
listOf(
SubscriptionEntity.from(ChannelInfo(serviceId, "1", "https://youtube.com/channel/1", "https://youtube.com/channel/1", "channel-1")),
SubscriptionEntity.from(ChannelInfo(serviceId, "2", "https://youtube.com/channel/2", "https://youtube.com/channel/2", "channel-2")),
SubscriptionEntity.from(ChannelInfo(serviceId, "3", "https://youtube.com/channel/3", "https://youtube.com/channel/3", "channel-3")),
SubscriptionEntity.from(ChannelInfo(serviceId, "4", "https://youtube.com/channel/4", "https://youtube.com/channel/4", "channel-4")),
)
)
feedDAO.insertAll(
listOf(
FeedEntity(1, 1),
FeedEntity(2, 1),
FeedEntity(3, 1),
FeedEntity(4, 2),
FeedEntity(5, 2),
FeedEntity(6, 3),
FeedEntity(7, 4),
)
)
}
}

View File

@@ -1,82 +0,0 @@
package org.schabi.newpipe.local.subscription;
import static org.junit.Assert.assertEquals;
import androidx.test.core.app.ApplicationProvider;
import org.junit.After;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.schabi.newpipe.database.AppDatabase;
import org.schabi.newpipe.database.feed.model.FeedGroupEntity;
import org.schabi.newpipe.database.subscription.SubscriptionEntity;
import org.schabi.newpipe.extractor.channel.ChannelInfo;
import org.schabi.newpipe.extractor.exceptions.ExtractionException;
import org.schabi.newpipe.testUtil.TestDatabase;
import org.schabi.newpipe.testUtil.TrampolineSchedulerRule;
import java.io.IOException;
import java.util.List;
public class SubscriptionManagerTest {
private AppDatabase database;
private SubscriptionManager manager;
@Rule
public TrampolineSchedulerRule trampolineScheduler = new TrampolineSchedulerRule();
private SubscriptionEntity getAssertOneSubscriptionEntity() {
final List<SubscriptionEntity> entities = manager
.getSubscriptions(FeedGroupEntity.GROUP_ALL_ID, "", false)
.blockingFirst();
assertEquals(1, entities.size());
return entities.get(0);
}
@Before
public void setup() {
database = TestDatabase.Companion.createReplacingNewPipeDatabase();
manager = new SubscriptionManager(ApplicationProvider.getApplicationContext());
}
@After
public void cleanUp() {
database.close();
}
@Test
public void testInsert() throws ExtractionException, IOException {
final ChannelInfo info = ChannelInfo.getInfo("https://www.youtube.com/c/3blue1brown");
final SubscriptionEntity subscription = SubscriptionEntity.from(info);
manager.insertSubscription(subscription);
final SubscriptionEntity readSubscription = getAssertOneSubscriptionEntity();
// the uid has changed, since the uid is chosen upon inserting, but the rest should match
assertEquals(subscription.getServiceId(), readSubscription.getServiceId());
assertEquals(subscription.getUrl(), readSubscription.getUrl());
assertEquals(subscription.getName(), readSubscription.getName());
assertEquals(subscription.getAvatarUrl(), readSubscription.getAvatarUrl());
assertEquals(subscription.getSubscriberCount(), readSubscription.getSubscriberCount());
assertEquals(subscription.getDescription(), readSubscription.getDescription());
}
@Test
public void testUpdateNotificationMode() throws ExtractionException, IOException {
final ChannelInfo info = ChannelInfo.getInfo("https://www.youtube.com/c/veritasium");
final SubscriptionEntity subscription = SubscriptionEntity.from(info);
subscription.setNotificationMode(0);
manager.insertSubscription(subscription);
manager.updateNotificationMode(subscription.getServiceId(), subscription.getUrl(), 1)
.blockingAwait();
final SubscriptionEntity anotherSubscription = getAssertOneSubscriptionEntity();
assertEquals(0, subscription.getNotificationMode());
assertEquals(subscription.getUrl(), anotherSubscription.getUrl());
assertEquals(1, anotherSubscription.getNotificationMode());
}
}

View File

@@ -12,21 +12,15 @@ import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.MediumTest
import androidx.test.internal.runner.junit4.statement.UiThreadStatement
import org.junit.Assert
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
import org.junit.Assert.assertNull
import org.junit.Assert.assertTrue
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.schabi.newpipe.R
import org.schabi.newpipe.extractor.MediaFormat
import org.schabi.newpipe.extractor.downloader.Response
import org.schabi.newpipe.extractor.stream.AudioStream
import org.schabi.newpipe.extractor.stream.Stream
import org.schabi.newpipe.extractor.stream.SubtitlesStream
import org.schabi.newpipe.extractor.stream.VideoStream
import org.schabi.newpipe.util.StreamItemAdapter.StreamInfoWrapper
@MediumTest
@RunWith(AndroidJUnit4::class)
@@ -90,7 +84,7 @@ class StreamItemAdapterTest {
@Test
fun subtitleStreams_noIcon() {
val adapter = StreamItemAdapter<SubtitlesStream, Stream>(
StreamItemAdapter.StreamInfoWrapper(
StreamItemAdapter.StreamSizeWrapper(
(0 until 5).map {
SubtitlesStream.Builder()
.setContent("https://example.com", true)
@@ -111,7 +105,7 @@ class StreamItemAdapterTest {
@Test
fun audioStreams_noIcon() {
val adapter = StreamItemAdapter<AudioStream, Stream>(
StreamItemAdapter.StreamInfoWrapper(
StreamItemAdapter.StreamSizeWrapper(
(0 until 5).map {
AudioStream.Builder()
.setId(Stream.ID_UNKNOWN)
@@ -129,109 +123,12 @@ class StreamItemAdapterTest {
}
}
@Test
fun retrieveMediaFormatFromFileTypeHeaders() {
val streams = getIncompleteAudioStreams(5)
val wrapper = StreamInfoWrapper(streams, context)
val retrieveMediaFormat = { stream: AudioStream, response: Response ->
StreamInfoWrapper.retrieveMediaFormatFromFileTypeHeaders(stream, wrapper, response)
}
val helper = AssertionHelper(streams, wrapper, retrieveMediaFormat)
helper.assertInvalidResponse(getResponse(mapOf(Pair("content-length", "mp3"))), 0)
helper.assertInvalidResponse(getResponse(mapOf(Pair("file-type", "mp0"))), 1)
helper.assertValidResponse(getResponse(mapOf(Pair("x-amz-meta-file-type", "aiff"))), 2, MediaFormat.AIFF)
helper.assertValidResponse(getResponse(mapOf(Pair("file-type", "mp3"))), 3, MediaFormat.MP3)
}
@Test
fun retrieveMediaFormatFromContentDispositionHeader() {
val streams = getIncompleteAudioStreams(11)
val wrapper = StreamInfoWrapper(streams, context)
val retrieveMediaFormat = { stream: AudioStream, response: Response ->
StreamInfoWrapper.retrieveMediaFormatFromContentDispositionHeader(stream, wrapper, response)
}
val helper = AssertionHelper(streams, wrapper, retrieveMediaFormat)
helper.assertInvalidResponse(getResponse(mapOf(Pair("content-length", "mp3"))), 0)
helper.assertInvalidResponse(
getResponse(mapOf(Pair("Content-Disposition", "filename=\"train.png\""))), 1
)
helper.assertInvalidResponse(
getResponse(mapOf(Pair("Content-Disposition", "form-data; name=\"data.csv\""))), 2
)
helper.assertInvalidResponse(
getResponse(mapOf(Pair("Content-Disposition", "form-data; filename=\"data.csv\""))), 3
)
helper.assertInvalidResponse(
getResponse(mapOf(Pair("Content-Disposition", "form-data; name=\"fieldName\"; filename*=\"filename.jpg\""))), 4
)
helper.assertValidResponse(
getResponse(mapOf(Pair("Content-Disposition", "filename=\"train.ogg\""))),
5, MediaFormat.OGG
)
helper.assertValidResponse(
getResponse(mapOf(Pair("Content-Disposition", "some-form-data; filename=\"audio.flac\""))),
6, MediaFormat.FLAC
)
helper.assertValidResponse(
getResponse(mapOf(Pair("Content-Disposition", "form-data; name=\"audio.aiff\"; filename=\"audio.aiff\""))),
7, MediaFormat.AIFF
)
helper.assertValidResponse(
getResponse(mapOf(Pair("Content-Disposition", "form-data; name=\"alien?\"; filename*=UTF-8''%CE%B1%CE%BB%CE%B9%CF%B5%CE%BD.m4a"))),
8, MediaFormat.M4A
)
helper.assertValidResponse(
getResponse(mapOf(Pair("Content-Disposition", "form-data; name=\"audio.mp3\"; filename=\"audio.opus\"; filename*=UTF-8''alien.opus"))),
9, MediaFormat.OPUS
)
helper.assertValidResponse(
getResponse(mapOf(Pair("Content-Disposition", "form-data; name=\"audio.mp3\"; filename=\"audio.opus\"; filename*=\"UTF-8''alien.opus\""))),
10, MediaFormat.OPUS
)
}
@Test
fun retrieveMediaFormatFromContentTypeHeader() {
val streams = getIncompleteAudioStreams(12)
val wrapper = StreamInfoWrapper(streams, context)
val retrieveMediaFormat = { stream: AudioStream, response: Response ->
StreamInfoWrapper.retrieveMediaFormatFromContentTypeHeader(stream, wrapper, response)
}
val helper = AssertionHelper(streams, wrapper, retrieveMediaFormat)
helper.assertInvalidResponse(getResponse(mapOf(Pair("content-length", "984501"))), 0)
helper.assertInvalidResponse(getResponse(mapOf(Pair("Content-Type", "audio/xyz"))), 1)
helper.assertInvalidResponse(getResponse(mapOf(Pair("Content-Type", "mp3"))), 2)
helper.assertInvalidResponse(getResponse(mapOf(Pair("Content-Type", "mp3"))), 3)
helper.assertInvalidResponse(getResponse(mapOf(Pair("Content-Type", "audio/mpeg"))), 4)
helper.assertInvalidResponse(getResponse(mapOf(Pair("Content-Type", "audio/aif"))), 5)
helper.assertInvalidResponse(getResponse(mapOf(Pair("Content-Type", "whatever"))), 6)
helper.assertInvalidResponse(getResponse(mapOf()), 7)
helper.assertValidResponse(
getResponse(mapOf(Pair("Content-Type", "audio/flac"))), 8, MediaFormat.FLAC
)
helper.assertValidResponse(
getResponse(mapOf(Pair("Content-Type", "audio/wav"))), 9, MediaFormat.WAV
)
helper.assertValidResponse(
getResponse(mapOf(Pair("Content-Type", "audio/opus"))), 10, MediaFormat.OPUS
)
helper.assertValidResponse(
getResponse(mapOf(Pair("Content-Type", "audio/aiff"))), 11, MediaFormat.AIFF
)
}
/**
* @return a list of video streams, in which their video only property mirrors the provided
* [videoOnly] vararg.
*/
private fun getVideoStreams(vararg videoOnly: Boolean) =
StreamItemAdapter.StreamInfoWrapper(
StreamItemAdapter.StreamSizeWrapper(
videoOnly.map {
VideoStream.Builder()
.setId(Stream.ID_UNKNOWN)
@@ -264,19 +161,6 @@ class StreamItemAdapterTest {
}
)
private fun getIncompleteAudioStreams(size: Int): List<AudioStream> {
val list = ArrayList<AudioStream>(size)
for (i in 1..size) {
list.add(
AudioStream.Builder()
.setId(Stream.ID_UNKNOWN)
.setContent("https://example.com/$i", true)
.build()
)
}
return list
}
/**
* Checks whether the item at [position] in the [spinner] has the correct icon visibility when
* it is shown in normal mode (selected) and in dropdown mode (user is choosing one of a list).
@@ -312,56 +196,11 @@ class StreamItemAdapterTest {
streams.forEachIndexed { index, stream ->
val secondaryStreamHelper: SecondaryStreamHelper<T>? = stream?.let {
SecondaryStreamHelper(
StreamItemAdapter.StreamInfoWrapper(streams, context),
StreamItemAdapter.StreamSizeWrapper(streams, context),
it
)
}
put(index, secondaryStreamHelper)
}
}
private fun getResponse(headers: Map<String, String>): Response {
val listHeaders = HashMap<String, List<String>>()
headers.forEach { entry ->
listHeaders[entry.key] = listOf(entry.value)
}
return Response(200, null, listHeaders, "", "")
}
/**
* Helper class for assertion related to extractions of [MediaFormat]s.
*/
class AssertionHelper<T : Stream>(
private val streams: List<T>,
private val wrapper: StreamInfoWrapper<T>,
private val retrieveMediaFormat: (stream: T, response: Response) -> Boolean
) {
/**
* Assert that an invalid response does not result in wrongly extracted [MediaFormat].
*/
fun assertInvalidResponse(
response: Response,
index: Int
) {
assertFalse(
"invalid header returns valid value", retrieveMediaFormat(streams[index], response)
)
assertNull("Media format extracted although stated otherwise", wrapper.getFormat(index))
}
/**
* Assert that a valid response results in correctly extracted and handled [MediaFormat].
*/
fun assertValidResponse(
response: Response,
index: Int,
format: MediaFormat
) {
assertTrue(
"header was not recognized", retrieveMediaFormat(streams[index], response)
)
assertEquals("Wrong media format extracted", format, wrapper.getFormat(index))
}
}
}

View File

@@ -3,6 +3,7 @@ package org.schabi.newpipe
import androidx.preference.PreferenceManager
import com.facebook.stetho.Stetho
import com.facebook.stetho.okhttp3.StethoInterceptor
import leakcanary.AppWatcher
import leakcanary.LeakCanary
import okhttp3.OkHttpClient
import org.schabi.newpipe.extractor.downloader.Downloader
@@ -12,6 +13,8 @@ class DebugApp : App() {
super.onCreate()
initStetho()
// Give each object 10 seconds to be GC'ed, before LeakCanary gets nosy on it
AppWatcher.config = AppWatcher.config.copy(watchDurationMillis = 10000)
LeakCanary.config = LeakCanary.config.copy(
dumpHeap = PreferenceManager
.getDefaultSharedPreferences(this).getBoolean(

View File

@@ -20,11 +20,9 @@ import org.schabi.newpipe.extractor.downloader.Downloader;
import org.schabi.newpipe.ktx.ExceptionUtils;
import org.schabi.newpipe.settings.NewPipeSettings;
import org.schabi.newpipe.util.Localization;
import org.schabi.newpipe.util.image.ImageStrategy;
import org.schabi.newpipe.util.image.PicassoHelper;
import org.schabi.newpipe.util.PicassoHelper;
import org.schabi.newpipe.util.ServiceHelper;
import org.schabi.newpipe.util.StateSaver;
import org.schabi.newpipe.util.image.PreferredImageQuality;
import java.io.IOException;
import java.io.InterruptedIOException;
@@ -101,9 +99,8 @@ public class App extends Application {
// Initialize image loader
final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this);
PicassoHelper.init(this);
ImageStrategy.setPreferredImageQuality(PreferredImageQuality.fromPreferenceKey(this,
prefs.getString(getString(R.string.image_quality_key),
getString(R.string.image_quality_default))));
PicassoHelper.setShouldLoadImages(
prefs.getBoolean(getString(R.string.download_thumbnail_key), true));
PicassoHelper.setIndicatorsEnabled(MainActivity.DEBUG
&& prefs.getBoolean(getString(R.string.show_image_indicators_key), false));

View File

@@ -12,6 +12,7 @@ import androidx.fragment.app.FragmentManager;
import icepick.Icepick;
import icepick.State;
import leakcanary.AppWatcher;
public abstract class BaseFragment extends Fragment {
protected final String TAG = getClass().getSimpleName() + "@" + Integer.toHexString(hashCode());
@@ -76,33 +77,20 @@ public abstract class BaseFragment extends Fragment {
protected void onRestoreInstanceState(@NonNull final Bundle savedInstanceState) {
}
@Override
public void onDestroy() {
super.onDestroy();
AppWatcher.INSTANCE.getObjectWatcher().watch(this);
}
/*//////////////////////////////////////////////////////////////////////////
// Init
//////////////////////////////////////////////////////////////////////////*/
/**
* This method is called in {@link #onViewCreated(View, Bundle)} to initialize the views.
*
* <p>
* {@link #initListeners()} is called after this method to initialize the corresponding
* listeners.
* </p>
* @param rootView The inflated view for this fragment
* (provided by {@link #onViewCreated(View, Bundle)})
* @param savedInstanceState The saved state of this fragment
* (provided by {@link #onViewCreated(View, Bundle)})
*/
protected void initViews(final View rootView, final Bundle savedInstanceState) {
}
/**
* Initialize the listeners for this fragment.
*
* <p>
* This method is called after {@link #initViews(View, Bundle)}
* in {@link #onViewCreated(View, Bundle)}.
* </p>
*/
protected void initListeners() {
}
@@ -120,20 +108,9 @@ public abstract class BaseFragment extends Fragment {
}
}
/**
* Finds the root fragment by looping through all of the parent fragments. The root fragment
* is supposed to be {@link org.schabi.newpipe.fragments.MainFragment}, and is the fragment that
* handles keeping the backstack of opened fragments in NewPipe, and also the player bottom
* sheet. This function therefore returns the fragment manager of said fragment.
*
* @return the fragment manager of the root fragment, i.e.
* {@link org.schabi.newpipe.fragments.MainFragment}
*/
protected FragmentManager getFM() {
Fragment current = this;
while (current.getParentFragment() != null) {
current = current.getParentFragment();
}
return current.getFragmentManager();
return getParentFragment() == null
? getFragmentManager()
: getParentFragment().getFragmentManager();
}
}

View File

@@ -63,6 +63,7 @@ import org.schabi.newpipe.databinding.InstanceSpinnerLayoutBinding;
import org.schabi.newpipe.databinding.ToolbarLayoutBinding;
import org.schabi.newpipe.error.ErrorUtil;
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;
@@ -219,14 +220,14 @@ public class MainActivity extends AppCompatActivity {
final int currentServiceId = ServiceHelper.getSelectedServiceId(this);
final StreamingService service = NewPipe.getService(currentServiceId);
int kioskMenuItemId = 0;
int kioskId = 0;
for (final String ks : service.getKioskList().getAvailableKiosks()) {
drawerLayoutBinding.navigation.getMenu()
.add(R.id.menu_tabs_group, kioskMenuItemId, 0, KioskTranslator
.add(R.id.menu_tabs_group, kioskId, 0, KioskTranslator
.getTranslatedKioskName(ks, this))
.setIcon(KioskTranslator.getKioskIcon(ks));
kioskMenuItemId++;
kioskId++;
}
drawerLayoutBinding.navigation.getMenu()
@@ -258,8 +259,15 @@ public class MainActivity extends AppCompatActivity {
private boolean drawerItemSelected(final MenuItem item) {
switch (item.getGroupId()) {
case R.id.menu_services_group:
changeService(item);
break;
if (item.getItemId() == ServiceList.PeerTube.getServiceId()
&& DeviceUtils.isTv(getApplicationContext())
&& !item.isActionViewExpanded()) {
((Spinner) item.getActionView()).performClick();
return true;
} else {
changeService(item);
break;
}
case R.id.menu_tabs_group:
try {
tabSelected(item);
@@ -306,16 +314,20 @@ public class MainActivity extends AppCompatActivity {
NavigationHelper.openStatisticFragment(getSupportFragmentManager());
break;
default:
final StreamingService currentService = ServiceHelper.getSelectedService(this);
int kioskMenuItemId = 0;
for (final String kioskId : currentService.getKioskList().getAvailableKiosks()) {
if (kioskMenuItemId == item.getItemId()) {
NavigationHelper.openKioskFragment(getSupportFragmentManager(),
currentService.getServiceId(), kioskId);
break;
final int currentServiceId = ServiceHelper.getSelectedServiceId(this);
final StreamingService service = NewPipe.getService(currentServiceId);
String serviceName = "";
int kioskId = 0;
for (final String ks : service.getKioskList().getAvailableKiosks()) {
if (kioskId == item.getItemId()) {
serviceName = ks;
}
kioskMenuItemId++;
kioskId++;
}
NavigationHelper.openKioskFragment(getSupportFragmentManager(), currentServiceId,
serviceName);
break;
}
}
@@ -379,8 +391,8 @@ public class MainActivity extends AppCompatActivity {
.add(R.id.menu_services_group, s.getServiceId(), ORDER, title)
.setIcon(ServiceHelper.getIcon(s.getServiceId()));
// peertube specifics
if (s.getServiceId() == 3) {
// PeerTube specifics
if (s == ServiceList.PeerTube) {
enhancePeertubeMenu(menuItem);
}
}

View File

@@ -120,13 +120,13 @@ class NewVersionWorker(
// Parse the json from the response.
try {
val newpipeVersionInfo = JsonParser.`object`()
val githubStableObject = JsonParser.`object`()
.from(response.responseBody()).getObject("flavors")
.getObject("newpipe")
.getObject("github").getObject("stable")
val versionName = newpipeVersionInfo.getString("version")
val versionCode = newpipeVersionInfo.getInt("version_code")
val apkLocationUrl = newpipeVersionInfo.getString("apk")
val versionName = githubStableObject.getString("version")
val versionCode = githubStableObject.getInt("version_code")
val apkLocationUrl = githubStableObject.getString("apk")
compareAppVersionAndShowNotification(versionName, apkLocationUrl, versionCode)
} catch (e: JsonParserException) {
// Most likely something is wrong in data received from NEWPIPE_API_URL.

View File

@@ -75,7 +75,7 @@ public final class QueueItemMenuUtil {
return true;
case R.id.menu_item_share:
shareText(context, item.getTitle(), item.getUrl(),
item.getThumbnails());
item.getThumbnailUrl());
return true;
case R.id.menu_item_download:
fetchStreamInfoAndSaveToDatabase(context, item.getServiceId(), item.getUrl(),

View File

@@ -45,7 +45,6 @@ import org.schabi.newpipe.database.stream.model.StreamEntity;
import org.schabi.newpipe.databinding.ListRadioIconItemBinding;
import org.schabi.newpipe.databinding.SingleChoiceDialogViewBinding;
import org.schabi.newpipe.download.DownloadDialog;
import org.schabi.newpipe.download.LoadingDialog;
import org.schabi.newpipe.error.ErrorInfo;
import org.schabi.newpipe.error.ErrorUtil;
import org.schabi.newpipe.error.ReCaptchaActivity;
@@ -65,7 +64,6 @@ import org.schabi.newpipe.extractor.exceptions.PrivateContentException;
import org.schabi.newpipe.extractor.exceptions.ReCaptchaException;
import org.schabi.newpipe.extractor.exceptions.SoundCloudGoPlusContentException;
import org.schabi.newpipe.extractor.exceptions.YoutubeMusicPremiumContentException;
import org.schabi.newpipe.extractor.linkhandler.ListLinkHandler;
import org.schabi.newpipe.extractor.playlist.PlaylistInfo;
import org.schabi.newpipe.extractor.stream.StreamInfo;
import org.schabi.newpipe.ktx.ExceptionUtils;
@@ -73,11 +71,10 @@ import org.schabi.newpipe.local.dialog.PlaylistDialog;
import org.schabi.newpipe.player.PlayerType;
import org.schabi.newpipe.player.helper.PlayerHelper;
import org.schabi.newpipe.player.helper.PlayerHolder;
import org.schabi.newpipe.player.playqueue.ChannelTabPlayQueue;
import org.schabi.newpipe.player.playqueue.ChannelPlayQueue;
import org.schabi.newpipe.player.playqueue.PlayQueue;
import org.schabi.newpipe.player.playqueue.PlaylistPlayQueue;
import org.schabi.newpipe.player.playqueue.SinglePlayQueue;
import org.schabi.newpipe.util.ChannelTabHelper;
import org.schabi.newpipe.util.Constants;
import org.schabi.newpipe.util.DeviceUtils;
import org.schabi.newpipe.util.ExtractorHelper;
@@ -792,10 +789,10 @@ public class RouterActivity extends AppCompatActivity {
}
}
}, () ->
}, () -> {
// this branch is executed if there is no activity context
inFlight(false)
);
inFlight(false);
});
}
<T> Single<T> pleaseWait(final Single<T> single) {
@@ -815,24 +812,19 @@ public class RouterActivity extends AppCompatActivity {
@SuppressLint("CheckResult")
private void openDownloadDialog(final int currentServiceId, final String currentUrl) {
inFlight(true);
final LoadingDialog loadingDialog = new LoadingDialog(R.string.loading_metadata_title);
loadingDialog.show(getParentFragmentManager(), "loadingDialog");
disposables.add(ExtractorHelper.getStreamInfo(currentServiceId, currentUrl, true)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.compose(this::pleaseWait)
.subscribe(result ->
runOnVisible(ctx -> {
loadingDialog.dismiss();
final FragmentManager fm = ctx.getSupportFragmentManager();
final DownloadDialog downloadDialog = new DownloadDialog(ctx, result);
// dismiss listener to be handled by FragmentManager
downloadDialog.show(fm, "downloadDialog");
}
), throwable -> runOnVisible(ctx -> {
loadingDialog.dismiss();
((RouterActivity) ctx).showUnsupportedUrlDialog(currentUrl);
})));
), throwable -> runOnVisible(ctx ->
((RouterActivity) ctx).showUnsupportedUrlDialog(currentUrl))));
}
private void openAddToPlaylistDialog(final int currentServiceId, final String currentUrl) {
@@ -1024,16 +1016,7 @@ public class RouterActivity extends AppCompatActivity {
}
playQueue = new SinglePlayQueue((StreamInfo) info);
} else if (info instanceof ChannelInfo) {
final Optional<ListLinkHandler> playableTab = ((ChannelInfo) info).getTabs()
.stream()
.filter(ChannelTabHelper::isStreamsTab)
.findFirst();
if (playableTab.isPresent()) {
playQueue = new ChannelTabPlayQueue(info.getServiceId(), playableTab.get());
} else {
return; // there is no playable tab
}
playQueue = new ChannelPlayQueue((ChannelInfo) info);
} else if (info instanceof PlaylistInfo) {
playQueue = new PlaylistPlayQueue((PlaylistInfo) info);
} else {

View File

@@ -1,38 +1,28 @@
package org.schabi.newpipe.about
import android.os.Bundle
import android.util.Base64
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.webkit.WebView
import androidx.appcompat.app.AlertDialog
import androidx.core.os.bundleOf
import androidx.fragment.app.Fragment
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
import io.reactivex.rxjava3.core.Observable
import io.reactivex.rxjava3.disposables.CompositeDisposable
import io.reactivex.rxjava3.disposables.Disposable
import io.reactivex.rxjava3.schedulers.Schedulers
import org.schabi.newpipe.BuildConfig
import org.schabi.newpipe.R
import org.schabi.newpipe.databinding.FragmentLicensesBinding
import org.schabi.newpipe.databinding.ItemSoftwareComponentBinding
import org.schabi.newpipe.util.Localization
import org.schabi.newpipe.util.external_communication.ShareUtils
/**
* Fragment containing the software licenses.
*/
class LicenseFragment : Fragment() {
private lateinit var softwareComponents: Array<SoftwareComponent>
private var activeSoftwareComponent: SoftwareComponent? = null
private var activeLicense: License? = null
private val compositeDisposable = CompositeDisposable()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
softwareComponents = arguments?.getParcelableArray(ARG_COMPONENTS) as Array<SoftwareComponent>
activeSoftwareComponent = savedInstanceState?.getSerializable(SOFTWARE_COMPONENT_KEY) as? SoftwareComponent
activeLicense = savedInstanceState?.getSerializable(LICENSE_KEY) as? License
// Sort components by name
softwareComponents.sortBy { it.name }
}
@@ -49,8 +39,9 @@ class LicenseFragment : Fragment() {
): View {
val binding = FragmentLicensesBinding.inflate(inflater, container, false)
binding.licensesAppReadLicense.setOnClickListener {
activeLicense = StandardLicenses.GPL3
compositeDisposable.add(
showLicense(NEWPIPE_SOFTWARE_COMPONENT)
showLicense(activity, StandardLicenses.GPL3)
)
}
for (component in softwareComponents) {
@@ -66,70 +57,26 @@ class LicenseFragment : Fragment() {
val root: View = componentBinding.root
root.tag = component
root.setOnClickListener {
activeLicense = component.license
compositeDisposable.add(
showLicense(component)
showLicense(activity, component)
)
}
binding.licensesSoftwareComponents.addView(root)
registerForContextMenu(root)
}
activeSoftwareComponent?.let { compositeDisposable.add(showLicense(it)) }
activeLicense?.let { compositeDisposable.add(showLicense(activity, it)) }
return binding.root
}
override fun onSaveInstanceState(savedInstanceState: Bundle) {
super.onSaveInstanceState(savedInstanceState)
activeSoftwareComponent?.let { savedInstanceState.putSerializable(SOFTWARE_COMPONENT_KEY, it) }
}
private fun showLicense(
softwareComponent: SoftwareComponent
): Disposable {
return if (context == null) {
Disposable.empty()
} else {
val context = requireContext()
activeSoftwareComponent = softwareComponent
Observable.fromCallable { getFormattedLicense(context, softwareComponent.license) }
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe { formattedLicense ->
val webViewData = Base64.encodeToString(
formattedLicense.toByteArray(), Base64.NO_PADDING
)
val webView = WebView(context)
webView.loadData(webViewData, "text/html; charset=UTF-8", "base64")
Localization.assureCorrectAppLanguage(context)
val builder = AlertDialog.Builder(requireContext())
.setTitle(softwareComponent.name)
.setView(webView)
.setOnCancelListener { activeSoftwareComponent = null }
.setOnDismissListener { activeSoftwareComponent = null }
.setPositiveButton(R.string.done) { dialog, _ -> dialog.dismiss() }
if (softwareComponent != NEWPIPE_SOFTWARE_COMPONENT) {
builder.setNeutralButton(R.string.open_website_license) { _, _ ->
ShareUtils.openUrlInApp(requireContext(), softwareComponent.link)
}
}
builder.show()
}
}
activeLicense?.let { savedInstanceState.putSerializable(LICENSE_KEY, it) }
}
companion object {
private const val ARG_COMPONENTS = "components"
private const val SOFTWARE_COMPONENT_KEY = "ACTIVE_SOFTWARE_COMPONENT"
private val NEWPIPE_SOFTWARE_COMPONENT = SoftwareComponent(
"NewPipe",
"2014-2023",
"Team NewPipe",
"https://newpipe.net/",
StandardLicenses.GPL3,
BuildConfig.VERSION_NAME
)
private const val LICENSE_KEY = "ACTIVE_LICENSE"
fun newInstance(softwareComponents: Array<SoftwareComponent>): LicenseFragment {
val fragment = LicenseFragment()
fragment.arguments = bundleOf(ARG_COMPONENTS to softwareComponents)

View File

@@ -1,8 +1,17 @@
package org.schabi.newpipe.about
import android.content.Context
import android.util.Base64
import android.webkit.WebView
import androidx.appcompat.app.AlertDialog
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
import io.reactivex.rxjava3.core.Observable
import io.reactivex.rxjava3.disposables.Disposable
import io.reactivex.rxjava3.schedulers.Schedulers
import org.schabi.newpipe.R
import org.schabi.newpipe.util.Localization
import org.schabi.newpipe.util.ThemeHelper
import org.schabi.newpipe.util.external_communication.ShareUtils
import java.io.IOException
/**
@@ -11,7 +20,7 @@ import java.io.IOException
* @return String which contains a HTML formatted license page
* styled according to the context's theme
*/
fun getFormattedLicense(context: Context, license: License): String {
private fun getFormattedLicense(context: Context, license: License): String {
try {
return context.assets.open(license.filename).bufferedReader().use { it.readText() }
// split the HTML file and insert the stylesheet into the HEAD of the file
@@ -25,7 +34,7 @@ fun getFormattedLicense(context: Context, license: License): String {
* @param context the Android context
* @return String which is a CSS stylesheet according to the context's theme
*/
fun getLicenseStylesheet(context: Context): String {
private fun getLicenseStylesheet(context: Context): String {
val isLightTheme = ThemeHelper.isLightThemeSelected(context)
val licenseBackgroundColor = getHexRGBColor(
context, if (isLightTheme) R.color.light_license_background_color else R.color.dark_license_background_color
@@ -47,6 +56,48 @@ fun getLicenseStylesheet(context: Context): String {
* @param color the color number from R.color
* @return a six characters long String with hexadecimal RGB values
*/
fun getHexRGBColor(context: Context, color: Int): String {
private fun getHexRGBColor(context: Context, color: Int): String {
return context.getString(color).substring(3)
}
fun showLicense(context: Context?, component: SoftwareComponent): Disposable {
return showLicense(context, component.license) {
setPositiveButton(R.string.dismiss) { dialog, _ ->
dialog.dismiss()
}
setNeutralButton(R.string.open_website_license) { _, _ ->
ShareUtils.openUrlInApp(context!!, component.link)
}
}
}
fun showLicense(context: Context?, license: License) = showLicense(context, license) {
setPositiveButton(R.string.ok) { dialog, _ -> dialog.dismiss() }
}
private fun showLicense(
context: Context?,
license: License,
block: AlertDialog.Builder.() -> AlertDialog.Builder
): Disposable {
return if (context == null) {
Disposable.empty()
} else {
Observable.fromCallable { getFormattedLicense(context, license) }
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe { formattedLicense ->
val webViewData =
Base64.encodeToString(formattedLicense.toByteArray(), Base64.NO_PADDING)
val webView = WebView(context)
webView.loadData(webViewData, "text/html; charset=UTF-8", "base64")
Localization.assureCorrectAppLanguage(context)
AlertDialog.Builder(context)
.setTitle(license.name)
.setView(webView)
.block()
.show()
}
}
}

View File

@@ -2,7 +2,6 @@ package org.schabi.newpipe.about
import android.os.Parcelable
import kotlinx.parcelize.Parcelize
import java.io.Serializable
@Parcelize
class SoftwareComponent
@@ -14,4 +13,4 @@ constructor(
val link: String,
val license: License,
val version: String? = null
) : Parcelable, Serializable
) : Parcelable

View File

@@ -93,30 +93,18 @@ abstract class FeedDAO {
uploadDateBefore: OffsetDateTime?
): Maybe<List<StreamWithState>>
/**
* Remove links to streams that are older than the given date
* **but keep at least one stream per uploader**.
*
* One stream per uploader is kept because it is needed as reference
* when fetching new streams to check if they are new or not.
* @param offsetDateTime the newest date to keep, older streams are removed
*/
@Query(
"""
DELETE FROM feed
WHERE feed.stream_id IN (SELECT uid from (
SELECT s.uid,
(SELECT MAX(upload_date)
FROM streams s1
INNER JOIN feed f1
ON s1.uid = f1.stream_id
WHERE f1.subscription_id = f.subscription_id) max_upload_date
FROM streams s
INNER JOIN feed f
ON s.uid = f.stream_id
WHERE s.upload_date < :offsetDateTime
AND s.upload_date <> max_upload_date))
DELETE FROM feed WHERE
feed.stream_id IN (
SELECT s.uid FROM streams s
INNER JOIN feed f
ON s.uid = f.stream_id
WHERE s.upload_date < :offsetDateTime
)
"""
)
abstract fun unlinkStreamsOlderThan(offsetDateTime: OffsetDateTime)

View File

@@ -3,6 +3,7 @@ package org.schabi.newpipe.database.feed.model
import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.ForeignKey
import androidx.room.ForeignKey.CASCADE
import androidx.room.Index
import org.schabi.newpipe.database.feed.model.FeedGroupSubscriptionEntity.Companion.FEED_GROUP_SUBSCRIPTION_TABLE
import org.schabi.newpipe.database.feed.model.FeedGroupSubscriptionEntity.Companion.GROUP_ID
@@ -18,14 +19,14 @@ import org.schabi.newpipe.database.subscription.SubscriptionEntity
entity = FeedGroupEntity::class,
parentColumns = [FeedGroupEntity.ID],
childColumns = [GROUP_ID],
onDelete = ForeignKey.CASCADE, onUpdate = ForeignKey.CASCADE, deferred = true
onDelete = CASCADE, onUpdate = CASCADE, deferred = true
),
ForeignKey(
entity = SubscriptionEntity::class,
parentColumns = [SubscriptionEntity.SUBSCRIPTION_UID],
childColumns = [SUBSCRIPTION_ID],
onDelete = ForeignKey.CASCADE, onUpdate = ForeignKey.CASCADE, deferred = true
onDelete = CASCADE, onUpdate = CASCADE, deferred = true
)
]
)

View File

@@ -7,7 +7,6 @@ import org.schabi.newpipe.database.playlist.model.PlaylistStreamEntity
import org.schabi.newpipe.database.stream.model.StreamEntity
import org.schabi.newpipe.database.stream.model.StreamStateEntity
import org.schabi.newpipe.extractor.stream.StreamInfoItem
import org.schabi.newpipe.util.image.ImageStrategy
data class PlaylistStreamEntry(
@Embedded
@@ -29,7 +28,7 @@ data class PlaylistStreamEntry(
item.duration = streamEntity.duration
item.uploaderName = streamEntity.uploader
item.uploaderUrl = streamEntity.uploaderUrl
item.thumbnails = ImageStrategy.dbUrlToImageList(streamEntity.thumbnailUrl)
item.thumbnailUrl = streamEntity.thumbnailUrl
return item
}

View File

@@ -11,7 +11,6 @@ import androidx.room.PrimaryKey;
import org.schabi.newpipe.database.playlist.PlaylistLocalItem;
import org.schabi.newpipe.extractor.playlist.PlaylistInfo;
import org.schabi.newpipe.util.Constants;
import org.schabi.newpipe.util.image.ImageStrategy;
import static org.schabi.newpipe.database.LocalItem.LocalItemType.PLAYLIST_REMOTE_ITEM;
import static org.schabi.newpipe.database.playlist.model.PlaylistRemoteEntity.REMOTE_PLAYLIST_NAME;
@@ -70,9 +69,8 @@ public class PlaylistRemoteEntity implements PlaylistLocalItem {
@Ignore
public PlaylistRemoteEntity(final PlaylistInfo info) {
this(info.getServiceId(), info.getName(), info.getUrl(),
// use uploader avatar when no thumbnail is available
ImageStrategy.imageListToDbUrl(info.getThumbnails().isEmpty()
? info.getUploaderAvatars() : info.getThumbnails()),
info.getThumbnailUrl() == null
? info.getUploaderAvatarUrl() : info.getThumbnailUrl(),
info.getUploaderName(), info.getStreamCount());
}
@@ -86,10 +84,7 @@ public class PlaylistRemoteEntity implements PlaylistLocalItem {
&& getStreamCount() == info.getStreamCount()
&& TextUtils.equals(getName(), info.getName())
&& TextUtils.equals(getUrl(), info.getUrl())
// we want to update the local playlist data even when either the remote thumbnail
// URL changes, or the preferred image quality setting is changed by the user
&& TextUtils.equals(getThumbnailUrl(),
ImageStrategy.imageListToDbUrl(info.getThumbnails()))
&& TextUtils.equals(getThumbnailUrl(), info.getThumbnailUrl())
&& TextUtils.equals(getUploader(), info.getUploaderName());
}

View File

@@ -7,7 +7,6 @@ import org.schabi.newpipe.database.history.model.StreamHistoryEntity
import org.schabi.newpipe.database.stream.model.StreamEntity
import org.schabi.newpipe.database.stream.model.StreamStateEntity.STREAM_PROGRESS_MILLIS
import org.schabi.newpipe.extractor.stream.StreamInfoItem
import org.schabi.newpipe.util.image.ImageStrategy
import java.time.OffsetDateTime
class StreamStatisticsEntry(
@@ -31,7 +30,7 @@ class StreamStatisticsEntry(
item.duration = streamEntity.duration
item.uploaderName = streamEntity.uploader
item.uploaderUrl = streamEntity.uploaderUrl
item.thumbnails = ImageStrategy.dbUrlToImageList(streamEntity.thumbnailUrl)
item.thumbnailUrl = streamEntity.thumbnailUrl
return item
}

View File

@@ -13,7 +13,6 @@ import org.schabi.newpipe.extractor.stream.StreamInfo
import org.schabi.newpipe.extractor.stream.StreamInfoItem
import org.schabi.newpipe.extractor.stream.StreamType
import org.schabi.newpipe.player.playqueue.PlayQueueItem
import org.schabi.newpipe.util.image.ImageStrategy
import java.io.Serializable
import java.time.OffsetDateTime
@@ -68,8 +67,7 @@ data class StreamEntity(
constructor(item: StreamInfoItem) : this(
serviceId = item.serviceId, url = item.url, title = item.name,
streamType = item.streamType, duration = item.duration, uploader = item.uploaderName,
uploaderUrl = item.uploaderUrl,
thumbnailUrl = ImageStrategy.imageListToDbUrl(item.thumbnails), viewCount = item.viewCount,
uploaderUrl = item.uploaderUrl, thumbnailUrl = item.thumbnailUrl, viewCount = item.viewCount,
textualUploadDate = item.textualUploadDate, uploadDate = item.uploadDate?.offsetDateTime(),
isUploadDateApproximation = item.uploadDate?.isApproximation
)
@@ -78,8 +76,7 @@ data class StreamEntity(
constructor(info: StreamInfo) : this(
serviceId = info.serviceId, url = info.url, title = info.name,
streamType = info.streamType, duration = info.duration, uploader = info.uploaderName,
uploaderUrl = info.uploaderUrl,
thumbnailUrl = ImageStrategy.imageListToDbUrl(info.thumbnails), viewCount = info.viewCount,
uploaderUrl = info.uploaderUrl, thumbnailUrl = info.thumbnailUrl, viewCount = info.viewCount,
textualUploadDate = info.textualUploadDate, uploadDate = info.uploadDate?.offsetDateTime(),
isUploadDateApproximation = info.uploadDate?.isApproximation
)
@@ -88,8 +85,7 @@ data class StreamEntity(
constructor(item: PlayQueueItem) : this(
serviceId = item.serviceId, url = item.url, title = item.title,
streamType = item.streamType, duration = item.duration, uploader = item.uploader,
uploaderUrl = item.uploaderUrl,
thumbnailUrl = ImageStrategy.imageListToDbUrl(item.thumbnails)
uploaderUrl = item.uploaderUrl, thumbnailUrl = item.thumbnailUrl
)
fun toStreamInfoItem(): StreamInfoItem {
@@ -97,7 +93,7 @@ data class StreamEntity(
item.duration = duration
item.uploaderName = uploader
item.uploaderUrl = uploaderUrl
item.thumbnails = ImageStrategy.dbUrlToImageList(thumbnailUrl)
item.thumbnailUrl = thumbnailUrl
if (viewCount != null) item.viewCount = viewCount as Long
item.textualUploadDate = textualUploadDate

View File

@@ -10,7 +10,6 @@ import androidx.room.PrimaryKey;
import org.schabi.newpipe.extractor.channel.ChannelInfo;
import org.schabi.newpipe.extractor.channel.ChannelInfoItem;
import org.schabi.newpipe.util.Constants;
import org.schabi.newpipe.util.image.ImageStrategy;
import static org.schabi.newpipe.database.subscription.SubscriptionEntity.SUBSCRIPTION_SERVICE_ID;
import static org.schabi.newpipe.database.subscription.SubscriptionEntity.SUBSCRIPTION_TABLE;
@@ -58,8 +57,8 @@ public class SubscriptionEntity {
final SubscriptionEntity result = new SubscriptionEntity();
result.setServiceId(info.getServiceId());
result.setUrl(info.getUrl());
result.setData(info.getName(), ImageStrategy.imageListToDbUrl(info.getAvatars()),
info.getDescription(), info.getSubscriberCount());
result.setData(info.getName(), info.getAvatarUrl(), info.getDescription(),
info.getSubscriberCount());
return result;
}
@@ -139,7 +138,7 @@ public class SubscriptionEntity {
@Ignore
public ChannelInfoItem toChannelInfoItem() {
final ChannelInfoItem item = new ChannelInfoItem(getServiceId(), getUrl(), getName());
item.setThumbnails(ImageStrategy.dbUrlToImageList(getAvatarUrl()));
item.setThumbnailUrl(getAvatarUrl());
item.setSubscriberCount(getSubscriberCount());
item.setDescription(getDescription());
return item;

View File

@@ -67,7 +67,7 @@ import org.schabi.newpipe.util.PermissionHelper;
import org.schabi.newpipe.util.SecondaryStreamHelper;
import org.schabi.newpipe.util.SimpleOnSeekBarChangeListener;
import org.schabi.newpipe.util.StreamItemAdapter;
import org.schabi.newpipe.util.StreamItemAdapter.StreamInfoWrapper;
import org.schabi.newpipe.util.StreamItemAdapter.StreamSizeWrapper;
import org.schabi.newpipe.util.AudioTrackAdapter;
import org.schabi.newpipe.util.AudioTrackAdapter.AudioTracksWrapper;
import org.schabi.newpipe.util.ThemeHelper;
@@ -97,9 +97,9 @@ public class DownloadDialog extends DialogFragment
@State
StreamInfo currentInfo;
@State
StreamInfoWrapper<VideoStream> wrappedVideoStreams;
StreamSizeWrapper<VideoStream> wrappedVideoStreams;
@State
StreamInfoWrapper<SubtitlesStream> wrappedSubtitleStreams;
StreamSizeWrapper<SubtitlesStream> wrappedSubtitleStreams;
@State
AudioTracksWrapper wrappedAudioTracks;
@State
@@ -187,8 +187,8 @@ public class DownloadDialog extends DialogFragment
wrappedAudioTracks.size() > 1
);
this.wrappedVideoStreams = new StreamInfoWrapper<>(videoStreams, context);
this.wrappedSubtitleStreams = new StreamInfoWrapper<>(
this.wrappedVideoStreams = new StreamSizeWrapper<>(videoStreams, context);
this.wrappedSubtitleStreams = new StreamSizeWrapper<>(
getStreamsOfSpecifiedDelivery(info.getSubtitles(), PROGRESSIVE_HTTP), context);
this.selectedVideoIndex = ListHelper.getDefaultResolutionIndex(context, videoStreams);
@@ -258,17 +258,17 @@ public class DownloadDialog extends DialogFragment
* Update the displayed video streams based on the selected audio track.
*/
private void updateSecondaryStreams() {
final StreamInfoWrapper<AudioStream> audioStreams = getWrappedAudioStreams();
final StreamSizeWrapper<AudioStream> audioStreams = getWrappedAudioStreams();
final var secondaryStreams = new SparseArrayCompat<SecondaryStreamHelper<AudioStream>>(4);
final List<VideoStream> videoStreams = wrappedVideoStreams.getStreamsList();
wrappedVideoStreams.resetInfo();
wrappedVideoStreams.resetSizes();
for (int i = 0; i < videoStreams.size(); i++) {
if (!videoStreams.get(i).isVideoOnly()) {
continue;
}
final AudioStream audioStream = SecondaryStreamHelper.getAudioStreamFor(
context, audioStreams.getStreamsList(), videoStreams.get(i));
final AudioStream audioStream = SecondaryStreamHelper
.getAudioStreamFor(audioStreams.getStreamsList(), videoStreams.get(i));
if (audioStream != null) {
secondaryStreams.append(i, new SecondaryStreamHelper<>(audioStreams, audioStream));
@@ -313,7 +313,6 @@ public class DownloadDialog extends DialogFragment
selectedSubtitleIndex = getSubtitleIndexBy(subtitleStreamsAdapter.getAll());
dialogBinding.qualitySpinner.setOnItemSelectedListener(this);
dialogBinding.audioStreamSpinner.setOnItemSelectedListener(this);
dialogBinding.audioTrackSpinner.setOnItemSelectedListener(this);
dialogBinding.videoAudioGroup.setOnCheckedChangeListener(this);
@@ -396,7 +395,7 @@ public class DownloadDialog extends DialogFragment
private void fetchStreamsSize() {
disposables.clear();
disposables.add(StreamInfoWrapper.fetchMoreInfoForWrapper(wrappedVideoStreams)
disposables.add(StreamSizeWrapper.fetchSizeForWrapper(wrappedVideoStreams)
.subscribe(result -> {
if (dialogBinding.videoAudioGroup.getCheckedRadioButtonId()
== R.id.video_button) {
@@ -406,7 +405,7 @@ public class DownloadDialog extends DialogFragment
new ErrorInfo(throwable, UserAction.DOWNLOAD_OPEN_DIALOG,
"Downloading video stream size",
currentInfo.getServiceId()))));
disposables.add(StreamInfoWrapper.fetchMoreInfoForWrapper(getWrappedAudioStreams())
disposables.add(StreamSizeWrapper.fetchSizeForWrapper(getWrappedAudioStreams())
.subscribe(result -> {
if (dialogBinding.videoAudioGroup.getCheckedRadioButtonId()
== R.id.audio_button) {
@@ -416,7 +415,7 @@ public class DownloadDialog extends DialogFragment
new ErrorInfo(throwable, UserAction.DOWNLOAD_OPEN_DIALOG,
"Downloading audio stream size",
currentInfo.getServiceId()))));
disposables.add(StreamInfoWrapper.fetchMoreInfoForWrapper(wrappedSubtitleStreams)
disposables.add(StreamSizeWrapper.fetchSizeForWrapper(wrappedSubtitleStreams)
.subscribe(result -> {
if (dialogBinding.videoAudioGroup.getCheckedRadioButtonId()
== R.id.subtitle_button) {
@@ -724,9 +723,9 @@ public class DownloadDialog extends DialogFragment
dialogBinding.subtitleButton.setEnabled(enabled);
}
private StreamInfoWrapper<AudioStream> getWrappedAudioStreams() {
private StreamSizeWrapper<AudioStream> getWrappedAudioStreams() {
if (selectedAudioTrackIndex < 0 || selectedAudioTrackIndex > wrappedAudioTracks.size()) {
return StreamInfoWrapper.empty();
return StreamSizeWrapper.empty();
}
return wrappedAudioTracks.getTracksList().get(selectedAudioTrackIndex);
}
@@ -766,7 +765,7 @@ public class DownloadDialog extends DialogFragment
}
private void showFailedDialog(@StringRes final int msg) {
assureCorrectAppLanguage(requireContext());
assureCorrectAppLanguage(getContext());
new AlertDialog.Builder(context)
.setTitle(R.string.general_error)
.setMessage(msg)
@@ -799,7 +798,7 @@ public class DownloadDialog extends DialogFragment
filenameTmp += "opus";
} else if (format != null) {
mimeTmp = format.mimeType;
filenameTmp += format.getSuffix();
filenameTmp += format.suffix;
}
break;
case R.id.video_button:
@@ -808,7 +807,7 @@ public class DownloadDialog extends DialogFragment
format = videoStreamsAdapter.getItem(selectedVideoIndex).getFormat();
if (format != null) {
mimeTmp = format.mimeType;
filenameTmp += format.getSuffix();
filenameTmp += format.suffix;
}
break;
case R.id.subtitle_button:
@@ -820,9 +819,9 @@ public class DownloadDialog extends DialogFragment
}
if (format == MediaFormat.TTML) {
filenameTmp += MediaFormat.SRT.getSuffix();
filenameTmp += MediaFormat.SRT.suffix;
} else if (format != null) {
filenameTmp += format.getSuffix();
filenameTmp += format.suffix;
}
break;
default:

View File

@@ -1,87 +0,0 @@
package org.schabi.newpipe.download;
import android.os.Bundle;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.StringRes;
import androidx.appcompat.widget.Toolbar;
import androidx.fragment.app.DialogFragment;
import org.schabi.newpipe.MainActivity;
import org.schabi.newpipe.R;
import org.schabi.newpipe.databinding.DownloadLoadingDialogBinding;
/**
* This class contains a dialog which shows a loading indicator and has a customizable title.
*/
public class LoadingDialog extends DialogFragment {
private static final String TAG = "LoadingDialog";
private static final boolean DEBUG = MainActivity.DEBUG;
private DownloadLoadingDialogBinding dialogLoadingBinding;
private final @StringRes int title;
/**
* Create a new LoadingDialog.
*
* <p>
* The dialog contains a loading indicator and has a customizable title.
* <br/>
* Use {@code show()} to display the dialog to the user.
* </p>
*
* @param title an informative title shown in the dialog's toolbar
*/
public LoadingDialog(final @StringRes int title) {
this.title = title;
}
@Override
public void onCreate(@Nullable final Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
if (DEBUG) {
Log.d(TAG, "onCreate() called with: "
+ "savedInstanceState = [" + savedInstanceState + "]");
}
this.setCancelable(false);
}
@Override
public View onCreateView(
@NonNull final LayoutInflater inflater,
final ViewGroup container,
final Bundle savedInstanceState) {
if (DEBUG) {
Log.d(TAG, "onCreateView() called with: "
+ "inflater = [" + inflater + "], container = [" + container + "], "
+ "savedInstanceState = [" + savedInstanceState + "]");
}
return inflater.inflate(R.layout.download_loading_dialog, container);
}
@Override
public void onViewCreated(@NonNull final View view, @Nullable final Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
dialogLoadingBinding = DownloadLoadingDialogBinding.bind(view);
initToolbar(dialogLoadingBinding.toolbarLayout.toolbar);
}
private void initToolbar(final Toolbar toolbar) {
if (DEBUG) {
Log.d(TAG, "initToolbar() called with: toolbar = [" + toolbar + "]");
}
toolbar.setTitle(requireContext().getString(title));
toolbar.setNavigationOnClickListener(v -> dismiss());
}
@Override
public void onDestroyView() {
dialogLoadingBinding = null;
super.onDestroyView();
}
}

View File

@@ -11,6 +11,7 @@ import org.schabi.newpipe.extractor.exceptions.AccountTerminatedException
import org.schabi.newpipe.extractor.exceptions.ContentNotAvailableException
import org.schabi.newpipe.extractor.exceptions.ContentNotSupportedException
import org.schabi.newpipe.extractor.exceptions.ExtractionException
import org.schabi.newpipe.extractor.services.youtube.extractors.YoutubeStreamExtractor.DeobfuscateException
import org.schabi.newpipe.ktx.isNetworkRelated
import org.schabi.newpipe.util.ServiceHelper
@@ -95,6 +96,7 @@ class ErrorInfo(
throwable is ContentNotAvailableException -> R.string.content_not_available
throwable != null && throwable.isNetworkRelated -> R.string.network_error
throwable is ContentNotSupportedException -> R.string.content_not_supported
throwable is DeobfuscateException -> R.string.youtube_signature_deobfuscation_error
throwable is ExtractionException -> R.string.parsing_error
throwable is ExoPlaybackException -> {
when (throwable.type) {

View File

@@ -1,16 +1,12 @@
package org.schabi.newpipe.fragments;
import static org.schabi.newpipe.ktx.ViewUtils.animate;
import android.os.Bundle;
import android.util.Log;
import android.view.View;
import android.widget.ProgressBar;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.StringRes;
import androidx.fragment.app.Fragment;
import org.schabi.newpipe.BaseFragment;
@@ -24,15 +20,15 @@ import java.util.concurrent.atomic.AtomicBoolean;
import icepick.State;
import static org.schabi.newpipe.ktx.ViewUtils.animate;
public abstract class BaseStateFragment<I> extends BaseFragment implements ViewContract<I> {
@State
protected AtomicBoolean wasLoading = new AtomicBoolean();
protected AtomicBoolean isLoading = new AtomicBoolean();
@Nullable
protected View emptyStateView;
@Nullable
protected TextView emptyStateMessageView;
private View emptyStateView;
@Nullable
private ProgressBar loadingProgressBar;
@@ -69,7 +65,6 @@ public abstract class BaseStateFragment<I> extends BaseFragment implements ViewC
protected void initViews(final View rootView, final Bundle savedInstanceState) {
super.initViews(rootView, savedInstanceState);
emptyStateView = rootView.findViewById(R.id.empty_state_view);
emptyStateMessageView = rootView.findViewById(R.id.empty_state_message);
loadingProgressBar = rootView.findViewById(R.id.loading_progress_bar);
errorPanelHelper = new ErrorPanelHelper(this, rootView, this::onRetryButtonClicked);
}
@@ -80,8 +75,6 @@ public abstract class BaseStateFragment<I> extends BaseFragment implements ViewC
if (errorPanelHelper != null) {
errorPanelHelper.dispose();
}
emptyStateView = null;
emptyStateMessageView = null;
}
protected void onRetryButtonClicked() {
@@ -196,12 +189,6 @@ public abstract class BaseStateFragment<I> extends BaseFragment implements ViewC
errorPanelHelper.showTextError(errorString);
}
protected void setEmptyStateMessage(@StringRes final int text) {
if (emptyStateMessageView != null) {
emptyStateMessageView.setText(text);
}
}
public final void hideErrorPanel() {
errorPanelHelper.hide();
lastPanelError = null;

View File

@@ -1,16 +1,6 @@
package org.schabi.newpipe.fragments;
import static android.widget.RelativeLayout.ABOVE;
import static android.widget.RelativeLayout.ALIGN_PARENT_BOTTOM;
import static android.widget.RelativeLayout.ALIGN_PARENT_TOP;
import static android.widget.RelativeLayout.BELOW;
import static com.google.android.material.tabs.TabLayout.INDICATOR_GRAVITY_BOTTOM;
import static com.google.android.material.tabs.TabLayout.INDICATOR_GRAVITY_TOP;
import android.content.Context;
import android.content.SharedPreferences;
import android.content.res.ColorStateList;
import android.graphics.Color;
import android.os.Bundle;
import android.util.Log;
import android.view.LayoutInflater;
@@ -19,9 +9,7 @@ import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.widget.RelativeLayout;
import androidx.annotation.ColorInt;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.ActionBar;
@@ -29,7 +17,6 @@ import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentManager;
import androidx.fragment.app.FragmentStatePagerAdapterMenuWorkaround;
import androidx.preference.PreferenceManager;
import androidx.viewpager.widget.ViewPager;
import com.google.android.material.tabs.TabLayout;
@@ -38,13 +25,10 @@ import org.schabi.newpipe.R;
import org.schabi.newpipe.databinding.FragmentMainBinding;
import org.schabi.newpipe.error.ErrorUtil;
import org.schabi.newpipe.extractor.exceptions.ExtractionException;
import org.schabi.newpipe.local.playlist.LocalPlaylistFragment;
import org.schabi.newpipe.settings.tabs.Tab;
import org.schabi.newpipe.settings.tabs.TabsManager;
import org.schabi.newpipe.util.NavigationHelper;
import org.schabi.newpipe.util.ServiceHelper;
import org.schabi.newpipe.util.ThemeHelper;
import org.schabi.newpipe.views.ScrollableTabLayout;
import java.util.ArrayList;
import java.util.List;
@@ -58,11 +42,8 @@ public class MainFragment extends BaseFragment implements TabLayout.OnTabSelecte
private boolean hasTabsChanged = false;
private SharedPreferences prefs;
private boolean youtubeRestrictedModeEnabled;
private boolean previousYoutubeRestrictedModeEnabled;
private String youtubeRestrictedModeEnabledKey;
private boolean mainTabsPositionBottom;
private String mainTabsPositionKey;
/*//////////////////////////////////////////////////////////////////////////
// Fragment's LifeCycle
@@ -85,11 +66,10 @@ public class MainFragment extends BaseFragment implements TabLayout.OnTabSelecte
}
});
prefs = PreferenceManager.getDefaultSharedPreferences(requireContext());
youtubeRestrictedModeEnabledKey = getString(R.string.youtube_restricted_mode_enabled);
youtubeRestrictedModeEnabled = prefs.getBoolean(youtubeRestrictedModeEnabledKey, false);
mainTabsPositionKey = getString(R.string.main_tabs_position_key);
mainTabsPositionBottom = prefs.getBoolean(mainTabsPositionKey, false);
previousYoutubeRestrictedModeEnabled =
PreferenceManager.getDefaultSharedPreferences(requireContext())
.getBoolean(youtubeRestrictedModeEnabledKey, false);
}
@Override
@@ -107,26 +87,24 @@ public class MainFragment extends BaseFragment implements TabLayout.OnTabSelecte
binding.mainTabLayout.setupWithViewPager(binding.pager);
binding.mainTabLayout.addOnTabSelectedListener(this);
binding.mainTabLayout.setTabRippleColor(binding.mainTabLayout.getTabRippleColor()
.withAlpha(32));
setupTabs();
updateTabLayoutPosition();
}
@Override
public void onResume() {
super.onResume();
final boolean newYoutubeRestrictedModeEnabled =
prefs.getBoolean(youtubeRestrictedModeEnabledKey, false);
if (youtubeRestrictedModeEnabled != newYoutubeRestrictedModeEnabled || hasTabsChanged) {
youtubeRestrictedModeEnabled = newYoutubeRestrictedModeEnabled;
final boolean youtubeRestrictedModeEnabled =
PreferenceManager.getDefaultSharedPreferences(requireContext())
.getBoolean(youtubeRestrictedModeEnabledKey, false);
if (previousYoutubeRestrictedModeEnabled != youtubeRestrictedModeEnabled) {
previousYoutubeRestrictedModeEnabled = youtubeRestrictedModeEnabled;
setupTabs();
} else if (hasTabsChanged) {
setupTabs();
}
final boolean newMainTabsPosition = prefs.getBoolean(mainTabsPositionKey, false);
if (mainTabsPositionBottom != newMainTabsPosition) {
mainTabsPositionBottom = newMainTabsPosition;
updateTabLayoutPosition();
}
}
@@ -140,12 +118,6 @@ public class MainFragment extends BaseFragment implements TabLayout.OnTabSelecte
}
}
@Override
public void onDestroyView() {
super.onDestroyView();
binding = null;
}
/*//////////////////////////////////////////////////////////////////////////
// Menu
//////////////////////////////////////////////////////////////////////////*/
@@ -194,6 +166,7 @@ public class MainFragment extends BaseFragment implements TabLayout.OnTabSelecte
}
binding.pager.setAdapter(null);
binding.pager.setOffscreenPageLimit(tabsList.size());
binding.pager.setAdapter(pagerAdapter);
updateTabsIconAndDescription();
@@ -217,44 +190,6 @@ public class MainFragment extends BaseFragment implements TabLayout.OnTabSelecte
setTitle(tabsList.get(tabPosition).getTabName(requireContext()));
}
public void commitPlaylistTabs() {
pagerAdapter.getLocalPlaylistFragments()
.stream()
.forEach(LocalPlaylistFragment::commitChanges);
}
private void updateTabLayoutPosition() {
final ScrollableTabLayout tabLayout = binding.mainTabLayout;
final ViewPager viewPager = binding.pager;
final boolean bottom = mainTabsPositionBottom;
// change layout params to make the tab layout appear either at the top or at the bottom
final var tabParams = (RelativeLayout.LayoutParams) tabLayout.getLayoutParams();
final var pagerParams = (RelativeLayout.LayoutParams) viewPager.getLayoutParams();
tabParams.removeRule(bottom ? ALIGN_PARENT_TOP : ALIGN_PARENT_BOTTOM);
tabParams.addRule(bottom ? ALIGN_PARENT_BOTTOM : ALIGN_PARENT_TOP);
pagerParams.removeRule(bottom ? BELOW : ABOVE);
pagerParams.addRule(bottom ? ABOVE : BELOW, R.id.main_tab_layout);
tabLayout.setSelectedTabIndicatorGravity(
bottom ? INDICATOR_GRAVITY_TOP : INDICATOR_GRAVITY_BOTTOM);
tabLayout.setLayoutParams(tabParams);
viewPager.setLayoutParams(pagerParams);
// change the background and icon color of the tab layout:
// service-colored at the top, app-background-colored at the bottom
tabLayout.setBackgroundColor(ThemeHelper.resolveColorFromAttr(requireContext(),
bottom ? R.attr.colorSecondary : R.attr.colorPrimary));
@ColorInt final int iconColor = bottom
? ThemeHelper.resolveColorFromAttr(requireContext(), R.attr.colorAccent)
: Color.WHITE;
tabLayout.setTabRippleColor(ColorStateList.valueOf(iconColor).withAlpha(32));
tabLayout.setTabIconTint(ColorStateList.valueOf(iconColor));
tabLayout.setSelectedTabIndicatorColor(iconColor);
}
@Override
public void onTabSelected(final TabLayout.Tab selectedTab) {
if (DEBUG) {
@@ -274,18 +209,10 @@ public class MainFragment extends BaseFragment implements TabLayout.OnTabSelecte
updateTitleForTab(tab.getPosition());
}
public static final class SelectedTabsPagerAdapter
private static final class SelectedTabsPagerAdapter
extends FragmentStatePagerAdapterMenuWorkaround {
private final Context context;
private final List<Tab> internalTabsList;
/**
* Keep reference to LocalPlaylistFragments, because their data can be modified by the user
* during runtime and changes are not committed immediately. However, in some cases,
* the changes need to be committed immediately by calling
* {@link LocalPlaylistFragment#commitChanges()}.
* The fragments are removed when {@link LocalPlaylistFragment#onDestroy()} is called.
*/
private final List<LocalPlaylistFragment> localPlaylistFragments = new ArrayList<>();
private SelectedTabsPagerAdapter(final Context context,
final FragmentManager fragmentManager,
@@ -312,17 +239,9 @@ public class MainFragment extends BaseFragment implements TabLayout.OnTabSelecte
((BaseFragment) fragment).useAsFrontPage(true);
}
if (fragment instanceof LocalPlaylistFragment) {
localPlaylistFragments.add((LocalPlaylistFragment) fragment);
}
return fragment;
}
public List<LocalPlaylistFragment> getLocalPlaylistFragments() {
return localPlaylistFragments;
}
@Override
public int getItemPosition(@NonNull final Object object) {
// Causes adapter to reload all Fragments when

View File

@@ -1,285 +0,0 @@
package org.schabi.newpipe.fragments.detail;
import static android.text.TextUtils.isEmpty;
import static org.schabi.newpipe.extractor.utils.Utils.isBlank;
import static org.schabi.newpipe.util.text.TextLinkifier.SET_LINK_MOVEMENT_METHOD;
import android.graphics.Typeface;
import android.os.Bundle;
import android.text.SpannableStringBuilder;
import android.text.Spanned;
import android.text.method.LinkMovementMethod;
import android.text.style.ClickableSpan;
import android.text.style.StyleSpan;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.LinearLayout;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.StringRes;
import androidx.appcompat.widget.TooltipCompat;
import androidx.core.text.HtmlCompat;
import com.google.android.material.chip.Chip;
import org.schabi.newpipe.BaseFragment;
import org.schabi.newpipe.R;
import org.schabi.newpipe.databinding.FragmentDescriptionBinding;
import org.schabi.newpipe.databinding.ItemMetadataBinding;
import org.schabi.newpipe.databinding.ItemMetadataTagsBinding;
import org.schabi.newpipe.extractor.Image;
import org.schabi.newpipe.extractor.StreamingService;
import org.schabi.newpipe.extractor.stream.Description;
import org.schabi.newpipe.util.NavigationHelper;
import org.schabi.newpipe.util.external_communication.ShareUtils;
import org.schabi.newpipe.util.image.ImageStrategy;
import org.schabi.newpipe.util.text.TextLinkifier;
import java.util.List;
import io.reactivex.rxjava3.disposables.CompositeDisposable;
public abstract class BaseDescriptionFragment extends BaseFragment {
private final CompositeDisposable descriptionDisposables = new CompositeDisposable();
protected FragmentDescriptionBinding binding;
@Override
public View onCreateView(@NonNull final LayoutInflater inflater,
@Nullable final ViewGroup container,
@Nullable final Bundle savedInstanceState) {
binding = FragmentDescriptionBinding.inflate(inflater, container, false);
setupDescription();
setupMetadata(inflater, binding.detailMetadataLayout);
addTagsMetadataItem(inflater, binding.detailMetadataLayout);
return binding.getRoot();
}
@Override
public void onDestroy() {
descriptionDisposables.clear();
super.onDestroy();
}
/**
* Get the description to display.
* @return description object
*/
@Nullable
protected abstract Description getDescription();
/**
* Get the streaming service. Used for generating description links.
* @return streaming service
*/
@Nullable
protected abstract StreamingService getService();
/**
* Get the streaming service ID. Used for tag links.
* @return service ID
*/
protected abstract int getServiceId();
/**
* Get the URL of the described video or audio, used to generate description links.
* @return stream URL
*/
@Nullable
protected abstract String getStreamUrl();
/**
* Get the list of tags to display below the description.
* @return tag list
*/
@Nullable
public abstract List<String> getTags();
/**
* Add additional metadata to display.
* @param inflater LayoutInflater
* @param layout detailMetadataLayout
*/
protected abstract void setupMetadata(LayoutInflater inflater, LinearLayout layout);
private void setupDescription() {
final Description description = getDescription();
if (description == null || isEmpty(description.getContent())
|| description == Description.EMPTY_DESCRIPTION) {
binding.detailDescriptionView.setVisibility(View.GONE);
binding.detailSelectDescriptionButton.setVisibility(View.GONE);
return;
}
// start with disabled state. This also loads description content (!)
disableDescriptionSelection();
binding.detailSelectDescriptionButton.setOnClickListener(v -> {
if (binding.detailDescriptionNoteView.getVisibility() == View.VISIBLE) {
disableDescriptionSelection();
} else {
// enable selection only when button is clicked to prevent flickering
enableDescriptionSelection();
}
});
}
private void enableDescriptionSelection() {
binding.detailDescriptionNoteView.setVisibility(View.VISIBLE);
binding.detailDescriptionView.setTextIsSelectable(true);
final String buttonLabel = getString(R.string.description_select_disable);
binding.detailSelectDescriptionButton.setContentDescription(buttonLabel);
TooltipCompat.setTooltipText(binding.detailSelectDescriptionButton, buttonLabel);
binding.detailSelectDescriptionButton.setImageResource(R.drawable.ic_close);
}
private void disableDescriptionSelection() {
// show description content again, otherwise some links are not clickable
final Description description = getDescription();
if (description != null) {
TextLinkifier.fromDescription(binding.detailDescriptionView,
description, HtmlCompat.FROM_HTML_MODE_LEGACY,
getService(), getStreamUrl(),
descriptionDisposables, SET_LINK_MOVEMENT_METHOD);
}
binding.detailDescriptionNoteView.setVisibility(View.GONE);
binding.detailDescriptionView.setTextIsSelectable(false);
final String buttonLabel = getString(R.string.description_select_enable);
binding.detailSelectDescriptionButton.setContentDescription(buttonLabel);
TooltipCompat.setTooltipText(binding.detailSelectDescriptionButton, buttonLabel);
binding.detailSelectDescriptionButton.setImageResource(R.drawable.ic_select_all);
}
protected void addMetadataItem(final LayoutInflater inflater,
final LinearLayout layout,
final boolean linkifyContent,
@StringRes final int type,
@Nullable final String content) {
if (isBlank(content)) {
return;
}
final ItemMetadataBinding itemBinding =
ItemMetadataBinding.inflate(inflater, layout, false);
itemBinding.metadataTypeView.setText(type);
itemBinding.metadataTypeView.setOnLongClickListener(v -> {
ShareUtils.copyToClipboard(requireContext(), content);
return true;
});
if (linkifyContent) {
TextLinkifier.fromPlainText(itemBinding.metadataContentView, content, null, null,
descriptionDisposables, SET_LINK_MOVEMENT_METHOD);
} else {
itemBinding.metadataContentView.setText(content);
}
itemBinding.metadataContentView.setClickable(true);
layout.addView(itemBinding.getRoot());
}
private String imageSizeToText(final int heightOrWidth) {
if (heightOrWidth < 0) {
return getString(R.string.question_mark);
} else {
return String.valueOf(heightOrWidth);
}
}
protected void addImagesMetadataItem(final LayoutInflater inflater,
final LinearLayout layout,
@StringRes final int type,
final List<Image> images) {
final String preferredImageUrl = ImageStrategy.choosePreferredImage(images);
if (preferredImageUrl == null) {
return; // null will be returned in case there is no image
}
final ItemMetadataBinding itemBinding =
ItemMetadataBinding.inflate(inflater, layout, false);
itemBinding.metadataTypeView.setText(type);
final SpannableStringBuilder urls = new SpannableStringBuilder();
for (final Image image : images) {
if (urls.length() != 0) {
urls.append(", ");
}
final int entryBegin = urls.length();
if (image.getHeight() != Image.HEIGHT_UNKNOWN
|| image.getWidth() != Image.WIDTH_UNKNOWN
// if even the resolution level is unknown, ?x? will be shown
|| image.getEstimatedResolutionLevel() == Image.ResolutionLevel.UNKNOWN) {
urls.append(imageSizeToText(image.getHeight()));
urls.append('x');
urls.append(imageSizeToText(image.getWidth()));
} else {
switch (image.getEstimatedResolutionLevel()) {
case LOW:
urls.append(getString(R.string.image_quality_low));
break;
default: // unreachable, Image.ResolutionLevel.UNKNOWN is already filtered out
case MEDIUM:
urls.append(getString(R.string.image_quality_medium));
break;
case HIGH:
urls.append(getString(R.string.image_quality_high));
break;
}
}
urls.setSpan(new ClickableSpan() {
@Override
public void onClick(@NonNull final View widget) {
ShareUtils.openUrlInBrowser(requireContext(), image.getUrl());
}
}, entryBegin, urls.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
if (preferredImageUrl.equals(image.getUrl())) {
urls.setSpan(new StyleSpan(Typeface.BOLD), entryBegin, urls.length(),
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
}
}
itemBinding.metadataContentView.setText(urls);
itemBinding.metadataContentView.setMovementMethod(LinkMovementMethod.getInstance());
layout.addView(itemBinding.getRoot());
}
private void addTagsMetadataItem(final LayoutInflater inflater, final LinearLayout layout) {
final List<String> tags = getTags();
if (tags != null && !tags.isEmpty()) {
final var itemBinding = ItemMetadataTagsBinding.inflate(inflater, layout, false);
tags.stream().sorted(String.CASE_INSENSITIVE_ORDER).forEach(tag -> {
final Chip chip = (Chip) inflater.inflate(R.layout.chip,
itemBinding.metadataTagsChips, false);
chip.setText(tag);
chip.setOnClickListener(this::onTagClick);
chip.setOnLongClickListener(this::onTagLongClick);
itemBinding.metadataTagsChips.addView(chip);
});
layout.addView(itemBinding.getRoot());
}
}
private void onTagClick(final View chip) {
if (getParentFragment() != null) {
NavigationHelper.openSearchFragment(getParentFragment().getParentFragmentManager(),
getServiceId(), ((Chip) chip).getText().toString());
}
}
private boolean onTagLongClick(final View chip) {
ShareUtils.copyToClipboard(requireContext(), ((Chip) chip).getText().toString());
return true;
}
}

View File

@@ -1,29 +1,46 @@
package org.schabi.newpipe.fragments.detail;
import static android.text.TextUtils.isEmpty;
import static org.schabi.newpipe.extractor.stream.StreamExtractor.NO_AGE_LIMIT;
import static org.schabi.newpipe.extractor.utils.Utils.isBlank;
import static org.schabi.newpipe.util.Localization.getAppLocale;
import static org.schabi.newpipe.util.text.TextLinkifier.SET_LINK_MOVEMENT_METHOD;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.LinearLayout;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.StringRes;
import androidx.appcompat.widget.TooltipCompat;
import androidx.core.text.HtmlCompat;
import com.google.android.material.chip.Chip;
import org.schabi.newpipe.BaseFragment;
import org.schabi.newpipe.R;
import org.schabi.newpipe.extractor.StreamingService;
import org.schabi.newpipe.databinding.FragmentDescriptionBinding;
import org.schabi.newpipe.databinding.ItemMetadataBinding;
import org.schabi.newpipe.databinding.ItemMetadataTagsBinding;
import org.schabi.newpipe.extractor.stream.Description;
import org.schabi.newpipe.extractor.stream.StreamInfo;
import org.schabi.newpipe.util.Localization;
import java.util.List;
import org.schabi.newpipe.util.NavigationHelper;
import org.schabi.newpipe.util.external_communication.ShareUtils;
import org.schabi.newpipe.util.text.TextLinkifier;
import icepick.State;
import io.reactivex.rxjava3.disposables.CompositeDisposable;
public class DescriptionFragment extends BaseDescriptionFragment {
public class DescriptionFragment extends BaseFragment {
@State
StreamInfo streamInfo = null;
final CompositeDisposable descriptionDisposables = new CompositeDisposable();
FragmentDescriptionBinding binding;
public DescriptionFragment() {
}
@@ -32,64 +49,86 @@ public class DescriptionFragment extends BaseDescriptionFragment {
this.streamInfo = streamInfo;
}
@Nullable
@Override
protected Description getDescription() {
if (streamInfo == null) {
return null;
public View onCreateView(@NonNull final LayoutInflater inflater,
@Nullable final ViewGroup container,
@Nullable final Bundle savedInstanceState) {
binding = FragmentDescriptionBinding.inflate(inflater, container, false);
if (streamInfo != null) {
setupUploadDate();
setupDescription();
setupMetadata(inflater, binding.detailMetadataLayout);
}
return streamInfo.getDescription();
}
@Nullable
@Override
protected StreamingService getService() {
if (streamInfo == null) {
return null;
}
return streamInfo.getService();
return binding.getRoot();
}
@Override
protected int getServiceId() {
if (streamInfo == null) {
return -1;
}
return streamInfo.getServiceId();
public void onDestroy() {
descriptionDisposables.clear();
super.onDestroy();
}
@Nullable
@Override
protected String getStreamUrl() {
if (streamInfo == null) {
return null;
}
return streamInfo.getUrl();
}
@Nullable
@Override
public List<String> getTags() {
if (streamInfo == null) {
return null;
}
return streamInfo.getTags();
}
@Override
protected void setupMetadata(final LayoutInflater inflater,
final LinearLayout layout) {
if (streamInfo != null && streamInfo.getUploadDate() != null) {
private void setupUploadDate() {
if (streamInfo.getUploadDate() != null) {
binding.detailUploadDateView.setText(Localization
.localizeUploadDate(activity, streamInfo.getUploadDate().offsetDateTime()));
} else {
binding.detailUploadDateView.setVisibility(View.GONE);
}
}
if (streamInfo == null) {
private void setupDescription() {
final Description description = streamInfo.getDescription();
if (description == null || isEmpty(description.getContent())
|| description == Description.EMPTY_DESCRIPTION) {
binding.detailDescriptionView.setVisibility(View.GONE);
binding.detailSelectDescriptionButton.setVisibility(View.GONE);
return;
}
// start with disabled state. This also loads description content (!)
disableDescriptionSelection();
binding.detailSelectDescriptionButton.setOnClickListener(v -> {
if (binding.detailDescriptionNoteView.getVisibility() == View.VISIBLE) {
disableDescriptionSelection();
} else {
// enable selection only when button is clicked to prevent flickering
enableDescriptionSelection();
}
});
}
private void enableDescriptionSelection() {
binding.detailDescriptionNoteView.setVisibility(View.VISIBLE);
binding.detailDescriptionView.setTextIsSelectable(true);
final String buttonLabel = getString(R.string.description_select_disable);
binding.detailSelectDescriptionButton.setContentDescription(buttonLabel);
TooltipCompat.setTooltipText(binding.detailSelectDescriptionButton, buttonLabel);
binding.detailSelectDescriptionButton.setImageResource(R.drawable.ic_close);
}
private void disableDescriptionSelection() {
// show description content again, otherwise some links are not clickable
TextLinkifier.fromDescription(binding.detailDescriptionView,
streamInfo.getDescription(), HtmlCompat.FROM_HTML_MODE_LEGACY,
streamInfo.getService(), streamInfo.getUrl(),
descriptionDisposables, SET_LINK_MOVEMENT_METHOD);
binding.detailDescriptionNoteView.setVisibility(View.GONE);
binding.detailDescriptionView.setTextIsSelectable(false);
final String buttonLabel = getString(R.string.description_select_enable);
binding.detailSelectDescriptionButton.setContentDescription(buttonLabel);
TooltipCompat.setTooltipText(binding.detailSelectDescriptionButton, buttonLabel);
binding.detailSelectDescriptionButton.setImageResource(R.drawable.ic_select_all);
}
private void setupMetadata(final LayoutInflater inflater,
final LinearLayout layout) {
addMetadataItem(inflater, layout, false, R.string.metadata_category,
streamInfo.getCategory());
@@ -112,13 +151,69 @@ public class DescriptionFragment extends BaseDescriptionFragment {
streamInfo.getSupportInfo());
addMetadataItem(inflater, layout, true, R.string.metadata_host,
streamInfo.getHost());
addMetadataItem(inflater, layout, true, R.string.metadata_thumbnail_url,
streamInfo.getThumbnailUrl());
addImagesMetadataItem(inflater, layout, R.string.metadata_thumbnails,
streamInfo.getThumbnails());
addImagesMetadataItem(inflater, layout, R.string.metadata_uploader_avatars,
streamInfo.getUploaderAvatars());
addImagesMetadataItem(inflater, layout, R.string.metadata_subchannel_avatars,
streamInfo.getSubChannelAvatars());
addTagsMetadataItem(inflater, layout);
}
private void addMetadataItem(final LayoutInflater inflater,
final LinearLayout layout,
final boolean linkifyContent,
@StringRes final int type,
@Nullable final String content) {
if (isBlank(content)) {
return;
}
final ItemMetadataBinding itemBinding =
ItemMetadataBinding.inflate(inflater, layout, false);
itemBinding.metadataTypeView.setText(type);
itemBinding.metadataTypeView.setOnLongClickListener(v -> {
ShareUtils.copyToClipboard(requireContext(), content);
return true;
});
if (linkifyContent) {
TextLinkifier.fromPlainText(itemBinding.metadataContentView, content, null, null,
descriptionDisposables, SET_LINK_MOVEMENT_METHOD);
} else {
itemBinding.metadataContentView.setText(content);
}
itemBinding.metadataContentView.setClickable(true);
layout.addView(itemBinding.getRoot());
}
private void addTagsMetadataItem(final LayoutInflater inflater, final LinearLayout layout) {
if (streamInfo.getTags() != null && !streamInfo.getTags().isEmpty()) {
final var itemBinding = ItemMetadataTagsBinding.inflate(inflater, layout, false);
streamInfo.getTags().stream().sorted(String.CASE_INSENSITIVE_ORDER).forEach(tag -> {
final Chip chip = (Chip) inflater.inflate(R.layout.chip,
itemBinding.metadataTagsChips, false);
chip.setText(tag);
chip.setOnClickListener(this::onTagClick);
chip.setOnLongClickListener(this::onTagLongClick);
itemBinding.metadataTagsChips.addView(chip);
});
layout.addView(itemBinding.getRoot());
}
}
private void onTagClick(final View chip) {
if (getParentFragment() != null) {
NavigationHelper.openSearchFragment(getParentFragment().getParentFragmentManager(),
streamInfo.getServiceId(), ((Chip) chip).getText().toString());
}
}
private boolean onTagLongClick(final View chip) {
ShareUtils.copyToClipboard(requireContext(), ((Chip) chip).getText().toString());
return true;
}
private void addPrivacyMetadataItem(final LayoutInflater inflater, final LinearLayout layout) {

View File

@@ -24,6 +24,7 @@ import android.content.pm.ActivityInfo;
import android.database.ContentObserver;
import android.graphics.Color;
import android.graphics.Rect;
import android.graphics.drawable.Drawable;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
@@ -53,7 +54,6 @@ import androidx.appcompat.content.res.AppCompatResources;
import androidx.appcompat.widget.Toolbar;
import androidx.coordinatorlayout.widget.CoordinatorLayout;
import androidx.core.content.ContextCompat;
import androidx.fragment.app.Fragment;
import androidx.preference.PreferenceManager;
import com.google.android.exoplayer2.PlaybackException;
@@ -71,7 +71,6 @@ import org.schabi.newpipe.error.ErrorInfo;
import org.schabi.newpipe.error.ErrorUtil;
import org.schabi.newpipe.error.ReCaptchaActivity;
import org.schabi.newpipe.error.UserAction;
import org.schabi.newpipe.extractor.Image;
import org.schabi.newpipe.extractor.InfoItem;
import org.schabi.newpipe.extractor.NewPipe;
import org.schabi.newpipe.extractor.exceptions.ContentNotSupportedException;
@@ -84,13 +83,11 @@ import org.schabi.newpipe.extractor.stream.VideoStream;
import org.schabi.newpipe.fragments.BackPressable;
import org.schabi.newpipe.fragments.BaseStateFragment;
import org.schabi.newpipe.fragments.EmptyFragment;
import org.schabi.newpipe.fragments.MainFragment;
import org.schabi.newpipe.fragments.list.comments.CommentsFragment;
import org.schabi.newpipe.fragments.list.videos.RelatedItemsFragment;
import org.schabi.newpipe.ktx.AnimationType;
import org.schabi.newpipe.local.dialog.PlaylistDialog;
import org.schabi.newpipe.local.history.HistoryRecordManager;
import org.schabi.newpipe.local.playlist.LocalPlaylistFragment;
import org.schabi.newpipe.player.Player;
import org.schabi.newpipe.player.PlayerService;
import org.schabi.newpipe.player.PlayerType;
@@ -110,12 +107,11 @@ import org.schabi.newpipe.util.ListHelper;
import org.schabi.newpipe.util.Localization;
import org.schabi.newpipe.util.NavigationHelper;
import org.schabi.newpipe.util.PermissionHelper;
import org.schabi.newpipe.util.image.PicassoHelper;
import org.schabi.newpipe.util.PicassoHelper;
import org.schabi.newpipe.util.StreamTypeUtil;
import org.schabi.newpipe.util.ThemeHelper;
import org.schabi.newpipe.util.external_communication.KoreUtils;
import org.schabi.newpipe.util.external_communication.ShareUtils;
import org.schabi.newpipe.util.PlayButtonHelper;
import java.util.ArrayList;
import java.util.Iterator;
@@ -474,23 +470,10 @@ public final class VideoDetailFragment
binding.detailControlsBackground.setOnClickListener(v -> openBackgroundPlayer(false));
binding.detailControlsPopup.setOnClickListener(v -> openPopupPlayer(false));
binding.detailControlsPlaylistAppend.setOnClickListener(makeOnClickListener(info -> {
if (getFM() != null && currentInfo != null) {
final Fragment fragment = getParentFragmentManager().
findFragmentById(R.id.fragment_holder);
// commit previous pending changes to database
if (fragment instanceof LocalPlaylistFragment) {
((LocalPlaylistFragment) fragment).commitChanges();
} else if (fragment instanceof MainFragment) {
((MainFragment) fragment).commitPlaylistTabs();
}
binding.detailControlsPlaylistAppend.setOnClickListener(makeOnClickListener(info ->
disposables.add(PlaylistDialog.createCorrespondingDialog(requireContext(),
List.of(new StreamEntity(info)),
dialog -> dialog.show(getParentFragmentManager(), TAG)));
}
}));
dialog -> dialog.show(getParentFragmentManager(), TAG)))));
binding.detailControlsDownload.setOnClickListener(v -> {
if (PermissionHelper.checkStoragePermissions(activity,
PermissionHelper.DOWNLOAD_DIALOG_REQUEST_CODE)) {
@@ -499,7 +482,7 @@ public final class VideoDetailFragment
});
binding.detailControlsShare.setOnClickListener(makeOnClickListener(info ->
ShareUtils.shareText(requireContext(), info.getName(), info.getUrl(),
info.getThumbnails())));
info.getThumbnailUrl())));
binding.detailControlsOpenInBrowser.setOnClickListener(makeOnClickListener(info ->
ShareUtils.openUrlInBrowser(requireContext(), info.getUrl())));
binding.detailControlsPlayWithKodi.setOnClickListener(makeOnClickListener(info ->
@@ -552,11 +535,9 @@ public final class VideoDetailFragment
}));
binding.detailControlsBackground.setOnLongClickListener(makeOnLongClickListener(info ->
openBackgroundPlayer(true)
));
openBackgroundPlayer(true)));
binding.detailControlsPopup.setOnLongClickListener(makeOnLongClickListener(info ->
openPopupPlayer(true)
));
openPopupPlayer(true)));
binding.detailControlsDownload.setOnLongClickListener(makeOnLongClickListener(info ->
NavigationHelper.openDownloads(activity)));
@@ -639,7 +620,8 @@ public final class VideoDetailFragment
final View.OnTouchListener controlsTouchListener = (view, motionEvent) -> {
if (motionEvent.getAction() == MotionEvent.ACTION_DOWN
&& PlayButtonHelper.shouldShowHoldToAppendTip(activity)) {
&& PreferenceManager.getDefaultSharedPreferences(activity)
.getBoolean(getString(R.string.show_hold_to_append_key), true)) {
animate(binding.touchAppendDetail, true, 250, AnimationType.ALPHA, 0, () ->
animate(binding.touchAppendDetail, false, 1500, AnimationType.ALPHA, 1000));
@@ -739,7 +721,7 @@ public final class VideoDetailFragment
final boolean isPlayerStopped = !isPlayerAvailable() || player.isStopped();
if (playQueueItem != null && isPlayerStopped) {
updateOverlayData(playQueueItem.getTitle(),
playQueueItem.getUploader(), playQueueItem.getThumbnails());
playQueueItem.getUploader(), playQueueItem.getThumbnailUrl());
}
}
@@ -1481,6 +1463,11 @@ public final class VideoDetailFragment
displayUploaderAsSubChannel(info);
}
final Drawable buddyDrawable =
AppCompatResources.getDrawable(activity, R.drawable.placeholder_person);
binding.detailSubChannelThumbnailView.setImageDrawable(buddyDrawable);
binding.detailUploaderThumbnailView.setImageDrawable(buddyDrawable);
if (info.getViewCount() >= 0) {
if (info.getStreamType().equals(StreamType.AUDIO_LIVE_STREAM)) {
binding.detailViewCountView.setText(Localization.listeningCount(activity,
@@ -1547,13 +1534,13 @@ public final class VideoDetailFragment
binding.detailSecondaryControlPanel.setVisibility(View.GONE);
checkUpdateProgressInfo(info);
PicassoHelper.loadDetailsThumbnail(info.getThumbnails()).tag(PICASSO_VIDEO_DETAILS_TAG)
PicassoHelper.loadDetailsThumbnail(info.getThumbnailUrl()).tag(PICASSO_VIDEO_DETAILS_TAG)
.into(binding.detailThumbnailImageView);
showMetaInfoInTextView(info.getMetaInfo(), binding.detailMetaInfoTextView,
binding.detailMetaInfoSeparator, disposables);
if (!isPlayerAvailable() || player.isStopped()) {
updateOverlayData(info.getName(), info.getUploaderName(), info.getThumbnails());
updateOverlayData(info.getName(), info.getUploaderName(), info.getThumbnailUrl());
}
if (!info.getErrors().isEmpty()) {
@@ -1598,7 +1585,7 @@ public final class VideoDetailFragment
binding.detailUploaderTextView.setVisibility(View.GONE);
}
PicassoHelper.loadAvatar(info.getUploaderAvatars()).tag(PICASSO_VIDEO_DETAILS_TAG)
PicassoHelper.loadAvatar(info.getUploaderAvatarUrl()).tag(PICASSO_VIDEO_DETAILS_TAG)
.into(binding.detailSubChannelThumbnailView);
binding.detailSubChannelThumbnailView.setVisibility(View.VISIBLE);
binding.detailUploaderThumbnailView.setVisibility(View.GONE);
@@ -1630,10 +1617,10 @@ public final class VideoDetailFragment
binding.detailUploaderTextView.setVisibility(View.GONE);
}
PicassoHelper.loadAvatar(info.getSubChannelAvatars()).tag(PICASSO_VIDEO_DETAILS_TAG)
PicassoHelper.loadAvatar(info.getSubChannelAvatarUrl()).tag(PICASSO_VIDEO_DETAILS_TAG)
.into(binding.detailSubChannelThumbnailView);
binding.detailSubChannelThumbnailView.setVisibility(View.VISIBLE);
PicassoHelper.loadAvatar(info.getUploaderAvatars()).tag(PICASSO_VIDEO_DETAILS_TAG)
PicassoHelper.loadAvatar(info.getUploaderAvatarUrl()).tag(PICASSO_VIDEO_DETAILS_TAG)
.into(binding.detailUploaderThumbnailView);
binding.detailUploaderThumbnailView.setVisibility(View.VISIBLE);
}
@@ -1808,7 +1795,7 @@ public final class VideoDetailFragment
return;
}
updateOverlayData(info.getName(), info.getUploaderName(), info.getThumbnails());
updateOverlayData(info.getName(), info.getUploaderName(), info.getThumbnailUrl());
if (currentInfo != null && info.getUrl().equals(currentInfo.getUrl())) {
return;
}
@@ -1837,7 +1824,7 @@ public final class VideoDetailFragment
if (currentInfo != null) {
updateOverlayData(currentInfo.getName(),
currentInfo.getUploaderName(),
currentInfo.getThumbnails());
currentInfo.getThumbnailUrl());
}
updateOverlayPlayQueueButtonVisibility();
}
@@ -2202,7 +2189,7 @@ public final class VideoDetailFragment
playerHolder.stopService();
setInitialData(0, null, "", null);
currentInfo = null;
updateOverlayData(null, null, List.of());
updateOverlayData(null, null, null);
}
/*//////////////////////////////////////////////////////////////////////////
@@ -2384,11 +2371,11 @@ public final class VideoDetailFragment
private void updateOverlayData(@Nullable final String overlayTitle,
@Nullable final String uploader,
@NonNull final List<Image> thumbnails) {
@Nullable final String thumbnailUrl) {
binding.overlayTitleTextView.setText(isEmpty(overlayTitle) ? "" : overlayTitle);
binding.overlayChannelTextView.setText(isEmpty(uploader) ? "" : uploader);
binding.overlayThumbnail.setImageDrawable(null);
PicassoHelper.loadDetailsThumbnail(thumbnails).tag(PICASSO_VIDEO_DETAILS_TAG)
PicassoHelper.loadDetailsThumbnail(thumbnailUrl).tag(PICASSO_VIDEO_DETAILS_TAG)
.into(binding.overlayThumbnail);
}

View File

@@ -1,7 +1,5 @@
package org.schabi.newpipe.fragments.list;
import static org.schabi.newpipe.extractor.ServiceList.SoundCloud;
import android.os.Bundle;
import android.text.TextUtils;
import android.util.Log;
@@ -9,13 +7,13 @@ import android.view.View;
import androidx.annotation.NonNull;
import org.schabi.newpipe.R;
import org.schabi.newpipe.error.ErrorInfo;
import org.schabi.newpipe.error.UserAction;
import org.schabi.newpipe.extractor.InfoItem;
import org.schabi.newpipe.extractor.ListExtractor;
import org.schabi.newpipe.extractor.ListInfo;
import org.schabi.newpipe.extractor.Page;
import org.schabi.newpipe.extractor.channel.ChannelInfo;
import org.schabi.newpipe.extractor.exceptions.ContentNotSupportedException;
import org.schabi.newpipe.util.Constants;
import org.schabi.newpipe.views.NewPipeRecyclerView;
@@ -233,7 +231,11 @@ public abstract class BaseListInfoFragment<I extends InfoItem, L extends ListInf
showListFooter(hasMoreItems());
} else {
infoListAdapter.clearStreamItemList();
showEmptyState();
// showEmptyState should be called only if there is no item as
// well as no header in infoListAdapter
if (!(result instanceof ChannelInfo && infoListAdapter.getItemCount() == 1)) {
showEmptyState();
}
}
}
@@ -250,20 +252,6 @@ public abstract class BaseListInfoFragment<I extends InfoItem, L extends ListInf
}
}
@Override
public void showEmptyState() {
// show "no streams" for SoundCloud; otherwise "no videos"
// showing "no live streams" is handled in KioskFragment
if (emptyStateView != null) {
if (currentInfo.getService() == SoundCloud) {
setEmptyStateMessage(R.string.no_streams);
} else {
setEmptyStateMessage(R.string.no_videos);
}
}
super.showEmptyState();
}
/*//////////////////////////////////////////////////////////////////////////
// Utils
//////////////////////////////////////////////////////////////////////////*/

View File

@@ -1,107 +0,0 @@
package org.schabi.newpipe.fragments.list.channel;
import static org.schabi.newpipe.extractor.stream.StreamExtractor.UNKNOWN_SUBSCRIBER_COUNT;
import android.content.Context;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.widget.LinearLayout;
import androidx.annotation.Nullable;
import org.schabi.newpipe.R;
import org.schabi.newpipe.extractor.StreamingService;
import org.schabi.newpipe.extractor.channel.ChannelInfo;
import org.schabi.newpipe.extractor.stream.Description;
import org.schabi.newpipe.fragments.detail.BaseDescriptionFragment;
import org.schabi.newpipe.util.DeviceUtils;
import org.schabi.newpipe.util.Localization;
import java.util.List;
import icepick.State;
public class ChannelAboutFragment extends BaseDescriptionFragment {
@State
protected ChannelInfo channelInfo;
public static ChannelAboutFragment getInstance(final ChannelInfo channelInfo) {
final ChannelAboutFragment fragment = new ChannelAboutFragment();
fragment.channelInfo = channelInfo;
return fragment;
}
public ChannelAboutFragment() {
super();
}
@Override
protected void initViews(final View rootView, final Bundle savedInstanceState) {
super.initViews(rootView, savedInstanceState);
binding.constraintLayout.setPadding(0, DeviceUtils.dpToPx(8, requireContext()), 0, 0);
}
@Nullable
@Override
protected Description getDescription() {
if (channelInfo == null) {
return null;
}
return new Description(channelInfo.getDescription(), Description.PLAIN_TEXT);
}
@Nullable
@Override
protected StreamingService getService() {
if (channelInfo == null) {
return null;
}
return channelInfo.getService();
}
@Override
protected int getServiceId() {
if (channelInfo == null) {
return -1;
}
return channelInfo.getServiceId();
}
@Nullable
@Override
protected String getStreamUrl() {
return null;
}
@Nullable
@Override
public List<String> getTags() {
if (channelInfo == null) {
return null;
}
return channelInfo.getTags();
}
@Override
protected void setupMetadata(final LayoutInflater inflater,
final LinearLayout layout) {
// There is no upload date available for channels, so hide the relevant UI element
binding.detailUploadDateView.setVisibility(View.GONE);
if (channelInfo == null) {
return;
}
final Context context = getContext();
if (channelInfo.getSubscriberCount() != UNKNOWN_SUBSCRIBER_COUNT) {
addMetadataItem(inflater, layout, false, R.string.metadata_subscribers,
Localization.localizeNumber(context, channelInfo.getSubscriberCount()));
}
addImagesMetadataItem(inflater, layout, R.string.metadata_avatars,
channelInfo.getAvatars());
addImagesMetadataItem(inflater, layout, R.string.metadata_banners,
channelInfo.getBanners());
}
}

View File

@@ -5,7 +5,6 @@ import static org.schabi.newpipe.ktx.ViewUtils.animate;
import static org.schabi.newpipe.ktx.ViewUtils.animateBackgroundColor;
import android.content.Context;
import android.content.SharedPreferences;
import android.graphics.Color;
import android.os.Bundle;
import android.text.TextUtils;
@@ -17,50 +16,51 @@ import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Button;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.ActionBar;
import androidx.core.content.ContextCompat;
import androidx.core.graphics.ColorUtils;
import androidx.preference.PreferenceManager;
import com.google.android.material.snackbar.Snackbar;
import com.google.android.material.tabs.TabLayout;
import com.jakewharton.rxbinding4.view.RxView;
import org.schabi.newpipe.R;
import org.schabi.newpipe.database.subscription.NotificationMode;
import org.schabi.newpipe.database.subscription.SubscriptionEntity;
import org.schabi.newpipe.databinding.ChannelHeaderBinding;
import org.schabi.newpipe.databinding.FragmentChannelBinding;
import org.schabi.newpipe.databinding.PlaylistControlBinding;
import org.schabi.newpipe.error.ErrorInfo;
import org.schabi.newpipe.error.ErrorUtil;
import org.schabi.newpipe.error.UserAction;
import org.schabi.newpipe.extractor.ListExtractor;
import org.schabi.newpipe.extractor.channel.ChannelInfo;
import org.schabi.newpipe.extractor.exceptions.ContentNotSupportedException;
import org.schabi.newpipe.extractor.linkhandler.ListLinkHandler;
import org.schabi.newpipe.fragments.BaseStateFragment;
import org.schabi.newpipe.fragments.detail.TabAdapter;
import org.schabi.newpipe.extractor.stream.StreamInfoItem;
import org.schabi.newpipe.fragments.list.BaseListInfoFragment;
import org.schabi.newpipe.ktx.AnimationType;
import org.schabi.newpipe.local.feed.notifications.NotificationHelper;
import org.schabi.newpipe.local.subscription.SubscriptionManager;
import org.schabi.newpipe.util.ChannelTabHelper;
import org.schabi.newpipe.util.Constants;
import org.schabi.newpipe.local.feed.notifications.NotificationHelper;
import org.schabi.newpipe.player.PlayerType;
import org.schabi.newpipe.player.playqueue.ChannelPlayQueue;
import org.schabi.newpipe.player.playqueue.PlayQueue;
import org.schabi.newpipe.util.ExtractorHelper;
import org.schabi.newpipe.util.Localization;
import org.schabi.newpipe.util.NavigationHelper;
import org.schabi.newpipe.util.StateSaver;
import org.schabi.newpipe.util.image.ImageStrategy;
import org.schabi.newpipe.util.image.PicassoHelper;
import org.schabi.newpipe.util.PicassoHelper;
import org.schabi.newpipe.util.ThemeHelper;
import org.schabi.newpipe.util.external_communication.ShareUtils;
import java.util.List;
import java.util.Queue;
import java.util.concurrent.TimeUnit;
import java.util.function.Supplier;
import java.util.stream.Collectors;
import icepick.State;
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
import io.reactivex.rxjava3.core.Observable;
import io.reactivex.rxjava3.core.Single;
import io.reactivex.rxjava3.disposables.CompositeDisposable;
import io.reactivex.rxjava3.disposables.Disposable;
import io.reactivex.rxjava3.functions.Action;
@@ -68,37 +68,29 @@ import io.reactivex.rxjava3.functions.Consumer;
import io.reactivex.rxjava3.functions.Function;
import io.reactivex.rxjava3.schedulers.Schedulers;
public class ChannelFragment extends BaseStateFragment<ChannelInfo>
implements StateSaver.WriteRead {
public class ChannelFragment extends BaseListInfoFragment<StreamInfoItem, ChannelInfo>
implements View.OnClickListener {
private static final int BUTTON_DEBOUNCE_INTERVAL = 100;
private static final String PICASSO_CHANNEL_TAG = "PICASSO_CHANNEL_TAG";
@State
protected int serviceId = Constants.NO_SERVICE_ID;
@State
protected String name;
@State
protected String url;
private ChannelInfo currentInfo;
private Disposable currentWorker;
private final CompositeDisposable disposables = new CompositeDisposable();
private Disposable subscribeButtonMonitor;
private SubscriptionManager subscriptionManager;
private int lastTab;
private boolean channelContentNotSupported = false;
/*//////////////////////////////////////////////////////////////////////////
// Views
//////////////////////////////////////////////////////////////////////////*/
private FragmentChannelBinding binding;
private TabAdapter tabAdapter;
private SubscriptionManager subscriptionManager;
private FragmentChannelBinding channelBinding;
private ChannelHeaderBinding headerBinding;
private PlaylistControlBinding playlistControlBinding;
private MenuItem menuRssButton;
private MenuItem menuNotifyButton;
private SubscriptionEntity channelSubscription;
public static ChannelFragment getInstance(final int serviceId, final String url,
final String name) {
@@ -107,23 +99,22 @@ public class ChannelFragment extends BaseStateFragment<ChannelInfo>
return instance;
}
private void setInitialData(final int sid, final String u, final String title) {
this.serviceId = sid;
this.url = u;
this.name = !TextUtils.isEmpty(title) ? title : "";
public ChannelFragment() {
super(UserAction.REQUESTED_CHANNEL);
}
@Override
public void onResume() {
super.onResume();
if (activity != null && useAsFrontPage) {
setTitle(currentInfo != null ? currentInfo.getName() : name);
}
}
/*//////////////////////////////////////////////////////////////////////////
// LifeCycle
//////////////////////////////////////////////////////////////////////////*/
@Override
public void onCreate(final Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setHasOptionsMenu(true);
}
@Override
public void onAttach(@NonNull final Context context) {
super.onAttach(context);
@@ -134,58 +125,49 @@ public class ChannelFragment extends BaseStateFragment<ChannelInfo>
public View onCreateView(@NonNull final LayoutInflater inflater,
@Nullable final ViewGroup container,
@Nullable final Bundle savedInstanceState) {
binding = FragmentChannelBinding.inflate(inflater, container, false);
return binding.getRoot();
return inflater.inflate(R.layout.fragment_channel, container, false);
}
@Override // called from onViewCreated in BaseFragment.onViewCreated
protected void initViews(final View rootView, final Bundle savedInstanceState) {
super.initViews(rootView, savedInstanceState);
@Override
public void onViewCreated(@NonNull final View rootView, final Bundle savedInstanceState) {
super.onViewCreated(rootView, savedInstanceState);
channelBinding = FragmentChannelBinding.bind(rootView);
showContentNotSupportedIfNeeded();
}
tabAdapter = new TabAdapter(getChildFragmentManager());
binding.viewPager.setAdapter(tabAdapter);
binding.tabLayout.setupWithViewPager(binding.viewPager);
setTitle(name);
binding.channelTitleView.setText(name);
if (!ImageStrategy.shouldLoadImages()) {
// do not waste space for the banner if it is not going to be loaded
binding.channelBannerImage.setImageDrawable(null);
@Override
public void onDestroy() {
super.onDestroy();
disposables.clear();
if (subscribeButtonMonitor != null) {
subscribeButtonMonitor.dispose();
}
channelBinding = null;
headerBinding = null;
playlistControlBinding = null;
}
/*//////////////////////////////////////////////////////////////////////////
// Init
//////////////////////////////////////////////////////////////////////////*/
@Override
protected Supplier<View> getListHeaderSupplier() {
headerBinding = ChannelHeaderBinding
.inflate(activity.getLayoutInflater(), itemsList, false);
playlistControlBinding = headerBinding.playlistControl;
return headerBinding::getRoot;
}
@Override
protected void initListeners() {
super.initListeners();
final View.OnClickListener openSubChannel = v -> {
if (!TextUtils.isEmpty(currentInfo.getParentChannelUrl())) {
try {
NavigationHelper.openChannelFragment(getFM(), currentInfo.getServiceId(),
currentInfo.getParentChannelUrl(),
currentInfo.getParentChannelName());
} catch (final Exception e) {
ErrorUtil.showUiErrorSnackbar(this, "Opening channel fragment", e);
}
} else if (DEBUG) {
Log.i(TAG, "Can't open parent channel because we got no channel URL");
}
};
binding.subChannelAvatarView.setOnClickListener(openSubChannel);
binding.subChannelTitleView.setOnClickListener(openSubChannel);
headerBinding.subChannelTitleView.setOnClickListener(this);
headerBinding.subChannelAvatarView.setOnClickListener(this);
}
@Override
public void onDestroy() {
super.onDestroy();
if (currentWorker != null) {
currentWorker.dispose();
}
disposables.clear();
binding = null;
}
/*//////////////////////////////////////////////////////////////////////////
// Menu
//////////////////////////////////////////////////////////////////////////*/
@@ -194,33 +176,32 @@ public class ChannelFragment extends BaseStateFragment<ChannelInfo>
public void onCreateOptionsMenu(@NonNull final Menu menu,
@NonNull final MenuInflater inflater) {
super.onCreateOptionsMenu(menu, inflater);
inflater.inflate(R.menu.menu_channel, menu);
final ActionBar supportActionBar = activity.getSupportActionBar();
if (useAsFrontPage && supportActionBar != null) {
supportActionBar.setDisplayHomeAsUpEnabled(false);
} else {
inflater.inflate(R.menu.menu_channel, menu);
if (DEBUG) {
Log.d(TAG, "onCreateOptionsMenu() called with: "
+ "menu = [" + menu + "], inflater = [" + inflater + "]");
if (DEBUG) {
Log.d(TAG, "onCreateOptionsMenu() called with: "
+ "menu = [" + menu + "], inflater = [" + inflater + "]");
}
menuRssButton = menu.findItem(R.id.menu_item_rss);
menuNotifyButton = menu.findItem(R.id.menu_item_notify);
}
}
@Override
public void onPrepareOptionsMenu(@NonNull final Menu menu) {
super.onPrepareOptionsMenu(menu);
menuRssButton = menu.findItem(R.id.menu_item_rss);
menuNotifyButton = menu.findItem(R.id.menu_item_notify);
updateNotifyButton(channelSubscription);
}
@Override
public boolean onOptionsItemSelected(@NonNull final MenuItem item) {
public boolean onOptionsItemSelected(final MenuItem item) {
switch (item.getItemId()) {
case R.id.action_settings:
NavigationHelper.openSettings(requireContext());
break;
case R.id.menu_item_notify:
final boolean value = !item.isChecked();
item.setEnabled(false);
setNotify(value);
break;
case R.id.action_settings:
NavigationHelper.openSettings(requireContext());
break;
case R.id.menu_item_rss:
if (currentInfo != null) {
ShareUtils.openUrlInApp(requireContext(), currentInfo.getFeedUrl());
@@ -234,7 +215,7 @@ public class ChannelFragment extends BaseStateFragment<ChannelInfo>
case R.id.menu_item_share:
if (currentInfo != null) {
ShareUtils.shareText(requireContext(), name, currentInfo.getOriginalUrl(),
currentInfo.getAvatars());
currentInfo.getAvatarUrl());
}
break;
default:
@@ -243,14 +224,13 @@ public class ChannelFragment extends BaseStateFragment<ChannelInfo>
return true;
}
/*//////////////////////////////////////////////////////////////////////////
// Channel Subscription
//////////////////////////////////////////////////////////////////////////*/
private void monitorSubscription(final ChannelInfo info) {
final Consumer<Throwable> onError = (Throwable throwable) -> {
animate(binding.channelSubscribeButton, false, 100);
animate(headerBinding.channelSubscribeButton, false, 100);
showSnackBarError(new ErrorInfo(throwable, UserAction.SUBSCRIPTION_GET,
"Get subscription status", currentInfo));
};
@@ -283,9 +263,10 @@ public class ChannelFragment extends BaseStateFragment<ChannelInfo>
}, onError));
}
private Function<Object, Object> mapOnSubscribe(final SubscriptionEntity subscription) {
private Function<Object, Object> mapOnSubscribe(final SubscriptionEntity subscription,
final ChannelInfo info) {
return (@NonNull Object o) -> {
subscriptionManager.insertSubscription(subscription);
subscriptionManager.insertSubscription(subscription, info);
return o;
};
}
@@ -317,7 +298,8 @@ public class ChannelFragment extends BaseStateFragment<ChannelInfo>
.subscribe(onComplete, onError));
}
private Disposable monitorSubscribeButton(final Function<Object, Object> action) {
private Disposable monitorSubscribeButton(final Button subscribeButton,
final Function<Object, Object> action) {
final Consumer<Object> onNext = (@NonNull Object o) -> {
if (DEBUG) {
Log.d(TAG, "Changed subscription status to this channel!");
@@ -329,7 +311,7 @@ public class ChannelFragment extends BaseStateFragment<ChannelInfo>
"Changing subscription for " + currentInfo.getUrl(), currentInfo));
/* Emit clicks from main thread unto io thread */
return RxView.clicks(binding.channelSubscribeButton)
return RxView.clicks(subscribeButton)
.subscribeOn(AndroidSchedulers.mainThread())
.observeOn(Schedulers.io())
.debounce(BUTTON_DEBOUNCE_INTERVAL, TimeUnit.MILLISECONDS) // Ignore rapid clicks
@@ -355,20 +337,20 @@ public class ChannelFragment extends BaseStateFragment<ChannelInfo>
channel.setServiceId(info.getServiceId());
channel.setUrl(info.getUrl());
channel.setData(info.getName(),
ImageStrategy.imageListToDbUrl(info.getAvatars()),
info.getAvatarUrl(),
info.getDescription(),
info.getSubscriberCount());
channelSubscription = null;
updateNotifyButton(null);
subscribeButtonMonitor = monitorSubscribeButton(mapOnSubscribe(channel));
subscribeButtonMonitor = monitorSubscribeButton(
headerBinding.channelSubscribeButton, mapOnSubscribe(channel, info));
} else {
if (DEBUG) {
Log.d(TAG, "Found subscription to this channel!");
}
channelSubscription = subscriptionEntities.get(0);
updateNotifyButton(channelSubscription);
subscribeButtonMonitor =
monitorSubscribeButton(mapOnUnsubscribe(channelSubscription));
final SubscriptionEntity subscription = subscriptionEntities.get(0);
updateNotifyButton(subscription);
subscribeButtonMonitor = monitorSubscribeButton(
headerBinding.channelSubscribeButton, mapOnUnsubscribe(subscription));
}
};
}
@@ -379,33 +361,34 @@ public class ChannelFragment extends BaseStateFragment<ChannelInfo>
+ "isSubscribed = [" + isSubscribed + "]");
}
final boolean isButtonVisible = binding.channelSubscribeButton.getVisibility()
final boolean isButtonVisible = headerBinding.channelSubscribeButton.getVisibility()
== View.VISIBLE;
final int backgroundDuration = isButtonVisible ? 300 : 0;
final int textDuration = isButtonVisible ? 200 : 0;
final int subscribeBackground = ThemeHelper
.resolveColorFromAttr(activity, R.attr.colorPrimary);
final int subscribeText = ContextCompat.getColor(activity, R.color.subscribe_text_color);
final int subscribedBackground = ContextCompat
.getColor(activity, R.color.subscribed_background_color);
final int subscribedText = ContextCompat.getColor(activity, R.color.subscribed_text_color);
final int subscribeBackground = ColorUtils.blendARGB(ThemeHelper
.resolveColorFromAttr(activity, R.attr.colorPrimary), subscribedBackground, 0.35f);
final int subscribeText = ContextCompat.getColor(activity, R.color.subscribe_text_color);
if (isSubscribed) {
binding.channelSubscribeButton.setText(R.string.subscribed_button_title);
animateBackgroundColor(binding.channelSubscribeButton, backgroundDuration,
subscribeBackground, subscribedBackground);
animateTextColor(binding.channelSubscribeButton, textDuration, subscribeText,
subscribedText);
} else {
binding.channelSubscribeButton.setText(R.string.subscribe_button_title);
animateBackgroundColor(binding.channelSubscribeButton, backgroundDuration,
if (!isSubscribed) {
headerBinding.channelSubscribeButton.setText(R.string.subscribe_button_title);
animateBackgroundColor(headerBinding.channelSubscribeButton, backgroundDuration,
subscribedBackground, subscribeBackground);
animateTextColor(binding.channelSubscribeButton, textDuration, subscribedText,
animateTextColor(headerBinding.channelSubscribeButton, textDuration, subscribedText,
subscribeText);
} else {
headerBinding.channelSubscribeButton.setText(R.string.subscribed_button_title);
animateBackgroundColor(headerBinding.channelSubscribeButton, backgroundDuration,
subscribeBackground, subscribedBackground);
animateTextColor(headerBinding.channelSubscribeButton, textDuration, subscribeText,
subscribedText);
}
animate(binding.channelSubscribeButton, true, 100, AnimationType.LIGHT_SCALE_AND_ALPHA);
animate(headerBinding.channelSubscribeButton, true, 100,
AnimationType.LIGHT_SCALE_AND_ALPHA);
}
private void updateNotifyButton(@Nullable final SubscriptionEntity subscription) {
@@ -441,179 +424,108 @@ public class ChannelFragment extends BaseStateFragment<ChannelInfo>
* Show a snackbar with the option to enable notifications on new streams for this channel.
*/
private void showNotifySnackbar() {
Snackbar.make(binding.getRoot(), R.string.you_successfully_subscribed, Snackbar.LENGTH_LONG)
Snackbar.make(itemsList, R.string.you_successfully_subscribed, Snackbar.LENGTH_LONG)
.setAction(R.string.get_notified, v -> setNotify(true))
.setActionTextColor(Color.YELLOW)
.show();
}
/*//////////////////////////////////////////////////////////////////////////
// Init
// Load and handle
//////////////////////////////////////////////////////////////////////////*/
private void updateTabs() {
tabAdapter.clearAllItems();
@Override
protected Single<ListExtractor.InfoItemsPage<StreamInfoItem>> loadMoreItemsLogic() {
return ExtractorHelper.getMoreChannelItems(serviceId, url, currentNextPage);
}
if (currentInfo != null && !channelContentNotSupported) {
final Context context = requireContext();
final SharedPreferences preferences = PreferenceManager
.getDefaultSharedPreferences(context);
@Override
protected Single<ChannelInfo> loadResult(final boolean forceLoad) {
return ExtractorHelper.getChannelInfo(serviceId, url, forceLoad);
}
for (final ListLinkHandler linkHandler : currentInfo.getTabs()) {
final String tab = linkHandler.getContentFilters().get(0);
if (ChannelTabHelper.showChannelTab(context, preferences, tab)) {
final ChannelTabFragment channelTabFragment =
ChannelTabFragment.getInstance(serviceId, linkHandler, name);
channelTabFragment.useAsFrontPage(useAsFrontPage);
tabAdapter.addFragment(channelTabFragment,
context.getString(ChannelTabHelper.getTranslationKey(tab)));
/*//////////////////////////////////////////////////////////////////////////
// OnClick
//////////////////////////////////////////////////////////////////////////*/
@Override
public void onClick(final View v) {
if (isLoading.get() || currentInfo == null) {
return;
}
switch (v.getId()) {
case R.id.sub_channel_avatar_view:
case R.id.sub_channel_title_view:
if (!TextUtils.isEmpty(currentInfo.getParentChannelUrl())) {
try {
NavigationHelper.openChannelFragment(getFM(), currentInfo.getServiceId(),
currentInfo.getParentChannelUrl(),
currentInfo.getParentChannelName());
} catch (final Exception e) {
ErrorUtil.showUiErrorSnackbar(this, "Opening channel fragment", e);
}
} else if (DEBUG) {
Log.i(TAG, "Can't open parent channel because we got no channel URL");
}
}
if (ChannelTabHelper.showChannelTab(
context, preferences, R.string.show_channel_tabs_about)) {
tabAdapter.addFragment(
ChannelAboutFragment.getInstance(currentInfo),
context.getString(R.string.channel_tab_about));
}
}
tabAdapter.notifyDataSetUpdate();
for (int i = 0; i < tabAdapter.getCount(); i++) {
binding.tabLayout.getTabAt(i).setText(tabAdapter.getItemTitle(i));
}
// Restore previously selected tab
final TabLayout.Tab ltab = binding.tabLayout.getTabAt(lastTab);
if (ltab != null) {
binding.tabLayout.selectTab(ltab);
break;
}
}
/*//////////////////////////////////////////////////////////////////////////
// State Saving
//////////////////////////////////////////////////////////////////////////*/
@Override
public String generateSuffix() {
return null;
}
@Override
public void writeTo(final Queue<Object> objectsToSave) {
objectsToSave.add(currentInfo);
objectsToSave.add(binding == null ? 0 : binding.tabLayout.getSelectedTabPosition());
}
@Override
public void readFrom(@NonNull final Queue<Object> savedObjects) {
currentInfo = (ChannelInfo) savedObjects.poll();
lastTab = (Integer) savedObjects.poll();
}
@Override
public void onSaveInstanceState(final @NonNull Bundle outState) {
super.onSaveInstanceState(outState);
if (binding != null) {
outState.putInt("LastTab", binding.tabLayout.getSelectedTabPosition());
}
}
@Override
protected void onRestoreInstanceState(@NonNull final Bundle savedInstanceState) {
super.onRestoreInstanceState(savedInstanceState);
lastTab = savedInstanceState.getInt("LastTab", 0);
}
/*//////////////////////////////////////////////////////////////////////////
// Contract
//////////////////////////////////////////////////////////////////////////*/
@Override
protected void doInitialLoadLogic() {
if (currentInfo == null) {
startLoading(false);
} else {
handleResult(currentInfo);
}
}
@Override
public void startLoading(final boolean forceLoad) {
super.startLoading(forceLoad);
currentInfo = null;
updateTabs();
if (currentWorker != null) {
currentWorker.dispose();
}
runWorker(forceLoad);
}
private void runWorker(final boolean forceLoad) {
currentWorker = ExtractorHelper.getChannelInfo(serviceId, url, forceLoad)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(result -> {
isLoading.set(false);
handleResult(result);
}, throwable -> showError(new ErrorInfo(throwable, UserAction.REQUESTED_CHANNEL,
url == null ? "No URL" : url, serviceId)));
}
@Override
public void showLoading() {
super.showLoading();
PicassoHelper.cancelTag(PICASSO_CHANNEL_TAG);
animate(binding.channelSubscribeButton, false, 100);
animate(headerBinding.channelSubscribeButton, false, 100);
}
@Override
public void handleResult(@NonNull final ChannelInfo result) {
super.handleResult(result);
currentInfo = result;
setInitialData(result.getServiceId(), result.getOriginalUrl(), result.getName());
if (ImageStrategy.shouldLoadImages() && !result.getBanners().isEmpty()) {
PicassoHelper.loadBanner(result.getBanners()).tag(PICASSO_CHANNEL_TAG)
.into(binding.channelBannerImage);
} else {
// do not waste space for the banner, if the user disabled images or there is not one
binding.channelBannerImage.setImageDrawable(null);
}
headerBinding.getRoot().setVisibility(View.VISIBLE);
PicassoHelper.loadBanner(result.getBannerUrl()).tag(PICASSO_CHANNEL_TAG)
.into(headerBinding.channelBannerImage);
PicassoHelper.loadAvatar(result.getAvatarUrl()).tag(PICASSO_CHANNEL_TAG)
.into(headerBinding.channelAvatarView);
PicassoHelper.loadAvatar(result.getParentChannelAvatarUrl()).tag(PICASSO_CHANNEL_TAG)
.into(headerBinding.subChannelAvatarView);
PicassoHelper.loadAvatar(result.getAvatars()).tag(PICASSO_CHANNEL_TAG)
.into(binding.channelAvatarView);
PicassoHelper.loadAvatar(result.getParentChannelAvatars()).tag(PICASSO_CHANNEL_TAG)
.into(binding.subChannelAvatarView);
binding.channelTitleView.setText(result.getName());
binding.channelSubscriberView.setVisibility(View.VISIBLE);
headerBinding.channelSubscriberView.setVisibility(View.VISIBLE);
if (result.getSubscriberCount() >= 0) {
binding.channelSubscriberView.setText(Localization
headerBinding.channelSubscriberView.setText(Localization
.shortSubscriberCount(activity, result.getSubscriberCount()));
} else {
binding.channelSubscriberView.setText(R.string.subscribers_count_not_available);
headerBinding.channelSubscriberView.setText(R.string.subscribers_count_not_available);
}
if (!TextUtils.isEmpty(currentInfo.getParentChannelName())) {
binding.subChannelTitleView.setText(String.format(
headerBinding.subChannelTitleView.setText(String.format(
getString(R.string.channel_created_by),
currentInfo.getParentChannelName())
);
binding.subChannelTitleView.setVisibility(View.VISIBLE);
binding.subChannelAvatarView.setVisibility(View.VISIBLE);
headerBinding.subChannelTitleView.setVisibility(View.VISIBLE);
headerBinding.subChannelAvatarView.setVisibility(View.VISIBLE);
} else {
headerBinding.subChannelTitleView.setVisibility(View.GONE);
}
if (menuRssButton != null) {
menuRssButton.setVisible(!TextUtils.isEmpty(result.getFeedUrl()));
}
// PlaylistControls should be visible only if there is some item in
// infoListAdapter other than header
if (infoListAdapter.getItemCount() != 1) {
playlistControlBinding.getRoot().setVisibility(View.VISIBLE);
} else {
playlistControlBinding.getRoot().setVisibility(View.GONE);
}
channelContentNotSupported = false;
for (final Throwable throwable : result.getErrors()) {
if (throwable instanceof ContentNotSupportedException) {
@@ -627,21 +539,62 @@ public class ChannelFragment extends BaseStateFragment<ChannelInfo>
if (subscribeButtonMonitor != null) {
subscribeButtonMonitor.dispose();
}
updateTabs();
updateSubscription(result);
monitorSubscription(result);
playlistControlBinding.playlistCtrlPlayAllButton
.setOnClickListener(view -> NavigationHelper
.playOnMainPlayer(activity, getPlayQueue()));
playlistControlBinding.playlistCtrlPlayPopupButton
.setOnClickListener(view -> NavigationHelper
.playOnPopupPlayer(activity, getPlayQueue(), false));
playlistControlBinding.playlistCtrlPlayBgButton
.setOnClickListener(view -> NavigationHelper
.playOnBackgroundPlayer(activity, getPlayQueue(), false));
playlistControlBinding.playlistCtrlPlayPopupButton.setOnLongClickListener(view -> {
NavigationHelper.enqueueOnPlayer(activity, getPlayQueue(), PlayerType.POPUP);
return true;
});
playlistControlBinding.playlistCtrlPlayBgButton.setOnLongClickListener(view -> {
NavigationHelper.enqueueOnPlayer(activity, getPlayQueue(), PlayerType.AUDIO);
return true;
});
}
private void showContentNotSupportedIfNeeded() {
// channelBinding might not be initialized when handleResult() is called
// (e.g. after rotating the screen, #6696)
if (!channelContentNotSupported || binding == null) {
if (!channelContentNotSupported || channelBinding == null) {
return;
}
binding.errorContentNotSupported.setVisibility(View.VISIBLE);
binding.channelKaomoji.setText("(︶︹︺)");
binding.channelKaomoji.setTextSize(TypedValue.COMPLEX_UNIT_SP, 45f);
channelBinding.errorContentNotSupported.setVisibility(View.VISIBLE);
channelBinding.channelKaomoji.setText("(︶︹︺)");
channelBinding.channelKaomoji.setTextSize(TypedValue.COMPLEX_UNIT_SP, 45f);
channelBinding.channelNoVideos.setVisibility(View.GONE);
}
private PlayQueue getPlayQueue() {
final List<StreamInfoItem> streamItems = infoListAdapter.getItemsList().stream()
.filter(StreamInfoItem.class::isInstance)
.map(StreamInfoItem.class::cast)
.collect(Collectors.toList());
return new ChannelPlayQueue(currentInfo.getServiceId(), currentInfo.getUrl(),
currentInfo.getNextPage(), streamItems, 0);
}
/*//////////////////////////////////////////////////////////////////////////
// Utils
//////////////////////////////////////////////////////////////////////////*/
@Override
public void setTitle(final String title) {
super.setTitle(title);
if (!useAsFrontPage) {
headerBinding.channelTitleView.setText(title);
}
}
}

View File

@@ -1,168 +0,0 @@
package org.schabi.newpipe.fragments.list.channel;
import android.os.Bundle;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import org.schabi.newpipe.R;
import org.schabi.newpipe.databinding.PlaylistControlBinding;
import org.schabi.newpipe.error.UserAction;
import org.schabi.newpipe.extractor.InfoItem;
import org.schabi.newpipe.extractor.ListExtractor;
import org.schabi.newpipe.extractor.channel.tabs.ChannelTabInfo;
import org.schabi.newpipe.extractor.exceptions.ParsingException;
import org.schabi.newpipe.extractor.linkhandler.ListLinkHandler;
import org.schabi.newpipe.extractor.linkhandler.ListLinkHandlerFactory;
import org.schabi.newpipe.extractor.linkhandler.ReadyChannelTabListLinkHandler;
import org.schabi.newpipe.extractor.stream.StreamInfoItem;
import org.schabi.newpipe.fragments.list.BaseListInfoFragment;
import org.schabi.newpipe.fragments.list.playlist.PlaylistControlViewHolder;
import org.schabi.newpipe.player.playqueue.ChannelTabPlayQueue;
import org.schabi.newpipe.player.playqueue.PlayQueue;
import org.schabi.newpipe.util.ChannelTabHelper;
import org.schabi.newpipe.util.ExtractorHelper;
import org.schabi.newpipe.util.PlayButtonHelper;
import java.util.List;
import java.util.function.Supplier;
import java.util.stream.Collectors;
import icepick.State;
import io.reactivex.rxjava3.core.Single;
public class ChannelTabFragment extends BaseListInfoFragment<InfoItem, ChannelTabInfo>
implements PlaylistControlViewHolder {
// states must be protected and not private for IcePick being able to access them
@State
protected ListLinkHandler tabHandler;
@State
protected String channelName;
private PlaylistControlBinding playlistControlBinding;
@NonNull
public static ChannelTabFragment getInstance(final int serviceId,
final ListLinkHandler tabHandler,
final String channelName) {
final ChannelTabFragment instance = new ChannelTabFragment();
instance.serviceId = serviceId;
instance.tabHandler = tabHandler;
instance.channelName = channelName;
return instance;
}
public ChannelTabFragment() {
super(UserAction.REQUESTED_CHANNEL);
}
/*//////////////////////////////////////////////////////////////////////////
// LifeCycle
//////////////////////////////////////////////////////////////////////////*/
@Override
public void onCreate(final Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setHasOptionsMenu(false);
}
@Override
public View onCreateView(@NonNull final LayoutInflater inflater,
@Nullable final ViewGroup container,
@Nullable final Bundle savedInstanceState) {
return inflater.inflate(R.layout.fragment_channel_tab, container, false);
}
@Override
public void onDestroyView() {
super.onDestroyView();
playlistControlBinding = null;
}
@Override
protected Supplier<View> getListHeaderSupplier() {
if (ChannelTabHelper.isStreamsTab(tabHandler)) {
playlistControlBinding = PlaylistControlBinding
.inflate(activity.getLayoutInflater(), itemsList, false);
return playlistControlBinding::getRoot;
}
return null;
}
@Override
protected Single<ChannelTabInfo> loadResult(final boolean forceLoad) {
return ExtractorHelper.getChannelTab(serviceId, tabHandler, forceLoad);
}
@Override
protected Single<ListExtractor.InfoItemsPage<InfoItem>> loadMoreItemsLogic() {
return ExtractorHelper.getMoreChannelTabItems(serviceId, tabHandler, currentNextPage);
}
@Override
public void setTitle(final String title) {
// The channel name is displayed as title in the toolbar.
// The title is always a description of the content of the tab fragment.
// It should be unique for each channel because multiple channel tabs
// can be added to the main page. Therefore, the channel name is used.
// Using the title variable would cause the title to be the same for all channel tabs.
super.setTitle(channelName);
}
@Override
public void handleResult(@NonNull final ChannelTabInfo result) {
super.handleResult(result);
// FIXME this is a really hacky workaround, to avoid storing useless data in the fragment
// state. The problem is, `ReadyChannelTabListLinkHandler` might contain raw JSON data that
// uses a lot of memory (e.g. ~800KB for YouTube). While 800KB doesn't seem much, if
// you combine just a couple of channel tab fragments you easily go over the 1MB
// save&restore transaction limit, and get `TransactionTooLargeException`s. A proper
// solution would require rethinking about `ReadyChannelTabListLinkHandler`s.
if (tabHandler instanceof ReadyChannelTabListLinkHandler) {
try {
// once `handleResult` is called, the parsed data was already saved to cache, so
// we can discard any raw data in ReadyChannelTabListLinkHandler and create a
// link handler with identical properties, but without any raw data
final ListLinkHandlerFactory channelTabLHFactory = result.getService()
.getChannelTabLHFactory();
if (channelTabLHFactory != null) {
// some services do not not have a ChannelTabLHFactory
tabHandler = channelTabLHFactory.fromQuery(tabHandler.getId(),
tabHandler.getContentFilters(), tabHandler.getSortFilter());
}
} catch (final ParsingException e) {
// silently ignore the error, as the app can continue to function normally
Log.w(TAG, "Could not recreate channel tab handler", e);
}
}
if (playlistControlBinding != null) {
// PlaylistControls should be visible only if there is some item in
// infoListAdapter other than header
if (infoListAdapter.getItemCount() > 1) {
playlistControlBinding.getRoot().setVisibility(View.VISIBLE);
} else {
playlistControlBinding.getRoot().setVisibility(View.GONE);
}
PlayButtonHelper.initPlaylistControlClickListener(
activity, playlistControlBinding, this);
}
}
public PlayQueue getPlayQueue() {
final List<StreamInfoItem> streamItems = infoListAdapter.getItemsList().stream()
.filter(StreamInfoItem.class::isInstance)
.map(StreamInfoItem.class::cast)
.collect(Collectors.toList());
return new ChannelTabPlayQueue(currentInfo.getServiceId(), tabHandler,
currentInfo.getNextPage(), streamItems, 0);
}
}

View File

@@ -16,13 +16,11 @@ import org.schabi.newpipe.error.ErrorInfo;
import org.schabi.newpipe.error.UserAction;
import org.schabi.newpipe.extractor.ListExtractor;
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.kiosk.KioskInfo;
import org.schabi.newpipe.extractor.linkhandler.ListLinkHandlerFactory;
import org.schabi.newpipe.extractor.localization.ContentCountry;
import org.schabi.newpipe.extractor.services.media_ccc.extractors.MediaCCCLiveStreamKiosk;
import org.schabi.newpipe.extractor.stream.StreamInfoItem;
import org.schabi.newpipe.fragments.list.BaseListInfoFragment;
import org.schabi.newpipe.util.ExtractorHelper;
@@ -163,14 +161,4 @@ public class KioskFragment extends BaseListInfoFragment<StreamInfoItem, KioskInf
name = kioskTranslatedName;
setTitle(kioskTranslatedName);
}
@Override
public void showEmptyState() {
// show "no live streams" for live stream kiosk
super.showEmptyState();
if (MediaCCCLiveStreamKiosk.KIOSK_ID.equals(currentInfo.getId())
&& ServiceList.MediaCCC.getServiceId() == currentInfo.getServiceId()) {
setEmptyStateMessage(R.string.no_live_streams);
}
}
}

View File

@@ -1,11 +0,0 @@
package org.schabi.newpipe.fragments.list.playlist;
import org.schabi.newpipe.player.playqueue.PlayQueue;
/**
* Interface for {@code R.layout.playlist_control} view holders
* to give access to the play queue.
*/
public interface PlaylistControlViewHolder {
PlayQueue getPlayQueue();
}

View File

@@ -43,14 +43,14 @@ import org.schabi.newpipe.info_list.dialog.InfoItemDialog;
import org.schabi.newpipe.info_list.dialog.StreamDialogDefaultEntry;
import org.schabi.newpipe.local.dialog.PlaylistDialog;
import org.schabi.newpipe.local.playlist.RemotePlaylistManager;
import org.schabi.newpipe.player.PlayerType;
import org.schabi.newpipe.player.playqueue.PlayQueue;
import org.schabi.newpipe.player.playqueue.PlaylistPlayQueue;
import org.schabi.newpipe.util.ExtractorHelper;
import org.schabi.newpipe.util.Localization;
import org.schabi.newpipe.util.NavigationHelper;
import org.schabi.newpipe.util.image.PicassoHelper;
import org.schabi.newpipe.util.PicassoHelper;
import org.schabi.newpipe.util.external_communication.ShareUtils;
import org.schabi.newpipe.util.PlayButtonHelper;
import java.util.ArrayList;
import java.util.List;
@@ -64,8 +64,7 @@ import io.reactivex.rxjava3.core.Single;
import io.reactivex.rxjava3.disposables.CompositeDisposable;
import io.reactivex.rxjava3.disposables.Disposable;
public class PlaylistFragment extends BaseListInfoFragment<StreamInfoItem, PlaylistInfo>
implements PlaylistControlViewHolder {
public class PlaylistFragment extends BaseListInfoFragment<StreamInfoItem, PlaylistInfo> {
private static final String PICASSO_PLAYLIST_TAG = "PICASSO_PLAYLIST_TAG";
@@ -234,7 +233,7 @@ public class PlaylistFragment extends BaseListInfoFragment<StreamInfoItem, Playl
break;
case R.id.menu_item_share:
ShareUtils.shareText(requireContext(), name, url,
currentInfo == null ? List.of() : currentInfo.getThumbnails());
currentInfo == null ? null : currentInfo.getThumbnailUrl());
break;
case R.id.menu_item_bookmark:
onBookmarkClicked();
@@ -299,6 +298,7 @@ public class PlaylistFragment extends BaseListInfoFragment<StreamInfoItem, Playl
playlistControlBinding.getRoot().setVisibility(View.VISIBLE);
final String avatarUrl = result.getUploaderAvatarUrl();
if (result.getServiceId() == ServiceList.YouTube.getServiceId()
&& (YoutubeParsingHelper.isYoutubeMixId(result.getId())
|| YoutubeParsingHelper.isYoutubeMusicMixId(result.getId()))) {
@@ -314,7 +314,7 @@ public class PlaylistFragment extends BaseListInfoFragment<StreamInfoItem, Playl
R.drawable.ic_radio)
);
} else {
PicassoHelper.loadAvatar(result.getUploaderAvatars()).tag(PICASSO_PLAYLIST_TAG)
PicassoHelper.loadAvatar(avatarUrl).tag(PICASSO_PLAYLIST_TAG)
.into(headerBinding.uploaderAvatarView);
}
@@ -332,10 +332,25 @@ public class PlaylistFragment extends BaseListInfoFragment<StreamInfoItem, Playl
.observeOn(AndroidSchedulers.mainThread())
.subscribe(getPlaylistBookmarkSubscriber());
PlayButtonHelper.initPlaylistControlClickListener(activity, playlistControlBinding, this);
playlistControlBinding.playlistCtrlPlayAllButton.setOnClickListener(view ->
NavigationHelper.playOnMainPlayer(activity, getPlayQueue()));
playlistControlBinding.playlistCtrlPlayPopupButton.setOnClickListener(view ->
NavigationHelper.playOnPopupPlayer(activity, getPlayQueue(), false));
playlistControlBinding.playlistCtrlPlayBgButton.setOnClickListener(view ->
NavigationHelper.playOnBackgroundPlayer(activity, getPlayQueue(), false));
playlistControlBinding.playlistCtrlPlayPopupButton.setOnLongClickListener(view -> {
NavigationHelper.enqueueOnPlayer(activity, getPlayQueue(), PlayerType.POPUP);
return true;
});
playlistControlBinding.playlistCtrlPlayBgButton.setOnLongClickListener(view -> {
NavigationHelper.enqueueOnPlayer(activity, getPlayQueue(), PlayerType.AUDIO);
return true;
});
}
public PlayQueue getPlayQueue() {
private PlayQueue getPlayQueue() {
return getPlayQueue(0);
}

View File

@@ -167,10 +167,6 @@ public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.I
/*////////////////////////////////////////////////////////////////////////*/
/**
* TextWatcher to remove rich-text formatting on the search EditText when pasting content
* from the clipboard.
*/
private TextWatcher textWatcher;
public static SearchFragment getInstance(final int serviceId, final String searchString) {
@@ -587,13 +583,11 @@ public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.I
@Override
public void beforeTextChanged(final CharSequence s, final int start,
final int count, final int after) {
// Do nothing, old text is already clean
}
@Override
public void onTextChanged(final CharSequence s, final int start,
final int before, final int count) {
// Changes are handled in afterTextChanged; CharSequence cannot be changed here.
}
@Override

View File

@@ -8,7 +8,7 @@ import com.xwray.groupie.Item
import org.schabi.newpipe.R
import org.schabi.newpipe.extractor.stream.StreamSegment
import org.schabi.newpipe.util.Localization
import org.schabi.newpipe.util.image.PicassoHelper
import org.schabi.newpipe.util.PicassoHelper
class StreamSegmentItem(
private val item: StreamSegment,

View File

@@ -104,7 +104,7 @@ public enum StreamDialogDefaultEntry {
SHARE(R.string.share, (fragment, item) ->
ShareUtils.shareText(fragment.requireContext(), item.getName(), item.getUrl(),
item.getThumbnails())),
item.getThumbnailUrl())),
/**
* Opens a {@link DownloadDialog} after fetching some stream info.

View File

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

View File

@@ -31,8 +31,7 @@ import org.schabi.newpipe.local.history.HistoryRecordManager;
import org.schabi.newpipe.util.DeviceUtils;
import org.schabi.newpipe.util.Localization;
import org.schabi.newpipe.util.NavigationHelper;
import org.schabi.newpipe.util.image.ImageStrategy;
import org.schabi.newpipe.util.image.PicassoHelper;
import org.schabi.newpipe.util.PicassoHelper;
import org.schabi.newpipe.util.external_communication.ShareUtils;
import org.schabi.newpipe.util.text.CommentTextOnTouchListener;
import org.schabi.newpipe.util.text.TextLinkifier;
@@ -98,8 +97,8 @@ public class CommentsMiniInfoItemHolder extends InfoItemHolder {
}
final CommentsInfoItem item = (CommentsInfoItem) infoItem;
PicassoHelper.loadAvatar(item.getUploaderAvatars()).into(itemThumbnailView);
if (ImageStrategy.shouldLoadImages()) {
PicassoHelper.loadAvatar(item.getUploaderAvatarUrl()).into(itemThumbnailView);
if (PicassoHelper.getShouldLoadImages()) {
itemThumbnailView.setVisibility(View.VISIBLE);
itemRoot.setPadding(commentVerticalPadding, commentVerticalPadding,
commentVerticalPadding, commentVerticalPadding);

View File

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

View File

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

View File

@@ -91,6 +91,11 @@ public final class BookmarkFragment extends BaseLocalListFragment<List<PlaylistL
// Fragment LifeCycle - Views
///////////////////////////////////////////////////////////////////////////
@Override
protected void initViews(final View rootView, final Bundle savedInstanceState) {
super.initViews(rootView, savedInstanceState);
}
@Override
protected void initListeners() {
super.initListeners();

View File

@@ -38,6 +38,7 @@ import android.view.ViewGroup
import android.widget.Button
import androidx.appcompat.app.AlertDialog
import androidx.core.content.edit
import androidx.core.math.MathUtils
import androidx.core.os.bundleOf
import androidx.core.view.isVisible
import androidx.lifecycle.ViewModelProvider
@@ -59,7 +60,6 @@ import org.schabi.newpipe.database.feed.model.FeedGroupEntity
import org.schabi.newpipe.database.subscription.SubscriptionEntity
import org.schabi.newpipe.databinding.FragmentFeedBinding
import org.schabi.newpipe.error.ErrorInfo
import org.schabi.newpipe.error.ErrorUtil
import org.schabi.newpipe.error.UserAction
import org.schabi.newpipe.extractor.exceptions.AccountTerminatedException
import org.schabi.newpipe.extractor.exceptions.ContentNotAvailableException
@@ -453,33 +453,24 @@ class FeedFragment : BaseStateFragment<FeedState>() {
if (t is FeedLoadService.RequestException &&
t.cause is ContentNotAvailableException
) {
disposables.add(
Single.fromCallable {
NewPipeDatabase.getInstance(requireContext()).subscriptionDAO()
.getSubscription(t.subscriptionId)
}
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
{ subscriptionEntity ->
handleFeedNotAvailable(
subscriptionEntity,
t.cause,
errors.subList(i + 1, errors.size)
)
},
{ throwable -> Log.e(TAG, "Unable to process", throwable) }
)
)
// this will be called on the remaining errors by handleFeedNotAvailable()
return@handleItemsErrors
Single.fromCallable {
NewPipeDatabase.getInstance(requireContext()).subscriptionDAO()
.getSubscription(t.subscriptionId)
}.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
{ subscriptionEntity ->
handleFeedNotAvailable(
subscriptionEntity,
t.cause,
errors.subList(i + 1, errors.size)
)
},
{ throwable -> Log.e(TAG, "Unable to process", throwable) }
)
return // this will be called on the remaining errors by handleFeedNotAvailable()
}
}
if (errors.isNotEmpty()) {
// if no error was a ContentNotAvailableException, show a general error snackbar
ErrorUtil.showSnackbar(this, ErrorInfo(errors, UserAction.REQUESTED_FEED, ""))
}
}
private fun handleFeedNotAvailable(
@@ -588,7 +579,7 @@ class FeedFragment : BaseStateFragment<FeedState>() {
// state until the user scrolls them out of the visible area which causes a update/bind-call
groupAdapter.notifyItemRangeChanged(
0,
highlightCount.coerceIn(lastNewItemsCount, groupAdapter.itemCount)
MathUtils.clamp(highlightCount, lastNewItemsCount, groupAdapter.itemCount)
)
if (highlightCount > 0) {
@@ -607,13 +598,9 @@ class FeedFragment : BaseStateFragment<FeedState>() {
execOnEnd = {
// Disabled animations would result in immediately hiding the button
// after it showed up
// Context can be null in some cases, so we have to make sure it is not null in
// order to avoid a NullPointerException
context?.let {
if (DeviceUtils.hasAnimationsAnimatorDurationEnabled(it)) {
// Hide the new items button after 10s
hideNewItemsLoaded(true, 10000)
}
if (DeviceUtils.hasAnimationsAnimatorDurationEnabled(context)) {
// Hide the new items-"popup" after 10s
hideNewItemsLoaded(true, 10000)
}
}
)

View File

@@ -13,9 +13,9 @@ sealed class FeedState {
data class LoadedState(
val items: List<StreamItem>,
val oldestUpdate: OffsetDateTime?,
val oldestUpdate: OffsetDateTime? = null,
val notLoadedCount: Long,
val itemsErrors: List<Throwable>
val itemsErrors: List<Throwable> = emptyList()
) : FeedState()
data class ErrorState(

View File

@@ -86,7 +86,7 @@ class FeedViewModel(
.subscribe { (event, listFromDB, notLoadedCount, oldestUpdate) ->
mutableStateLiveData.postValue(
when (event) {
is IdleEvent -> FeedState.LoadedState(listFromDB.map { e -> StreamItem(e) }, oldestUpdate, notLoadedCount, listOf())
is IdleEvent -> FeedState.LoadedState(listFromDB.map { e -> StreamItem(e) }, oldestUpdate, notLoadedCount)
is ProgressEvent -> FeedState.ProgressState(event.currentProgress, event.maxProgress, event.progressMessage)
is SuccessResultEvent -> FeedState.LoadedState(listFromDB.map { e -> StreamItem(e) }, oldestUpdate, notLoadedCount, event.itemsErrors)
is ErrorResultEvent -> FeedState.ErrorState(event.error)

View File

@@ -18,8 +18,8 @@ import org.schabi.newpipe.extractor.stream.StreamType.POST_LIVE_AUDIO_STREAM
import org.schabi.newpipe.extractor.stream.StreamType.POST_LIVE_STREAM
import org.schabi.newpipe.extractor.stream.StreamType.VIDEO_STREAM
import org.schabi.newpipe.util.Localization
import org.schabi.newpipe.util.PicassoHelper
import org.schabi.newpipe.util.StreamTypeUtil
import org.schabi.newpipe.util.image.PicassoHelper
import java.util.concurrent.TimeUnit
import java.util.function.Consumer

View File

@@ -1,8 +1,6 @@
package org.schabi.newpipe.local.feed.notifications
import android.app.Notification
import android.app.NotificationManager
import android.app.PendingIntent
import android.content.Context
import android.content.Intent
import android.graphics.Bitmap
@@ -14,41 +12,46 @@ import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat
import androidx.core.app.PendingIntentCompat
import androidx.core.content.ContextCompat
import androidx.core.content.getSystemService
import androidx.preference.PreferenceManager
import com.squareup.picasso.Picasso
import com.squareup.picasso.Target
import org.schabi.newpipe.R
import org.schabi.newpipe.extractor.stream.StreamInfoItem
import org.schabi.newpipe.local.feed.service.FeedUpdateInfo
import org.schabi.newpipe.util.Localization
import org.schabi.newpipe.util.NavigationHelper
import org.schabi.newpipe.util.image.PicassoHelper
import org.schabi.newpipe.util.PicassoHelper
/**
* Helper for everything related to show notifications about new streams to the user.
*/
class NotificationHelper(val context: Context) {
private val manager = NotificationManagerCompat.from(context)
private val manager = context.getSystemService(
Context.NOTIFICATION_SERVICE
) as NotificationManager
private val iconLoadingTargets = ArrayList<Target>()
/**
* Show notifications for new streams from a single channel. The individual notifications are
* expandable on Android 7.0 and later.
*
* Opening the summary notification will open the corresponding channel page. Opening the
* individual notifications will open the corresponding video.
* Show a notification about new streams from a single channel.
* Opening the notification will open the corresponding channel page.
*/
fun displayNewStreamsNotifications(data: FeedUpdateInfo) {
val newStreams = data.newStreams
fun displayNewStreamsNotification(data: FeedUpdateInfo) {
val newStreams: List<StreamInfoItem> = data.newStreams
val summary = context.resources.getQuantityString(
R.plurals.new_streams, newStreams.size, newStreams.size
)
val summaryBuilder = NotificationCompat.Builder(
val builder = NotificationCompat.Builder(
context,
context.getString(R.string.streams_notification_channel_id)
)
.setContentTitle(data.name)
.setContentText(summary)
.setContentTitle(Localization.concatenateStrings(data.name, summary))
.setContentText(
data.listInfo.relatedItems.joinToString(
context.getString(R.string.enumeration_comma)
) { x -> x.name }
)
.setNumber(newStreams.size)
.setBadgeIconType(NotificationCompat.BADGE_ICON_LARGE)
.setPriority(NotificationCompat.PRIORITY_DEFAULT)
@@ -57,23 +60,21 @@ class NotificationHelper(val context: Context) {
.setColorized(true)
.setAutoCancel(true)
.setCategory(NotificationCompat.CATEGORY_SOCIAL)
.setGroupSummary(true)
.setGroup(data.url)
.setGroupAlertBehavior(NotificationCompat.GROUP_ALERT_SUMMARY)
// Build a summary notification for Android versions < 7.0
// Build style
val style = NotificationCompat.InboxStyle()
.setBigContentTitle(data.name)
newStreams.forEach { style.addLine(it.name) }
summaryBuilder.setStyle(style)
style.setSummaryText(summary)
style.setBigContentTitle(data.name)
builder.setStyle(style)
// open the channel page when clicking on the summary notification
summaryBuilder.setContentIntent(
// open the channel page when clicking on the notification
builder.setContentIntent(
PendingIntentCompat.getActivity(
context,
data.pseudoId,
NavigationHelper
.getChannelIntent(context, data.serviceId, data.url)
.getChannelIntent(context, data.listInfo.serviceId, data.listInfo.url)
.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK),
0,
false
@@ -83,23 +84,13 @@ class NotificationHelper(val context: Context) {
// a Target is like a listener for image loading events
val target = object : Target {
override fun onBitmapLoaded(bitmap: Bitmap, from: Picasso.LoadedFrom) {
// set channel icon only if there is actually one (for Android versions < 7.0)
summaryBuilder.setLargeIcon(bitmap)
// Show individual stream notifications, set channel icon only if there is actually
// one
showStreamNotifications(newStreams, data.serviceId, bitmap)
// Show summary notification
manager.notify(data.pseudoId, summaryBuilder.build())
builder.setLargeIcon(bitmap) // set only if there is actually one
manager.notify(data.pseudoId, builder.build())
iconLoadingTargets.remove(this) // allow it to be garbage-collected
}
override fun onBitmapFailed(e: Exception, errorDrawable: Drawable) {
// Show individual stream notifications
showStreamNotifications(newStreams, data.serviceId, null)
// Show summary notification
manager.notify(data.pseudoId, summaryBuilder.build())
manager.notify(data.pseudoId, builder.build())
iconLoadingTargets.remove(this) // allow it to be garbage-collected
}
@@ -115,49 +106,6 @@ class NotificationHelper(val context: Context) {
PicassoHelper.loadNotificationIcon(data.avatarUrl).into(target)
}
private fun showStreamNotifications(
newStreams: List<StreamInfoItem>,
serviceId: Int,
channelIcon: Bitmap?
) {
for (stream in newStreams) {
val notification = createStreamNotification(stream, serviceId, channelIcon)
manager.notify(stream.url.hashCode(), notification)
}
}
private fun createStreamNotification(
item: StreamInfoItem,
serviceId: Int,
channelIcon: Bitmap?
): Notification {
return NotificationCompat.Builder(
context,
context.getString(R.string.streams_notification_channel_id)
)
.setSmallIcon(R.drawable.ic_newpipe_triangle_white)
.setLargeIcon(channelIcon)
.setContentTitle(item.name)
.setContentText(item.uploaderName)
.setGroup(item.uploaderUrl)
.setColor(ContextCompat.getColor(context, R.color.ic_launcher_background))
.setColorized(true)
.setAutoCancel(true)
.setCategory(NotificationCompat.CATEGORY_SOCIAL)
.setContentIntent(
// Open the stream link in the player when clicking on the notification.
PendingIntentCompat.getActivity(
context,
item.url.hashCode(),
NavigationHelper.getStreamIntent(context, serviceId, item.url, item.name),
PendingIntent.FLAG_UPDATE_CURRENT,
false
)
)
.setSilent(true) // Avoid creating noise for individual stream notifications.
.build()
}
companion object {
/**
* Check whether notifications are enabled on the device.
@@ -176,7 +124,9 @@ class NotificationHelper(val context: Context) {
fun areNotificationsEnabledOnDevice(context: Context): Boolean {
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val channelId = context.getString(R.string.streams_notification_channel_id)
val manager = context.getSystemService<NotificationManager>()!!
val manager = context.getSystemService(
Context.NOTIFICATION_SERVICE
) as NotificationManager
val enabled = manager.areNotificationsEnabled()
val channel = manager.getNotificationChannel(channelId)
val importance = channel?.importance

View File

@@ -55,7 +55,7 @@ class NotificationWorker(
.map { feedUpdateInfoList ->
// display notifications for each feedUpdateInfo (i.e. channel)
feedUpdateInfoList.forEach { feedUpdateInfo ->
notificationHelper.displayNewStreamsNotifications(feedUpdateInfo)
notificationHelper.displayNewStreamsNotification(feedUpdateInfo)
}
return@map Result.success()
}

View File

@@ -1,7 +1,6 @@
package org.schabi.newpipe.local.feed.service
import android.content.Context
import android.content.SharedPreferences
import androidx.preference.PreferenceManager
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
import io.reactivex.rxjava3.core.Completable
@@ -14,17 +13,11 @@ import io.reactivex.rxjava3.schedulers.Schedulers
import org.schabi.newpipe.R
import org.schabi.newpipe.database.feed.model.FeedGroupEntity
import org.schabi.newpipe.database.subscription.NotificationMode
import org.schabi.newpipe.database.subscription.SubscriptionEntity
import org.schabi.newpipe.extractor.Info
import org.schabi.newpipe.extractor.NewPipe
import org.schabi.newpipe.extractor.feed.FeedInfo
import org.schabi.newpipe.extractor.ListInfo
import org.schabi.newpipe.extractor.stream.StreamInfoItem
import org.schabi.newpipe.local.feed.FeedDatabaseManager
import org.schabi.newpipe.local.subscription.SubscriptionManager
import org.schabi.newpipe.util.ChannelTabHelper
import org.schabi.newpipe.util.ExtractorHelper.getChannelInfo
import org.schabi.newpipe.util.ExtractorHelper.getChannelTab
import org.schabi.newpipe.util.ExtractorHelper.getMoreChannelTabItems
import org.schabi.newpipe.util.ExtractorHelper
import java.time.OffsetDateTime
import java.time.ZoneOffset
import java.util.concurrent.atomic.AtomicBoolean
@@ -82,9 +75,7 @@ class FeedLoadManager(private val context: Context) {
* subscriptions which have not been updated within the feed updated threshold
*/
val outdatedSubscriptions = when (groupId) {
FeedGroupEntity.GROUP_ALL_ID -> feedDatabaseManager.outdatedSubscriptions(
outdatedThreshold
)
FeedGroupEntity.GROUP_ALL_ID -> feedDatabaseManager.outdatedSubscriptions(outdatedThreshold)
GROUP_NOTIFICATION_ENABLED -> feedDatabaseManager.outdatedSubscriptionsWithNotificationMode(
outdatedThreshold, NotificationMode.ENABLED
)
@@ -110,7 +101,52 @@ class FeedLoadManager(private val context: Context) {
.runOn(Schedulers.io(), PARALLEL_EXTRACTIONS * 2)
.filter { !cancelSignal.get() }
.map { subscriptionEntity ->
loadStreams(subscriptionEntity, useFeedExtractor, defaultSharedPreferences)
var error: Throwable? = null
try {
// check for and load new streams
// either by using the dedicated feed method or by getting the channel info
val listInfo = if (useFeedExtractor) {
ExtractorHelper
.getFeedInfoFallbackToChannelInfo(
subscriptionEntity.serviceId,
subscriptionEntity.url
)
.onErrorReturn {
error = it // store error, otherwise wrapped into RuntimeException
throw it
}
.blockingGet()
} else {
ExtractorHelper
.getChannelInfo(
subscriptionEntity.serviceId,
subscriptionEntity.url,
true
)
.onErrorReturn {
error = it // store error, otherwise wrapped into RuntimeException
throw it
}
.blockingGet()
} as ListInfo<StreamInfoItem>
return@map Notification.createOnNext(
FeedUpdateInfo(
subscriptionEntity,
listInfo
)
)
} catch (e: Throwable) {
if (error == null) {
// do this to prevent blockingGet() from wrapping into RuntimeException
error = e
}
val request = "${subscriptionEntity.serviceId}:${subscriptionEntity.url}"
val wrapper =
FeedLoadService.RequestException(subscriptionEntity.uid, request, error!!)
return@map Notification.createOnError<FeedUpdateInfo>(wrapper)
}
}
.sequential()
.observeOn(AndroidSchedulers.mainThread())
@@ -128,112 +164,7 @@ class FeedLoadManager(private val context: Context) {
}
private fun broadcastProgress() {
FeedEventManager.postEvent(
FeedEventManager.Event.ProgressEvent(
currentProgress.get(),
maxProgress.get()
)
)
}
private fun loadStreams(
subscriptionEntity: SubscriptionEntity,
useFeedExtractor: Boolean,
defaultSharedPreferences: SharedPreferences
): Notification<FeedUpdateInfo> {
var error: Throwable? = null
val storeOriginalErrorAndRethrow = { e: Throwable ->
// keep original to prevent blockingGet() from wrapping it into RuntimeException
error = e
throw e
}
try {
// check for and load new streams
// either by using the dedicated feed method or by getting the channel info
var originalInfo: Info? = null
var streams: List<StreamInfoItem>? = null
val errors = ArrayList<Throwable>()
if (useFeedExtractor) {
NewPipe.getService(subscriptionEntity.serviceId)
.getFeedExtractor(subscriptionEntity.url)
?.also { feedExtractor ->
// the user wants to use a feed extractor and there is one, use it
val feedInfo = FeedInfo.getInfo(feedExtractor)
errors.addAll(feedInfo.errors)
originalInfo = feedInfo
streams = feedInfo.relatedItems
}
}
if (originalInfo == null) {
// use the normal channel tabs extractor if either the user wants it, or
// the current service does not have a dedicated feed extractor
val channelInfo = getChannelInfo(
subscriptionEntity.serviceId,
subscriptionEntity.url, true
)
.onErrorReturn(storeOriginalErrorAndRethrow)
.blockingGet()
errors.addAll(channelInfo.errors)
originalInfo = channelInfo
streams = channelInfo.tabs
.filter { tab ->
ChannelTabHelper.fetchFeedChannelTab(
context,
defaultSharedPreferences,
tab
)
}
.map {
Pair(
getChannelTab(subscriptionEntity.serviceId, it, true)
.onErrorReturn(storeOriginalErrorAndRethrow)
.blockingGet(),
it
)
}
.flatMap { (channelTabInfo, linkHandler) ->
errors.addAll(channelTabInfo.errors)
if (channelTabInfo.relatedItems.isEmpty() &&
channelTabInfo.nextPage != null
) {
val infoItemsPage = getMoreChannelTabItems(
subscriptionEntity.serviceId,
linkHandler, channelTabInfo.nextPage
)
.blockingGet()
errors.addAll(infoItemsPage.errors)
return@flatMap infoItemsPage.items
} else {
return@flatMap channelTabInfo.relatedItems
}
}
.filterIsInstance<StreamInfoItem>()
}
return Notification.createOnNext(
FeedUpdateInfo(
subscriptionEntity,
originalInfo!!,
streams!!,
errors,
)
)
} catch (e: Throwable) {
val request = "${subscriptionEntity.serviceId}:${subscriptionEntity.url}"
val wrapper = FeedLoadService.RequestException(
subscriptionEntity.uid,
request,
// do this to prevent blockingGet() from wrapping into RuntimeException
error ?: e
)
return Notification.createOnError(wrapper)
}
FeedEventManager.postEvent(FeedEventManager.Event.ProgressEvent(currentProgress.get(), maxProgress.get()))
}
/**
@@ -272,24 +203,24 @@ class FeedLoadManager(private val context: Context) {
for (notification in list) {
when {
notification.isOnNext -> {
val info = notification.value!!
val subscriptionId = notification.value!!.uid
val info = notification.value!!.listInfo
notification.value!!.newStreams = filterNewStreams(info.streams)
notification.value!!.newStreams = filterNewStreams(
notification.value!!.listInfo.relatedItems
)
feedDatabaseManager.upsertAll(info.uid, info.streams)
subscriptionManager.updateFromInfo(info)
feedDatabaseManager.upsertAll(subscriptionId, info.relatedItems)
subscriptionManager.updateFromInfo(subscriptionId, info)
if (info.errors.isNotEmpty()) {
feedResultsHolder.addErrors(
info.errors.map {
FeedLoadService.RequestException(
info.uid,
"${info.serviceId}:${info.url}",
it
)
}
FeedLoadService.RequestException.wrapList(
subscriptionId,
info
)
)
feedDatabaseManager.markAsOutdated(info.uid)
feedDatabaseManager.markAsOutdated(subscriptionId)
}
}
notification.isOnError -> {

View File

@@ -39,6 +39,8 @@ import org.schabi.newpipe.App
import org.schabi.newpipe.MainActivity.DEBUG
import org.schabi.newpipe.R
import org.schabi.newpipe.database.feed.model.FeedGroupEntity
import org.schabi.newpipe.extractor.ListInfo
import org.schabi.newpipe.extractor.stream.StreamInfoItem
import org.schabi.newpipe.local.feed.service.FeedEventManager.Event.ErrorResultEvent
import org.schabi.newpipe.local.feed.service.FeedEventManager.postEvent
import java.util.concurrent.TimeUnit
@@ -93,7 +95,13 @@ class FeedLoadService : Service() {
.doOnSubscribe {
startForeground(NOTIFICATION_ID, notificationBuilder.build())
}
.subscribe { _, error: Throwable? -> // explicitly mark error as nullable
.subscribe { _, error ->
// There seems to be a bug in the kotlin plugin as it tells you when
// building that this can't be null:
// "Condition 'error != null' is always 'true'"
// However it can indeed be null
// The suppression may be removed in further versions
@Suppress("SENSELESS_COMPARISON")
if (error != null) {
Log.e(TAG, "Error while storing result", error)
handleError(error)
@@ -124,7 +132,17 @@ class FeedLoadService : Service() {
// Loading & Handling
// /////////////////////////////////////////////////////////////////////////
class RequestException(val subscriptionId: Long, message: String, cause: Throwable) : Exception(message, cause)
class RequestException(val subscriptionId: Long, message: String, cause: Throwable) : Exception(message, cause) {
companion object {
fun wrapList(subscriptionId: Long, info: ListInfo<StreamInfoItem>): List<Throwable> {
val toReturn = ArrayList<Throwable>(info.errors.size)
info.errors.mapTo(toReturn) {
RequestException(subscriptionId, info.serviceId.toString() + ":" + info.url, it)
}
return toReturn
}
}
}
// /////////////////////////////////////////////////////////////////////////
// Notification

View File

@@ -2,58 +2,33 @@ package org.schabi.newpipe.local.feed.service
import org.schabi.newpipe.database.subscription.NotificationMode
import org.schabi.newpipe.database.subscription.SubscriptionEntity
import org.schabi.newpipe.extractor.Info
import org.schabi.newpipe.extractor.channel.ChannelInfo
import org.schabi.newpipe.extractor.ListInfo
import org.schabi.newpipe.extractor.stream.StreamInfoItem
import org.schabi.newpipe.util.image.ImageStrategy
/**
* Instances of this class might stay around in memory for some time while fetching the feed,
* because of [FeedLoadManager.BUFFER_COUNT_BEFORE_INSERT]. Therefore this class should contain
* as little data as possible to avoid out of memory errors. In particular, avoid storing whole
* [ChannelInfo] objects, as they might contain raw JSON info in ready channel tabs link handlers.
*/
data class FeedUpdateInfo(
val uid: Long,
@NotificationMode
val notificationMode: Int,
val name: String,
val avatarUrl: String,
val url: String,
val serviceId: Int,
// description and subscriberCount are null if the constructor info is from the fast feed method
val description: String?,
val subscriberCount: Long?,
val streams: List<StreamInfoItem>,
val errors: List<Throwable>,
val listInfo: ListInfo<StreamInfoItem>,
) {
constructor(
subscription: SubscriptionEntity,
info: Info,
streams: List<StreamInfoItem>,
errors: List<Throwable>,
listInfo: ListInfo<StreamInfoItem>,
) : this(
uid = subscription.uid,
notificationMode = subscription.notificationMode,
name = info.name,
avatarUrl = (info as? ChannelInfo)?.avatars?.let {
// if the newly fetched info is not from fast feed, then it contains updated avatars
ImageStrategy.imageListToDbUrl(it)
} ?: subscription.avatarUrl,
url = info.url,
serviceId = info.serviceId,
// there is no description and subscriberCount in the fast feed
description = (info as? ChannelInfo)?.description,
subscriberCount = (info as? ChannelInfo)?.subscriberCount,
streams = streams,
errors = errors,
name = subscription.name,
avatarUrl = subscription.avatarUrl,
listInfo = listInfo,
)
/**
* Integer id, can be used as notification id, etc.
*/
val pseudoId: Int
get() = url.hashCode()
get() = listInfo.url.hashCode()
lateinit var newStreams: List<StreamInfoItem>
}

View File

@@ -28,16 +28,14 @@ import org.schabi.newpipe.databinding.StatisticPlaylistControlBinding;
import org.schabi.newpipe.error.ErrorInfo;
import org.schabi.newpipe.error.UserAction;
import org.schabi.newpipe.extractor.stream.StreamInfoItem;
import org.schabi.newpipe.fragments.list.playlist.PlaylistControlViewHolder;
import org.schabi.newpipe.info_list.dialog.InfoItemDialog;
import org.schabi.newpipe.info_list.dialog.StreamDialogDefaultEntry;
import org.schabi.newpipe.local.BaseLocalListFragment;
import org.schabi.newpipe.player.playqueue.PlayQueue;
import org.schabi.newpipe.player.playqueue.SinglePlayQueue;
import org.schabi.newpipe.settings.HistorySettingsFragment;
import org.schabi.newpipe.util.NavigationHelper;
import org.schabi.newpipe.util.OnClickGesture;
import org.schabi.newpipe.util.PlayButtonHelper;
import org.schabi.newpipe.info_list.dialog.StreamDialogDefaultEntry;
import java.util.ArrayList;
import java.util.Collections;
@@ -51,8 +49,7 @@ import io.reactivex.rxjava3.disposables.CompositeDisposable;
import io.reactivex.rxjava3.disposables.Disposable;
public class StatisticsPlaylistFragment
extends BaseLocalListFragment<List<StreamStatisticsEntry>, Void>
implements PlaylistControlViewHolder {
extends BaseLocalListFragment<List<StreamStatisticsEntry>, Void> {
private final CompositeDisposable disposables = new CompositeDisposable();
@State
Parcelable itemsListState;
@@ -198,9 +195,14 @@ public class StatisticsPlaylistFragment
if (itemListAdapter != null) {
itemListAdapter.unsetSelectedListener();
}
if (playlistControlBinding != null) {
playlistControlBinding.playlistCtrlPlayBgButton.setOnClickListener(null);
playlistControlBinding.playlistCtrlPlayAllButton.setOnClickListener(null);
playlistControlBinding.playlistCtrlPlayPopupButton.setOnClickListener(null);
headerBinding = null;
playlistControlBinding = null;
headerBinding = null;
playlistControlBinding = null;
}
if (databaseSubscription != null) {
databaseSubscription.cancel();
@@ -274,8 +276,12 @@ public class StatisticsPlaylistFragment
itemsListState = null;
}
PlayButtonHelper.initPlaylistControlClickListener(activity, playlistControlBinding, this);
playlistControlBinding.playlistCtrlPlayAllButton.setOnClickListener(view ->
NavigationHelper.playOnMainPlayer(activity, getPlayQueue()));
playlistControlBinding.playlistCtrlPlayPopupButton.setOnClickListener(view ->
NavigationHelper.playOnPopupPlayer(activity, getPlayQueue(), false));
playlistControlBinding.playlistCtrlPlayBgButton.setOnClickListener(view ->
NavigationHelper.playOnBackgroundPlayer(activity, getPlayQueue(), false));
headerBinding.sortButton.setOnClickListener(view -> toggleSortMode());
hideLoading();
@@ -368,7 +374,7 @@ public class StatisticsPlaylistFragment
}
}
public PlayQueue getPlayQueue() {
private PlayQueue getPlayQueue() {
return getPlayQueue(0);
}

View File

@@ -8,7 +8,7 @@ import org.schabi.newpipe.database.playlist.PlaylistDuplicatesEntry;
import org.schabi.newpipe.database.playlist.PlaylistMetadataEntry;
import org.schabi.newpipe.local.LocalItemBuilder;
import org.schabi.newpipe.local.history.HistoryRecordManager;
import org.schabi.newpipe.util.image.PicassoHelper;
import org.schabi.newpipe.util.PicassoHelper;
import org.schabi.newpipe.util.Localization;
import java.time.format.DateTimeFormatter;

View File

@@ -16,7 +16,7 @@ import org.schabi.newpipe.local.LocalItemBuilder;
import org.schabi.newpipe.local.history.HistoryRecordManager;
import org.schabi.newpipe.util.DependentPreferenceHelper;
import org.schabi.newpipe.util.Localization;
import org.schabi.newpipe.util.image.PicassoHelper;
import org.schabi.newpipe.util.PicassoHelper;
import org.schabi.newpipe.util.ServiceHelper;
import org.schabi.newpipe.views.AnimatedProgressBar;

View File

@@ -16,7 +16,7 @@ import org.schabi.newpipe.local.LocalItemBuilder;
import org.schabi.newpipe.local.history.HistoryRecordManager;
import org.schabi.newpipe.util.DependentPreferenceHelper;
import org.schabi.newpipe.util.Localization;
import org.schabi.newpipe.util.image.PicassoHelper;
import org.schabi.newpipe.util.PicassoHelper;
import org.schabi.newpipe.util.ServiceHelper;
import org.schabi.newpipe.views.AnimatedProgressBar;

View File

@@ -8,7 +8,7 @@ import org.schabi.newpipe.database.playlist.model.PlaylistRemoteEntity;
import org.schabi.newpipe.local.LocalItemBuilder;
import org.schabi.newpipe.local.history.HistoryRecordManager;
import org.schabi.newpipe.util.Localization;
import org.schabi.newpipe.util.image.PicassoHelper;
import org.schabi.newpipe.util.PicassoHelper;
import org.schabi.newpipe.util.ServiceHelper;
import java.time.format.DateTimeFormatter;

View File

@@ -22,6 +22,7 @@ import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AlertDialog;
import androidx.preference.PreferenceManager;
import androidx.recyclerview.widget.ItemTouchHelper;
import androidx.recyclerview.widget.RecyclerView;
import androidx.viewbinding.ViewBinding;
@@ -41,18 +42,16 @@ import org.schabi.newpipe.databinding.PlaylistControlBinding;
import org.schabi.newpipe.error.ErrorInfo;
import org.schabi.newpipe.error.UserAction;
import org.schabi.newpipe.extractor.stream.StreamInfoItem;
import org.schabi.newpipe.fragments.MainFragment;
import org.schabi.newpipe.fragments.list.playlist.PlaylistControlViewHolder;
import org.schabi.newpipe.info_list.dialog.InfoItemDialog;
import org.schabi.newpipe.info_list.dialog.StreamDialogDefaultEntry;
import org.schabi.newpipe.local.BaseLocalListFragment;
import org.schabi.newpipe.local.history.HistoryRecordManager;
import org.schabi.newpipe.player.PlayerType;
import org.schabi.newpipe.player.playqueue.PlayQueue;
import org.schabi.newpipe.player.playqueue.SinglePlayQueue;
import org.schabi.newpipe.util.Localization;
import org.schabi.newpipe.util.NavigationHelper;
import org.schabi.newpipe.util.OnClickGesture;
import org.schabi.newpipe.util.PlayButtonHelper;
import org.schabi.newpipe.util.external_communication.ShareUtils;
import java.util.ArrayList;
@@ -70,9 +69,8 @@ import io.reactivex.rxjava3.disposables.Disposable;
import io.reactivex.rxjava3.schedulers.Schedulers;
import io.reactivex.rxjava3.subjects.PublishSubject;
public class LocalPlaylistFragment extends BaseLocalListFragment<List<PlaylistStreamEntry>, Void>
implements PlaylistControlViewHolder {
/** Save the list 10 seconds after the last change occurred. */
public class LocalPlaylistFragment extends BaseLocalListFragment<List<PlaylistStreamEntry>, Void> {
// Save the list 10 seconds after the last change occurred
private static final long SAVE_DEBOUNCE_MILLIS = 10000;
private static final int MINIMUM_INITIAL_DRAG_VELOCITY = 12;
@State
@@ -93,20 +91,13 @@ public class LocalPlaylistFragment extends BaseLocalListFragment<List<PlaylistSt
private PublishSubject<Long> debouncedSaveSignal;
private CompositeDisposable disposables;
/** Whether the playlist has been fully loaded from db. */
/* Has the playlist been fully loaded from db */
private AtomicBoolean isLoadingComplete;
/** Whether the playlist has been modified (e.g. items reordered or deleted) */
/* Has the playlist been modified (e.g. items reordered or deleted) */
private AtomicBoolean isModified;
/** Flag to prevent simultaneous rewrites of the playlist. */
/* Flag to prevent simultaneous rewrites of the playlist */
private boolean isRewritingPlaylist = false;
/**
* The pager adapter that the fragment is created from when it is used as frontpage, i.e.
* {@link #useAsFrontPage} is {@link true}.
*/
@Nullable
private MainFragment.SelectedTabsPagerAdapter tabsPagerAdapter = null;
public static LocalPlaylistFragment getInstance(final long playlistId, final String name) {
final LocalPlaylistFragment instance = new LocalPlaylistFragment();
instance.setInitialData(playlistId, name);
@@ -166,17 +157,6 @@ public class LocalPlaylistFragment extends BaseLocalListFragment<List<PlaylistSt
return headerBinding;
}
/**
* <p>Commit changes immediately if the playlist has been modified.</p>
* Delete operations and other modifications will be committed to ensure that the database
* is up to date, e.g. when the user adds the just deleted stream from another fragment.
*/
public void commitChanges() {
if (isModified != null && isModified.get()) {
saveImmediate();
}
}
@Override
protected void initListeners() {
super.initListeners();
@@ -285,10 +265,14 @@ public class LocalPlaylistFragment extends BaseLocalListFragment<List<PlaylistSt
if (itemListAdapter != null) {
itemListAdapter.unsetSelectedListener();
}
if (playlistControlBinding != null) {
playlistControlBinding.playlistCtrlPlayBgButton.setOnClickListener(null);
playlistControlBinding.playlistCtrlPlayAllButton.setOnClickListener(null);
playlistControlBinding.playlistCtrlPlayPopupButton.setOnClickListener(null);
headerBinding = null;
playlistControlBinding = null;
headerBinding = null;
playlistControlBinding = null;
}
if (databaseSubscription != null) {
databaseSubscription.cancel();
@@ -310,9 +294,6 @@ public class LocalPlaylistFragment extends BaseLocalListFragment<List<PlaylistSt
if (disposables != null) {
disposables.dispose();
}
if (tabsPagerAdapter != null) {
tabsPagerAdapter.getLocalPlaylistFragments().remove(this);
}
debouncedSaveSignal = null;
playlistManager = null;
@@ -368,7 +349,7 @@ public class LocalPlaylistFragment extends BaseLocalListFragment<List<PlaylistSt
@Override
public boolean onOptionsItemSelected(final MenuItem item) {
if (item.getItemId() == R.id.menu_item_share_playlist) {
createShareConfirmationDialog();
sharePlaylist();
} else if (item.getItemId() == R.id.menu_item_rename_playlist) {
createRenameDialog();
} else if (item.getItemId() == R.id.menu_item_remove_watched) {
@@ -396,33 +377,16 @@ public class LocalPlaylistFragment extends BaseLocalListFragment<List<PlaylistSt
}
/**
* Shares the playlist as a list of stream URLs if {@code shouldSharePlaylistDetails} is
* set to {@code false}. Shares the playlist name along with a list of video titles and URLs
* if {@code shouldSharePlaylistDetails} is set to {@code true}.
*
* @param shouldSharePlaylistDetails Whether the playlist details should be included in the
* shared content.
* Share the playlist as a newline-separated list of stream URLs.
*/
private void sharePlaylist(final boolean shouldSharePlaylistDetails) {
final Context context = requireContext();
public void sharePlaylist() {
disposables.add(playlistManager.getPlaylistStreams(playlistId)
.flatMapSingle(playlist -> Single.just(playlist.stream()
.map(PlaylistStreamEntry::getStreamEntity)
.map(streamEntity -> {
if (shouldSharePlaylistDetails) {
return context.getString(R.string.video_details_list_item,
streamEntity.getTitle(), streamEntity.getUrl());
} else {
return streamEntity.getUrl();
}
})
.map(StreamEntity::getUrl)
.collect(Collectors.joining("\n"))))
.observeOn(AndroidSchedulers.mainThread())
.subscribe(urlsText -> ShareUtils.shareText(
context, name, shouldSharePlaylistDetails
? context.getString(R.string.share_playlist_content_details,
name, urlsText) : urlsText),
.subscribe(urlsText -> ShareUtils.shareText(requireContext(), name, urlsText),
throwable -> showUiErrorSnackbar(this, "Sharing playlist", throwable)));
}
@@ -534,11 +498,38 @@ public class LocalPlaylistFragment extends BaseLocalListFragment<List<PlaylistSt
}
setVideoCount(itemListAdapter.getItemsList().size());
PlayButtonHelper.initPlaylistControlClickListener(activity, playlistControlBinding, this);
playlistControlBinding.playlistCtrlPlayAllButton.setOnClickListener(view -> {
NavigationHelper.playOnMainPlayer(activity, getPlayQueue());
showHoldToAppendTipIfNeeded();
});
playlistControlBinding.playlistCtrlPlayPopupButton.setOnClickListener(view -> {
NavigationHelper.playOnPopupPlayer(activity, getPlayQueue(), false);
showHoldToAppendTipIfNeeded();
});
playlistControlBinding.playlistCtrlPlayBgButton.setOnClickListener(view -> {
NavigationHelper.playOnBackgroundPlayer(activity, getPlayQueue(), false);
showHoldToAppendTipIfNeeded();
});
playlistControlBinding.playlistCtrlPlayPopupButton.setOnLongClickListener(view -> {
NavigationHelper.enqueueOnPlayer(activity, getPlayQueue(), PlayerType.POPUP);
return true;
});
playlistControlBinding.playlistCtrlPlayBgButton.setOnLongClickListener(view -> {
NavigationHelper.enqueueOnPlayer(activity, getPlayQueue(), PlayerType.AUDIO);
return true;
});
hideLoading();
}
private void showHoldToAppendTipIfNeeded() {
if (PreferenceManager.getDefaultSharedPreferences(activity)
.getBoolean(getString(R.string.show_hold_to_append_key), true)) {
Toast.makeText(activity, R.string.hold_to_append, Toast.LENGTH_SHORT).show();
}
}
///////////////////////////////////////////////////////////////////////////
// Fragment Error Handling
///////////////////////////////////////////////////////////////////////////
@@ -862,7 +853,7 @@ public class LocalPlaylistFragment extends BaseLocalListFragment<List<PlaylistSt
}
}
public PlayQueue getPlayQueue() {
private PlayQueue getPlayQueue() {
return getPlayQueue(0);
}
@@ -880,29 +871,5 @@ public class LocalPlaylistFragment extends BaseLocalListFragment<List<PlaylistSt
}
return new SinglePlayQueue(streamInfoItems, index);
}
/**
* Creates a dialog to confirm whether the user wants to share the playlist
* with the playlist details or just the list of stream URLs.
* After the user has made a choice, the playlist is shared.
*/
private void createShareConfirmationDialog() {
new AlertDialog.Builder(requireContext())
.setTitle(R.string.share_playlist)
.setMessage(R.string.share_playlist_with_titles_message)
.setCancelable(true)
.setPositiveButton(R.string.share_playlist_with_titles, (dialog, which) ->
sharePlaylist(/* shouldSharePlaylistDetails= */ true)
)
.setNegativeButton(R.string.share_playlist_with_list, (dialog, which) ->
sharePlaylist(/* shouldSharePlaylistDetails= */ false)
)
.show();
}
public void setTabsPagerAdapter(
@Nullable final MainFragment.SelectedTabsPagerAdapter tabsPagerAdapter) {
this.tabsPagerAdapter = tabsPagerAdapter;
}
}

View File

@@ -115,11 +115,6 @@ class SubscriptionFragment : BaseStateFragment<SubscriptionState>() {
feedGroupsCarouselState = feedGroupsCarousel.onSaveInstanceState()
}
override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
override fun onDestroy() {
super.onDestroy()
disposables.dispose()
@@ -341,7 +336,8 @@ class SubscriptionFragment : BaseStateFragment<SubscriptionState>() {
val actions = DialogInterface.OnClickListener { _, i ->
when (i) {
0 -> ShareUtils.shareText(
requireContext(), selectedItem.name, selectedItem.url, selectedItem.thumbnails
requireContext(), selectedItem.name, selectedItem.url,
selectedItem.thumbnailUrl
)
1 -> ShareUtils.openUrlInBrowser(requireContext(), selectedItem.url)
2 -> deleteChannel(selectedItem)

View File

@@ -1,7 +1,6 @@
package org.schabi.newpipe.local.subscription
import android.content.Context
import android.util.Pair
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
import io.reactivex.rxjava3.core.Completable
import io.reactivex.rxjava3.core.Flowable
@@ -12,13 +11,12 @@ import org.schabi.newpipe.database.stream.model.StreamEntity
import org.schabi.newpipe.database.subscription.NotificationMode
import org.schabi.newpipe.database.subscription.SubscriptionDAO
import org.schabi.newpipe.database.subscription.SubscriptionEntity
import org.schabi.newpipe.extractor.ListInfo
import org.schabi.newpipe.extractor.channel.ChannelInfo
import org.schabi.newpipe.extractor.channel.tabs.ChannelTabInfo
import org.schabi.newpipe.extractor.feed.FeedInfo
import org.schabi.newpipe.extractor.stream.StreamInfoItem
import org.schabi.newpipe.local.feed.FeedDatabaseManager
import org.schabi.newpipe.local.feed.service.FeedUpdateInfo
import org.schabi.newpipe.util.ExtractorHelper
import org.schabi.newpipe.util.image.ImageStrategy
class SubscriptionManager(context: Context) {
private val database = NewPipeDatabase.getInstance(context)
@@ -48,38 +46,28 @@ class SubscriptionManager(context: Context) {
}
}
fun upsertAll(infoList: List<Pair<ChannelInfo, List<ChannelTabInfo>>>): List<SubscriptionEntity> {
fun upsertAll(infoList: List<ChannelInfo>): List<SubscriptionEntity> {
val listEntities = subscriptionTable.upsertAll(
infoList.map { SubscriptionEntity.from(it.first) }
infoList.map { SubscriptionEntity.from(it) }
)
database.runInTransaction {
infoList.forEachIndexed { index, info ->
info.second.forEach {
feedDatabaseManager.upsertAll(
listEntities[index].uid,
it.relatedItems.filterIsInstance<StreamInfoItem>()
)
}
feedDatabaseManager.upsertAll(listEntities[index].uid, info.relatedItems)
}
}
return listEntities
}
fun updateChannelInfo(info: ChannelInfo): Completable =
subscriptionTable.getSubscription(info.serviceId, info.url)
.flatMapCompletable {
Completable.fromRunnable {
it.setData(
info.name,
ImageStrategy.imageListToDbUrl(info.avatars),
info.description,
info.subscriberCount
)
subscriptionTable.update(it)
}
fun updateChannelInfo(info: ChannelInfo): Completable = subscriptionTable.getSubscription(info.serviceId, info.url)
.flatMapCompletable {
Completable.fromRunnable {
it.setData(info.name, info.avatarUrl, info.description, info.subscriberCount)
subscriptionTable.update(it)
feedDatabaseManager.upsertAll(it.uid, info.relatedItems)
}
}
fun updateNotificationMode(serviceId: Int, url: String, @NotificationMode mode: Int): Completable {
return subscriptionTable().getSubscription(serviceId, url)
@@ -96,15 +84,19 @@ class SubscriptionManager(context: Context) {
}
}
fun updateFromInfo(info: FeedUpdateInfo) {
val subscriptionEntity = subscriptionTable.getSubscription(info.uid)
fun updateFromInfo(subscriptionId: Long, info: ListInfo<StreamInfoItem>) {
val subscriptionEntity = subscriptionTable.getSubscription(subscriptionId)
subscriptionEntity.name = info.name
subscriptionEntity.avatarUrl = info.avatarUrl
// these two fields are null if the feed info was fetched using the fast feed method
info.description?.let { subscriptionEntity.description = it }
info.subscriberCount?.let { subscriptionEntity.subscriberCount = it }
if (info is FeedInfo) {
subscriptionEntity.name = info.name
} else if (info is ChannelInfo) {
subscriptionEntity.setData(
info.name,
info.avatarUrl,
info.description,
info.subscriberCount
)
}
subscriptionTable.update(subscriptionEntity)
}
@@ -115,8 +107,11 @@ class SubscriptionManager(context: Context) {
.observeOn(AndroidSchedulers.mainThread())
}
fun insertSubscription(subscriptionEntity: SubscriptionEntity) {
subscriptionTable.insert(subscriptionEntity)
fun insertSubscription(subscriptionEntity: SubscriptionEntity, info: ChannelInfo) {
database.runInTransaction {
val subscriptionId = subscriptionTable.insert(subscriptionEntity)
feedDatabaseManager.upsertAll(subscriptionId, info.relatedItems)
}
}
fun deleteSubscription(subscriptionEntity: SubscriptionEntity) {
@@ -130,10 +125,7 @@ class SubscriptionManager(context: Context) {
*/
private fun rememberAllStreams(subscription: SubscriptionEntity): Completable {
return ExtractorHelper.getChannelInfo(subscription.serviceId, subscription.url, false)
.flatMap { info ->
ExtractorHelper.getChannelTab(subscription.serviceId, info.tabs.first(), false)
}
.map { channel -> channel.relatedItems.filterIsInstance<StreamInfoItem>().map { stream -> StreamEntity(stream) } }
.map { channel -> channel.relatedItems.map { stream -> StreamEntity(stream) } }
.flatMapCompletable { entities ->
Completable.fromAction {
database.streamDAO().upsertAll(entities)

View File

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

View File

@@ -10,7 +10,7 @@ import org.schabi.newpipe.database.subscription.SubscriptionEntity
import org.schabi.newpipe.databinding.PickerSubscriptionItemBinding
import org.schabi.newpipe.ktx.AnimationType
import org.schabi.newpipe.ktx.animate
import org.schabi.newpipe.util.image.PicassoHelper
import org.schabi.newpipe.util.PicassoHelper
data class PickerSubscriptionItem(
val subscriptionEntity: SubscriptionEntity,

View File

@@ -26,7 +26,6 @@ import android.content.Intent;
import android.net.Uri;
import android.text.TextUtils;
import android.util.Log;
import android.util.Pair;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
@@ -39,7 +38,6 @@ import org.schabi.newpipe.R;
import org.schabi.newpipe.database.subscription.SubscriptionEntity;
import org.schabi.newpipe.extractor.NewPipe;
import org.schabi.newpipe.extractor.channel.ChannelInfo;
import org.schabi.newpipe.extractor.channel.tabs.ChannelTabInfo;
import org.schabi.newpipe.extractor.subscription.SubscriptionItem;
import org.schabi.newpipe.ktx.ExceptionUtils;
import org.schabi.newpipe.streams.io.SharpInputStream;
@@ -50,7 +48,6 @@ import org.schabi.newpipe.util.ExtractorHelper;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
@@ -202,19 +199,12 @@ public class SubscriptionsImportService extends BaseImportExportService {
.parallel(PARALLEL_EXTRACTIONS)
.runOn(Schedulers.io())
.map((Function<SubscriptionItem, Notification<Pair<ChannelInfo,
List<ChannelTabInfo>>>>) subscriptionItem -> {
.map((Function<SubscriptionItem, Notification<ChannelInfo>>) subscriptionItem -> {
try {
final ChannelInfo channelInfo = ExtractorHelper
return Notification.createOnNext(ExtractorHelper
.getChannelInfo(subscriptionItem.getServiceId(),
subscriptionItem.getUrl(), true)
.blockingGet();
return Notification.createOnNext(new Pair<>(channelInfo,
Collections.singletonList(
ExtractorHelper.getChannelTab(
subscriptionItem.getServiceId(),
channelInfo.getTabs().get(0), true).blockingGet()
)));
.blockingGet());
} catch (final Throwable e) {
return Notification.createOnError(e);
}
@@ -233,7 +223,7 @@ public class SubscriptionsImportService extends BaseImportExportService {
}
private Subscriber<List<SubscriptionEntity>> getSubscriber() {
return new Subscriber<>() {
return new Subscriber<List<SubscriptionEntity>>() {
@Override
public void onSubscribe(final Subscription s) {
subscription = s;
@@ -264,11 +254,10 @@ public class SubscriptionsImportService extends BaseImportExportService {
};
}
private Consumer<Notification<Pair<ChannelInfo,
List<ChannelTabInfo>>>> getNotificationsConsumer() {
private Consumer<Notification<ChannelInfo>> getNotificationsConsumer() {
return notification -> {
if (notification.isOnNext()) {
final String name = notification.getValue().first.getName();
final String name = notification.getValue().getName();
eventListener.onItemCompleted(!TextUtils.isEmpty(name) ? name : "");
} else if (notification.isOnError()) {
final Throwable error = notification.getError();
@@ -286,12 +275,10 @@ public class SubscriptionsImportService extends BaseImportExportService {
};
}
private Function<List<Notification<Pair<ChannelInfo, List<ChannelTabInfo>>>>,
List<SubscriptionEntity>> upsertBatch() {
private Function<List<Notification<ChannelInfo>>, List<SubscriptionEntity>> upsertBatch() {
return notificationList -> {
final List<Pair<ChannelInfo, List<ChannelTabInfo>>> infoList =
new ArrayList<>(notificationList.size());
for (final Notification<Pair<ChannelInfo, List<ChannelTabInfo>>> n : notificationList) {
final List<ChannelInfo> infoList = new ArrayList<>(notificationList.size());
for (final Notification<ChannelInfo> n : notificationList) {
if (n.isOnNext()) {
infoList.add(n.getValue());
}

View File

@@ -16,7 +16,6 @@ import android.view.MenuItem;
import android.view.SubMenu;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageButton;
import android.widget.SeekBar;
import androidx.annotation.Nullable;
@@ -532,19 +531,18 @@ public final class PlayQueueActivity extends AppCompatActivity
////////////////////////////////////////////////////////////////////////////
private void onStateChanged(final int state) {
final ImageButton playPauseButton = queueControlBinding.controlPlayPause;
switch (state) {
case Player.STATE_PAUSED:
playPauseButton.setImageResource(R.drawable.ic_play_arrow);
playPauseButton.setContentDescription(getString(R.string.play));
queueControlBinding.controlPlayPause
.setImageResource(R.drawable.ic_play_arrow);
break;
case Player.STATE_PLAYING:
playPauseButton.setImageResource(R.drawable.ic_pause);
playPauseButton.setContentDescription(getString(R.string.pause));
queueControlBinding.controlPlayPause
.setImageResource(R.drawable.ic_pause);
break;
case Player.STATE_COMPLETED:
playPauseButton.setImageResource(R.drawable.ic_replay);
playPauseButton.setContentDescription(getString(R.string.replay));
queueControlBinding.controlPlayPause
.setImageResource(R.drawable.ic_replay);
break;
default:
break;
@@ -587,9 +585,11 @@ public final class PlayQueueActivity extends AppCompatActivity
}
private void onPlaybackParameterChanged(@Nullable final PlaybackParameters parameters) {
if (parameters != null && menu != null && player != null) {
final MenuItem item = menu.findItem(R.id.action_playback_speed);
item.setTitle(formatSpeed(parameters.speed));
if (parameters != null) {
if (menu != null && player != null) {
final MenuItem item = menu.findItem(R.id.action_playback_speed);
item.setTitle(formatSpeed(parameters.speed));
}
}
}
@@ -619,13 +619,11 @@ public final class PlayQueueActivity extends AppCompatActivity
final MenuItem audioTrackSelector = menu.findItem(R.id.action_audio_track);
final List<AudioStream> availableStreams =
Optional.ofNullable(player)
.map(Player::getCurrentMetadata)
Optional.ofNullable(player.getCurrentMetadata())
.flatMap(MediaItemTag::getMaybeAudioTrack)
.map(MediaItemTag.AudioTrack::getAudioStreams)
.orElse(null);
final Optional<AudioStream> selectedAudioStream = Optional.ofNullable(player)
.flatMap(Player::getSelectedAudioStream);
final Optional<AudioStream> selectedAudioStream = player.getSelectedAudioStream();
if (availableStreams == null || availableStreams.size() < 2
|| selectedAudioStream.isEmpty()) {

View File

@@ -87,7 +87,6 @@ import org.schabi.newpipe.error.ErrorInfo;
import org.schabi.newpipe.error.ErrorUtil;
import org.schabi.newpipe.error.UserAction;
import org.schabi.newpipe.extractor.stream.AudioStream;
import org.schabi.newpipe.extractor.Image;
import org.schabi.newpipe.extractor.stream.StreamInfo;
import org.schabi.newpipe.extractor.stream.StreamType;
import org.schabi.newpipe.extractor.stream.VideoStream;
@@ -118,7 +117,7 @@ import org.schabi.newpipe.player.ui.VideoPlayerUi;
import org.schabi.newpipe.util.DependentPreferenceHelper;
import org.schabi.newpipe.util.ListHelper;
import org.schabi.newpipe.util.NavigationHelper;
import org.schabi.newpipe.util.image.PicassoHelper;
import org.schabi.newpipe.util.PicassoHelper;
import org.schabi.newpipe.util.SerializedCache;
import org.schabi.newpipe.util.StreamTypeUtil;
@@ -806,10 +805,10 @@ public final class Player implements PlaybackListener, Listener {
};
}
private void loadCurrentThumbnail(final List<Image> thumbnails) {
private void loadCurrentThumbnail(final String url) {
if (DEBUG) {
Log.d(TAG, "Thumbnail - loadCurrentThumbnail() called with thumbnails = ["
+ thumbnails.size() + "]");
Log.d(TAG, "Thumbnail - loadCurrentThumbnail() called with url = ["
+ (url == null ? "null" : url) + "]");
}
// first cancel any previous loading
@@ -818,12 +817,12 @@ public final class Player implements PlaybackListener, Listener {
// Unset currentThumbnail, since it is now outdated. This ensures it is not used in media
// session metadata while the new thumbnail is being loaded by Picasso.
onThumbnailLoaded(null);
if (thumbnails.isEmpty()) {
if (isNullOrEmpty(url)) {
return;
}
// scale down the notification thumbnail for performance
PicassoHelper.loadScaledDownThumbnail(context, thumbnails)
PicassoHelper.loadScaledDownThumbnail(context, url)
.tag(PICASSO_PLAYER_THUMBNAIL_TAG)
.into(currentThumbnailTarget);
}
@@ -1083,7 +1082,7 @@ public final class Player implements PlaybackListener, Listener {
UIs.call(PlayerUi::onPrepared);
if (playWhenReady && !isMuted()) {
if (playWhenReady) {
audioReactor.requestAudioFocus();
}
}
@@ -1224,11 +1223,6 @@ public final class Player implements PlaybackListener, Listener {
public void toggleMute() {
final boolean wasMuted = isMuted();
simpleExoPlayer.setVolume(wasMuted ? 1 : 0);
if (wasMuted) {
audioReactor.requestAudioFocus();
} else {
audioReactor.abandonAudioFocus();
}
UIs.call(playerUi -> playerUi.onMuteUnmuteChanged(!wasMuted));
notifyPlaybackUpdateToListeners();
}
@@ -1626,9 +1620,7 @@ public final class Player implements PlaybackListener, Listener {
return;
}
if (!isMuted()) {
audioReactor.requestAudioFocus();
}
audioReactor.requestAudioFocus();
if (currentState == STATE_COMPLETED) {
if (playQueue.getIndex() == 0) {
@@ -1793,7 +1785,7 @@ public final class Player implements PlaybackListener, Listener {
maybeAutoQueueNextStream(info);
loadCurrentThumbnail(info.getThumbnails());
loadCurrentThumbnail(info.getThumbnailUrl());
registerStreamViewed();
notifyMetadataUpdateToListeners();
@@ -2073,36 +2065,43 @@ public final class Player implements PlaybackListener, Listener {
}
public void useVideoSource(final boolean videoEnabled) {
if (playQueue == null || audioPlayerSelected()) {
if (playQueue == null || isAudioOnly == !videoEnabled || audioPlayerSelected()) {
return;
}
isAudioOnly = !videoEnabled;
// The current metadata may be null sometimes (for e.g. when using an unstable connection
// in livestreams) so we will be not able to execute the block below.
// Reload the play queue manager in this case, which is the behavior when we don't know the
// index of the video renderer or playQueueManagerReloadingNeeded returns true.
getCurrentStreamInfo().ifPresentOrElse(info -> {
// In case we don't know the source type, fall back to either video-with-audio, or
// audio-only source type
// In the case we don't know the source type, fallback to the one with video with audio
// or audio-only source.
final SourceType sourceType = videoResolver.getStreamSourceType()
.orElse(SourceType.VIDEO_WITH_AUDIO_OR_AUDIO_ONLY);
if (playQueueManagerReloadingNeeded(sourceType, info, getVideoRendererIndex())) {
reloadPlayQueueManager();
} else {
if (StreamTypeUtil.isAudio(info.getStreamType())) {
// Nothing to do more than setting the recovery position
setRecovery();
return;
}
final var parametersBuilder = trackSelector.buildUponParameters();
// Enable/disable the video track and the ability to select subtitles
parametersBuilder.setTrackTypeDisabled(C.TRACK_TYPE_TEXT, !videoEnabled);
parametersBuilder.setTrackTypeDisabled(C.TRACK_TYPE_VIDEO, !videoEnabled);
trackSelector.setParameters(parametersBuilder);
}
setRecovery();
// Disable or enable video and subtitles renderers depending of the videoEnabled value
trackSelector.setParameters(trackSelector.buildUponParameters()
.setTrackTypeDisabled(C.TRACK_TYPE_TEXT, !videoEnabled)
.setTrackTypeDisabled(C.TRACK_TYPE_VIDEO, !videoEnabled));
}, () -> {
/*
The current metadata may be null sometimes (for e.g. when using an unstable connection
in livestreams) so we will be not able to execute the block below
Reload the play queue manager in this case, which is the behavior when we don't know the
index of the video renderer or playQueueManagerReloadingNeeded returns true
*/
// This is executed when the current stream info is not available.
reloadPlayQueueManager();
setRecovery();
});

View File

@@ -29,11 +29,8 @@ import android.os.IBinder;
import android.util.Log;
import org.schabi.newpipe.player.mediasession.MediaSessionPlayerUi;
import org.schabi.newpipe.player.notification.NotificationPlayerUi;
import org.schabi.newpipe.util.ThemeHelper;
import java.lang.ref.WeakReference;
/**
* One service for all players.
@@ -44,7 +41,7 @@ public final class PlayerService extends Service {
private Player player;
private final IBinder mBinder = new PlayerService.LocalBinder(this);
private final IBinder mBinder = new PlayerService.LocalBinder();
/*//////////////////////////////////////////////////////////////////////////
@@ -60,14 +57,6 @@ public final class PlayerService extends Service {
ThemeHelper.setTheme(this);
player = new Player(this);
/*
Create the player notification and start immediately the service in foreground,
otherwise if nothing is played or initializing the player and its components (especially
loading stream metadata) takes a lot of time, the app would crash on Android 8+ as the
service would never be put in the foreground while we said to the system we would do so
*/
player.UIs().get(NotificationPlayerUi.class)
.ifPresent(NotificationPlayerUi::createNotificationAndStartForeground);
}
@Override
@@ -77,38 +66,16 @@ public final class PlayerService extends Service {
+ "], flags = [" + flags + "], startId = [" + startId + "]");
}
/*
Be sure that the player notification is set and the service is started in foreground,
otherwise, the app may crash on Android 8+ as the service would never be put in the
foreground while we said to the system we would do so
The service is always requested to be started in foreground, so always creating a
notification if there is no one already and starting the service in foreground should
not create any issues
If the service is already started in foreground, requesting it to be started shouldn't
do anything
*/
if (player != null) {
player.UIs().get(NotificationPlayerUi.class)
.ifPresent(NotificationPlayerUi::createNotificationAndStartForeground);
}
if (Intent.ACTION_MEDIA_BUTTON.equals(intent.getAction())
&& (player == null || player.getPlayQueue() == null)) {
/*
No need to process media button's actions if the player is not working, otherwise
the player service would strangely start with nothing to play
Stop the service in this case, which will be removed from the foreground and its
notification cancelled in its destruction
*/
stopSelf();
&& player.getPlayQueue() == null) {
// No need to process media button's actions if the player is not working, otherwise the
// player service would strangely start with nothing to play
return START_NOT_STICKY;
}
if (player != null) {
player.handleIntent(intent);
player.UIs().get(MediaSessionPlayerUi.class)
.ifPresent(ui -> ui.handleMediaButtonIntent(intent));
}
player.handleIntent(intent);
player.UIs().get(MediaSessionPlayerUi.class)
.ifPresent(ui -> ui.handleMediaButtonIntent(intent));
return START_NOT_STICKY;
}
@@ -118,7 +85,7 @@ public final class PlayerService extends Service {
Log.d(TAG, "stopForImmediateReusing() called");
}
if (player != null && !player.exoPlayerIsNull()) {
if (!player.exoPlayerIsNull()) {
// Releases wifi & cpu, disables keepScreenOn, etc.
// We can't just pause the player here because it will make transition
// from one stream to a new stream not smooth
@@ -129,7 +96,7 @@ public final class PlayerService extends Service {
@Override
public void onTaskRemoved(final Intent rootIntent) {
super.onTaskRemoved(rootIntent);
if (player != null && !player.videoPlayerSelected()) {
if (!player.videoPlayerSelected()) {
return;
}
onDestroy();
@@ -167,19 +134,14 @@ public final class PlayerService extends Service {
return mBinder;
}
public static class LocalBinder extends Binder {
private final WeakReference<PlayerService> playerService;
LocalBinder(final PlayerService playerService) {
this.playerService = new WeakReference<>(playerService);
}
public class LocalBinder extends Binder {
public PlayerService getService() {
return playerService.get();
return PlayerService.this;
}
public Player getPlayer() {
return playerService.get().player;
return PlayerService.this.player;
}
}
}

View File

@@ -7,6 +7,7 @@ import android.view.View.OnTouchListener
import android.widget.ProgressBar
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.content.res.AppCompatResources
import androidx.core.math.MathUtils
import androidx.core.view.isVisible
import org.schabi.newpipe.MainActivity
import org.schabi.newpipe.R
@@ -112,7 +113,7 @@ class MainPlayerGestureListener(
// Update progress bar
val oldBrightness = layoutParams.screenBrightness
bar.progress = (bar.max * oldBrightness.coerceIn(0f, 1f)).toInt()
bar.progress = (bar.max * MathUtils.clamp(oldBrightness, 0f, 1f)).toInt()
bar.incrementProgressBy(distanceY.toInt())
// Update brightness

View File

@@ -4,7 +4,7 @@ import android.util.Log
import android.view.MotionEvent
import android.view.View
import android.view.ViewConfiguration
import androidx.core.view.isVisible
import androidx.core.math.MathUtils
import org.schabi.newpipe.MainActivity
import org.schabi.newpipe.ktx.AnimationType
import org.schabi.newpipe.ktx.animate
@@ -235,16 +235,14 @@ class PopupPlayerGestureListener(
isMoving = true
val diffX = (movingEvent.rawX - initialEvent.rawX)
val posX = (initialPopupX + diffX).coerceIn(
0f,
(playerUi.screenWidth - playerUi.popupLayoutParams.width).toFloat()
.coerceAtLeast(0f)
val posX = MathUtils.clamp(
initialPopupX + diffX,
0f, (playerUi.screenWidth - playerUi.popupLayoutParams.width).toFloat()
)
val diffY = (movingEvent.rawY - initialEvent.rawY)
val posY = (initialPopupY + diffY).coerceIn(
0f,
(playerUi.screenHeight - playerUi.popupLayoutParams.height).toFloat()
.coerceAtLeast(0f)
val posY = MathUtils.clamp(
initialPopupY + diffY,
0f, (playerUi.screenHeight - playerUi.popupLayoutParams.height).toFloat()
)
playerUi.popupLayoutParams.x = posX.toInt()
@@ -253,7 +251,8 @@ class PopupPlayerGestureListener(
// -- Determine if the ClosingOverlayView (red X) has to be shown or hidden --
val showClosingOverlayView: Boolean = playerUi.isInsideClosingRadius(movingEvent)
// Check if an view is in expected state and if not animate it into the correct state
if (binding.closingOverlay.isVisible != showClosingOverlayView) {
val expectedVisibility = if (showClosingOverlayView) View.VISIBLE else View.GONE
if (binding.closingOverlay.visibility != expectedVisibility) {
binding.closingOverlay.animate(showClosingOverlayView, 200)
}

View File

@@ -3,7 +3,6 @@ package org.schabi.newpipe.player.mediaitem;
import org.schabi.newpipe.extractor.stream.StreamInfo;
import org.schabi.newpipe.extractor.stream.StreamType;
import org.schabi.newpipe.player.playqueue.PlayQueueItem;
import org.schabi.newpipe.util.image.ImageStrategy;
import java.util.List;
import java.util.Optional;
@@ -75,7 +74,7 @@ public final class ExceptionTag implements MediaItemTag {
@Override
public String getThumbnailUrl() {
return ImageStrategy.choosePreferredImage(item.getThumbnails());
return item.getThumbnailUrl();
}
@Override

View File

@@ -81,9 +81,8 @@ public interface MediaItemTag {
@NonNull
default MediaItem asMediaItem() {
final String thumbnailUrl = getThumbnailUrl();
final MediaMetadata mediaMetadata = new MediaMetadata.Builder()
.setArtworkUri(thumbnailUrl == null ? null : Uri.parse(thumbnailUrl))
.setArtworkUri(Uri.parse(getThumbnailUrl()))
.setArtist(getUploaderName())
.setDescription(getTitle())
.setDisplayTitle(getTitle())

View File

@@ -6,7 +6,6 @@ import org.schabi.newpipe.extractor.stream.AudioStream;
import org.schabi.newpipe.extractor.stream.StreamInfo;
import org.schabi.newpipe.extractor.stream.StreamType;
import org.schabi.newpipe.extractor.stream.VideoStream;
import org.schabi.newpipe.util.image.ImageStrategy;
import java.util.Collections;
import java.util.List;
@@ -96,7 +95,7 @@ public final class StreamInfoTag implements MediaItemTag {
@Override
public String getThumbnailUrl() {
return ImageStrategy.choosePreferredImage(streamInfo.getThumbnails());
return streamInfo.getThumbnailUrl();
}
@Override

View File

@@ -20,7 +20,6 @@ import com.google.android.exoplayer2.util.Util;
import org.schabi.newpipe.player.Player;
import org.schabi.newpipe.player.playqueue.PlayQueue;
import org.schabi.newpipe.player.playqueue.PlayQueueItem;
import org.schabi.newpipe.util.image.ImageStrategy;
import java.util.ArrayList;
import java.util.Collections;
@@ -138,12 +137,9 @@ public class PlayQueueNavigator implements MediaSessionConnector.QueueNavigator
.putLong(MediaMetadataCompat.METADATA_KEY_NUM_TRACKS, player.getPlayQueue().size());
descBuilder.setExtras(additionalMetadata);
try {
descBuilder.setIconUri(Uri.parse(
ImageStrategy.choosePreferredImage(item.getThumbnails())));
} catch (final Throwable e) {
// no thumbnail available at all, or the user disabled image loading,
// or the obtained url is not a valid `Uri`
final Uri thumbnailUri = Uri.parse(item.getThumbnailUrl());
if (thumbnailUri != null) {
descBuilder.setIconUri(thumbnailUri);
}
return descBuilder.build();

View File

@@ -17,6 +17,7 @@ import org.schabi.newpipe.player.helper.PlayerHelper;
import org.schabi.newpipe.player.ui.PlayerUi;
public final class NotificationPlayerUi extends PlayerUi {
private boolean foregroundNotificationAlreadyCreated = false;
private final NotificationUtil notificationUtil;
public NotificationPlayerUi(@NonNull final Player player) {
@@ -24,6 +25,15 @@ public final class NotificationPlayerUi extends PlayerUi {
notificationUtil = new NotificationUtil(player);
}
@Override
public void initPlayer() {
super.initPlayer();
if (!foregroundNotificationAlreadyCreated) {
notificationUtil.createNotificationAndStartForeground();
foregroundNotificationAlreadyCreated = true;
}
}
@Override
public void destroy() {
super.destroy();
@@ -112,8 +122,4 @@ public final class NotificationPlayerUi extends PlayerUi {
super.onPlayQueueEdited();
notificationUtil.createNotificationIfNeededAndUpdate(false);
}
public void createNotificationAndStartForeground() {
notificationUtil.createNotificationAndStartForeground();
}
}

View File

@@ -4,7 +4,6 @@ import android.util.Log;
import androidx.annotation.NonNull;
import org.schabi.newpipe.extractor.InfoItem;
import org.schabi.newpipe.extractor.ListExtractor;
import org.schabi.newpipe.extractor.ListInfo;
import org.schabi.newpipe.extractor.Page;
@@ -16,7 +15,7 @@ import java.util.stream.Collectors;
import io.reactivex.rxjava3.core.SingleObserver;
import io.reactivex.rxjava3.disposables.Disposable;
abstract class AbstractInfoPlayQueue<T extends ListInfo<? extends InfoItem>>
abstract class AbstractInfoPlayQueue<T extends ListInfo<StreamInfoItem>>
extends PlayQueue {
boolean isInitial;
private boolean isComplete;
@@ -28,13 +27,7 @@ abstract class AbstractInfoPlayQueue<T extends ListInfo<? extends InfoItem>>
private transient Disposable fetchReactor;
protected AbstractInfoPlayQueue(final T info) {
this(info.getServiceId(), info.getUrl(), info.getNextPage(),
info.getRelatedItems()
.stream()
.filter(StreamInfoItem.class::isInstance)
.map(StreamInfoItem.class::cast)
.collect(Collectors.toList()),
0);
this(info.getServiceId(), info.getUrl(), info.getNextPage(), info.getRelatedItems(), 0);
}
protected AbstractInfoPlayQueue(final int serviceId,
@@ -79,11 +72,7 @@ abstract class AbstractInfoPlayQueue<T extends ListInfo<? extends InfoItem>>
}
nextPage = result.getNextPage();
append(extractListItems(result.getRelatedItems()
.stream()
.filter(StreamInfoItem.class::isInstance)
.map(StreamInfoItem.class::cast)
.collect(Collectors.toList())));
append(extractListItems(result.getRelatedItems()));
fetchReactor.dispose();
fetchReactor = null;
@@ -98,7 +87,7 @@ abstract class AbstractInfoPlayQueue<T extends ListInfo<? extends InfoItem>>
};
}
SingleObserver<ListExtractor.InfoItemsPage<? extends InfoItem>> getNextPageObserver() {
SingleObserver<ListExtractor.InfoItemsPage<StreamInfoItem>> getNextPageObserver() {
return new SingleObserver<>() {
@Override
public void onSubscribe(@NonNull final Disposable d) {
@@ -112,17 +101,13 @@ abstract class AbstractInfoPlayQueue<T extends ListInfo<? extends InfoItem>>
@Override
public void onSuccess(
@NonNull final ListExtractor.InfoItemsPage<? extends InfoItem> result) {
@NonNull final ListExtractor.InfoItemsPage<StreamInfoItem> result) {
if (!result.hasNextPage()) {
isComplete = true;
}
nextPage = result.getNextPage();
append(extractListItems(result.getItems()
.stream()
.filter(StreamInfoItem.class::isInstance)
.map(StreamInfoItem.class::cast)
.collect(Collectors.toList())));
append(extractListItems(result.getItems()));
fetchReactor.dispose();
fetchReactor = null;

View File

@@ -0,0 +1,47 @@
package org.schabi.newpipe.player.playqueue;
import org.schabi.newpipe.extractor.Page;
import org.schabi.newpipe.extractor.channel.ChannelInfo;
import org.schabi.newpipe.extractor.stream.StreamInfoItem;
import org.schabi.newpipe.util.ExtractorHelper;
import java.util.List;
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
import io.reactivex.rxjava3.schedulers.Schedulers;
public final class ChannelPlayQueue extends AbstractInfoPlayQueue<ChannelInfo> {
public ChannelPlayQueue(final ChannelInfo info) {
super(info);
}
public ChannelPlayQueue(final int serviceId,
final String url,
final Page nextPage,
final List<StreamInfoItem> streams,
final int index) {
super(serviceId, url, nextPage, streams, index);
}
@Override
protected String getTag() {
return "ChannelPlayQueue@" + Integer.toHexString(hashCode());
}
@Override
public void fetch() {
if (this.isInitial) {
ExtractorHelper.getChannelInfo(this.serviceId, this.baseUrl, false)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(getHeadListObserver());
} else {
ExtractorHelper.getMoreChannelItems(this.serviceId, this.baseUrl, this.nextPage)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(getNextPageObserver());
}
}
}

View File

@@ -1,53 +0,0 @@
package org.schabi.newpipe.player.playqueue;
import org.schabi.newpipe.extractor.Page;
import org.schabi.newpipe.extractor.channel.tabs.ChannelTabInfo;
import org.schabi.newpipe.extractor.linkhandler.ListLinkHandler;
import org.schabi.newpipe.extractor.stream.StreamInfoItem;
import org.schabi.newpipe.util.ExtractorHelper;
import java.util.Collections;
import java.util.List;
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
import io.reactivex.rxjava3.schedulers.Schedulers;
public final class ChannelTabPlayQueue extends AbstractInfoPlayQueue<ChannelTabInfo> {
final ListLinkHandler linkHandler;
public ChannelTabPlayQueue(final int serviceId,
final ListLinkHandler linkHandler,
final Page nextPage,
final List<StreamInfoItem> streams,
final int index) {
super(serviceId, linkHandler.getUrl(), nextPage, streams, index);
this.linkHandler = linkHandler;
}
public ChannelTabPlayQueue(final int serviceId,
final ListLinkHandler linkHandler) {
this(serviceId, linkHandler, null, Collections.emptyList(), 0);
}
@Override
protected String getTag() {
return "ChannelTabPlayQueue@" + Integer.toHexString(hashCode());
}
@Override
public void fetch() {
if (isInitial) {
ExtractorHelper.getChannelTab(this.serviceId, this.linkHandler, false)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(getHeadListObserver());
} else {
ExtractorHelper.getMoreChannelTabItems(this.serviceId, this.linkHandler, this.nextPage)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(getNextPageObserver());
}
}
}

View File

@@ -539,8 +539,7 @@ public abstract class PlayQueue implements Serializable {
public boolean equalStreamsAndIndex(@Nullable final PlayQueue other) {
if (equalStreams(other)) {
//noinspection ConstantConditions
return other.getIndex() == getIndex(); //NOSONAR: other is not null
return other.getIndex() == getIndex();
}
return false;
}

View File

@@ -3,14 +3,12 @@ package org.schabi.newpipe.player.playqueue;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import org.schabi.newpipe.extractor.Image;
import org.schabi.newpipe.extractor.stream.StreamInfo;
import org.schabi.newpipe.extractor.stream.StreamInfoItem;
import org.schabi.newpipe.extractor.stream.StreamType;
import org.schabi.newpipe.util.ExtractorHelper;
import java.io.Serializable;
import java.util.List;
import io.reactivex.rxjava3.core.Single;
import io.reactivex.rxjava3.schedulers.Schedulers;
@@ -26,7 +24,7 @@ public class PlayQueueItem implements Serializable {
private final int serviceId;
private final long duration;
@NonNull
private final List<Image> thumbnails;
private final String thumbnailUrl;
@NonNull
private final String uploader;
private final String uploaderUrl;
@@ -40,7 +38,7 @@ public class PlayQueueItem implements Serializable {
PlayQueueItem(@NonNull final StreamInfo info) {
this(info.getName(), info.getUrl(), info.getServiceId(), info.getDuration(),
info.getThumbnails(), info.getUploaderName(),
info.getThumbnailUrl(), info.getUploaderName(),
info.getUploaderUrl(), info.getStreamType());
if (info.getStartPosition() > 0) {
@@ -50,20 +48,20 @@ public class PlayQueueItem implements Serializable {
PlayQueueItem(@NonNull final StreamInfoItem item) {
this(item.getName(), item.getUrl(), item.getServiceId(), item.getDuration(),
item.getThumbnails(), item.getUploaderName(),
item.getThumbnailUrl(), item.getUploaderName(),
item.getUploaderUrl(), item.getStreamType());
}
@SuppressWarnings("ParameterNumber")
private PlayQueueItem(@Nullable final String name, @Nullable final String url,
final int serviceId, final long duration,
final List<Image> thumbnails, @Nullable final String uploader,
@Nullable final String thumbnailUrl, @Nullable final String uploader,
final String uploaderUrl, @NonNull final StreamType streamType) {
this.title = name != null ? name : EMPTY_STRING;
this.url = url != null ? url : EMPTY_STRING;
this.serviceId = serviceId;
this.duration = duration;
this.thumbnails = thumbnails;
this.thumbnailUrl = thumbnailUrl != null ? thumbnailUrl : EMPTY_STRING;
this.uploader = uploader != null ? uploader : EMPTY_STRING;
this.uploaderUrl = uploaderUrl;
this.streamType = streamType;
@@ -90,8 +88,8 @@ public class PlayQueueItem implements Serializable {
}
@NonNull
public List<Image> getThumbnails() {
return thumbnails;
public String getThumbnailUrl() {
return thumbnailUrl;
}
@NonNull

View File

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

View File

@@ -1,7 +1,5 @@
package org.schabi.newpipe.player.playqueue;
import androidx.annotation.NonNull;
import org.schabi.newpipe.extractor.stream.StreamInfo;
import org.schabi.newpipe.extractor.stream.StreamInfoItem;
@@ -22,11 +20,11 @@ public final class SinglePlayQueue extends PlayQueue {
getItem().setRecoveryPosition(startPosition);
}
public SinglePlayQueue(@NonNull final List<StreamInfoItem> items, final int index) {
public SinglePlayQueue(final List<StreamInfoItem> items, final int index) {
super(index, playQueueItemsOf(items));
}
private static List<PlayQueueItem> playQueueItemsOf(@NonNull final List<StreamInfoItem> items) {
private static List<PlayQueueItem> playQueueItemsOf(final List<StreamInfoItem> items) {
final List<PlayQueueItem> playQueueItems = new ArrayList<>(items.size());
for (final StreamInfoItem item : items) {
playQueueItems.add(new PlayQueueItem(item));
@@ -41,7 +39,5 @@ public final class SinglePlayQueue extends PlayQueue {
@Override
public void fetch() {
// Item was already passed in constructor.
// No further items need to be fetched as this is a PlayQueue with only one item
}
}

View File

@@ -14,7 +14,7 @@ import androidx.collection.SparseArrayCompat;
import com.google.common.base.Stopwatch;
import org.schabi.newpipe.extractor.stream.Frameset;
import org.schabi.newpipe.util.image.PicassoHelper;
import org.schabi.newpipe.util.PicassoHelper;
import java.util.Comparator;
import java.util.List;

View File

@@ -434,7 +434,7 @@ public final class MainPlayerUi extends VideoPlayerUi implements View.OnLayoutCh
return;
}
final boolean showQueue = !playQueue.getStreams().isEmpty();
final boolean showQueue = playQueue.getStreams().size() > 1;
final boolean showSegment = !player.getCurrentStreamInfo()
.map(StreamInfo::getStreamSegments)
.map(List::isEmpty)
@@ -740,7 +740,7 @@ public final class MainPlayerUi extends VideoPlayerUi implements View.OnLayoutCh
String videoUrl = player.getVideoUrl();
videoUrl += ("&t=" + seconds);
ShareUtils.shareText(context, currentItem.getTitle(),
videoUrl, currentItem.getThumbnails());
videoUrl, currentItem.getThumbnailUrl());
}
}
};

View File

@@ -41,7 +41,6 @@ import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.content.res.AppCompatResources;
import androidx.appcompat.view.ContextThemeWrapper;
import androidx.appcompat.widget.AppCompatImageButton;
import androidx.appcompat.widget.PopupMenu;
import androidx.core.graphics.BitmapCompat;
import androidx.core.graphics.Insets;
@@ -104,9 +103,6 @@ public abstract class VideoPlayerUi extends PlayerUi implements SeekBar.OnSeekBa
// other constants (TODO remove playback speeds and use normal menu for popup, too)
private static final float[] PLAYBACK_SPEEDS = {0.5f, 0.75f, 1.0f, 1.25f, 1.5f, 1.75f, 2.0f};
private enum PlayButtonAction {
PLAY, PAUSE, REPLAY
}
/*//////////////////////////////////////////////////////////////////////////
// Views
@@ -226,7 +222,7 @@ public abstract class VideoPlayerUi extends PlayerUi implements SeekBar.OnSeekBa
final PlayQueueItem currentItem = player.getCurrentItem();
if (currentItem != null) {
ShareUtils.shareText(context, currentItem.getTitle(),
player.getVideoUrlAtCurrentTime(), currentItem.getThumbnails());
player.getVideoUrlAtCurrentTime(), currentItem.getThumbnailUrl());
}
}));
binding.share.setOnLongClickListener(v -> {
@@ -759,29 +755,6 @@ public abstract class VideoPlayerUi extends PlayerUi implements SeekBar.OnSeekBa
// only MainPlayerUi can be in fullscreen, so overridden there
return false;
}
/**
* Update the play/pause button ({@link R.id.playPauseButton}) to reflect the action
* that will be performed when the button is clicked..
* @param action the action that is performed when the play/pause button is clicked
*/
private void updatePlayPauseButton(final PlayButtonAction action) {
final AppCompatImageButton button = binding.playPauseButton;
switch (action) {
case PLAY:
button.setContentDescription(context.getString(R.string.play));
button.setImageResource(R.drawable.ic_play_arrow);
break;
case PAUSE:
button.setContentDescription(context.getString(R.string.pause));
button.setImageResource(R.drawable.ic_pause);
break;
case REPLAY:
button.setContentDescription(context.getString(R.string.replay));
button.setImageResource(R.drawable.ic_replay);
break;
}
}
//endregion
@@ -812,7 +785,7 @@ public abstract class VideoPlayerUi extends PlayerUi implements SeekBar.OnSeekBa
animate(binding.loadingPanel, true, 0);
animate(binding.surfaceForeground, true, 100);
updatePlayPauseButton(PlayButtonAction.PLAY);
binding.playPauseButton.setImageResource(R.drawable.ic_play_arrow);
animatePlayButtons(false, 100);
binding.getRoot().setKeepScreenOn(false);
}
@@ -833,7 +806,7 @@ public abstract class VideoPlayerUi extends PlayerUi implements SeekBar.OnSeekBa
animate(binding.playPauseButton, false, 80, AnimationType.SCALE_AND_ALPHA, 0,
() -> {
updatePlayPauseButton(PlayButtonAction.PAUSE);
binding.playPauseButton.setImageResource(R.drawable.ic_pause);
animatePlayButtons(true, 200);
if (!isAnyListViewOpen()) {
binding.playPauseButton.requestFocus();
@@ -863,7 +836,7 @@ public abstract class VideoPlayerUi extends PlayerUi implements SeekBar.OnSeekBa
animate(binding.playPauseButton, false, 80, AnimationType.SCALE_AND_ALPHA, 0,
() -> {
updatePlayPauseButton(PlayButtonAction.PLAY);
binding.playPauseButton.setImageResource(R.drawable.ic_play_arrow);
animatePlayButtons(true, 200);
if (!isAnyListViewOpen()) {
binding.playPauseButton.requestFocus();
@@ -887,7 +860,7 @@ public abstract class VideoPlayerUi extends PlayerUi implements SeekBar.OnSeekBa
animate(binding.playPauseButton, false, 0, AnimationType.SCALE_AND_ALPHA, 0,
() -> {
updatePlayPauseButton(PlayButtonAction.REPLAY);
binding.playPauseButton.setImageResource(R.drawable.ic_replay);
animatePlayButtons(true, DEFAULT_CONTROLS_DURATION);
});

View File

@@ -15,7 +15,6 @@ import android.widget.Toast;
import androidx.activity.result.ActivityResult;
import androidx.activity.result.ActivityResultLauncher;
import androidx.activity.result.contract.ActivityResultContracts.StartActivityForResult;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AlertDialog;
import androidx.core.content.ContextCompat;
import androidx.preference.Preference;
@@ -31,10 +30,8 @@ import org.schabi.newpipe.extractor.localization.Localization;
import org.schabi.newpipe.streams.io.NoFileManagerSafeGuard;
import org.schabi.newpipe.streams.io.StoredFileHelper;
import org.schabi.newpipe.util.NavigationHelper;
import org.schabi.newpipe.util.image.ImageStrategy;
import org.schabi.newpipe.util.image.PicassoHelper;
import org.schabi.newpipe.util.PicassoHelper;
import org.schabi.newpipe.util.ZipHelper;
import org.schabi.newpipe.util.image.PreferredImageQuality;
import java.io.File;
import java.io.IOException;
@@ -107,11 +104,9 @@ public class ContentSettingsFragment extends BasePreferenceFragment {
.getPreferredContentCountry(requireContext());
initialLanguage = defaultPreferences.getString(getString(R.string.app_language_key), "en");
final Preference imageQualityPreference = requirePreference(R.string.image_quality_key);
imageQualityPreference.setOnPreferenceChangeListener(
findPreference(getString(R.string.download_thumbnail_key)).setOnPreferenceChangeListener(
(preference, newValue) -> {
ImageStrategy.setPreferredImageQuality(PreferredImageQuality
.fromPreferenceKey(requireContext(), (String) newValue));
PicassoHelper.setShouldLoadImages((Boolean) newValue);
try {
PicassoHelper.clearCache(preference.getContext());
Toast.makeText(preference.getContext(),
@@ -235,11 +230,8 @@ public class ContentSettingsFragment extends BasePreferenceFragment {
})
.setPositiveButton(R.string.ok, (dialog, which) -> {
dialog.dismiss();
final Context context = requireContext();
final SharedPreferences prefs = PreferenceManager
.getDefaultSharedPreferences(context);
manager.loadSharedPreferences(prefs);
cleanImport(context, prefs);
manager.loadSharedPreferences(PreferenceManager
.getDefaultSharedPreferences(requireContext()));
finishImport(importDataUri);
})
.show();
@@ -251,38 +243,6 @@ public class ContentSettingsFragment extends BasePreferenceFragment {
}
}
/**
* Remove settings that are not supposed to be imported on different devices
* and reset them to default values.
* @param context the context used for the import
* @param prefs the preferences used while running the import
*/
private void cleanImport(@NonNull final Context context,
@NonNull final SharedPreferences prefs) {
// Check if media tunnelling needs to be disabled automatically,
// if it was disabled automatically in the imported preferences.
final String tunnelingKey = context.getString(R.string.disable_media_tunneling_key);
final String automaticTunnelingKey =
context.getString(R.string.disabled_media_tunneling_automatically_key);
// R.string.disable_media_tunneling_key should always be true
// if R.string.disabled_media_tunneling_automatically_key equals 1,
// but we double check here just to be sure and to avoid regressions
// caused by possible later modification of the media tunneling functionality.
// R.string.disabled_media_tunneling_automatically_key == 0:
// automatic value overridden by user in settings
// R.string.disabled_media_tunneling_automatically_key == -1: not set
final boolean wasMediaTunnelingDisabledAutomatically =
prefs.getInt(automaticTunnelingKey, -1) == 1
&& prefs.getBoolean(tunnelingKey, false);
if (wasMediaTunnelingDisabledAutomatically) {
prefs.edit()
.putInt(automaticTunnelingKey, -1)
.putBoolean(tunnelingKey, false)
.apply();
NewPipeSettings.setMediaTunneling(context);
}
}
/**
* Save import path and restart system.
*

View File

@@ -2,7 +2,6 @@ package org.schabi.newpipe.settings
import android.content.SharedPreferences
import android.util.Log
import org.schabi.newpipe.MainActivity.DEBUG
import org.schabi.newpipe.streams.io.SharpOutputStream
import org.schabi.newpipe.streams.io.StoredFileHelper
import org.schabi.newpipe.util.ZipHelper
@@ -33,9 +32,7 @@ class ContentSettingsManager(private val fileLocator: NewPipeFileLocator) {
output.flush()
}
} catch (e: IOException) {
if (DEBUG) {
Log.e(TAG, "Unable to exportDatabase", e)
}
Log.e(TAG, "Unable to exportDatabase", e)
}
ZipHelper.addFileToZip(outZip, fileLocator.settings.path, "newpipe.settings")
@@ -70,9 +67,6 @@ class ContentSettingsManager(private val fileLocator: NewPipeFileLocator) {
return ZipHelper.extractFileFromZip(file, fileLocator.settings.path, "newpipe.settings")
}
/**
* Remove all shared preferences from the app and load the preferences supplied to the manager.
*/
fun loadSharedPreferences(preferences: SharedPreferences) {
try {
val preferenceEditor = preferences.edit()
@@ -108,13 +102,9 @@ class ContentSettingsManager(private val fileLocator: NewPipeFileLocator) {
preferenceEditor.commit()
}
} catch (e: IOException) {
if (DEBUG) {
Log.e(TAG, "Unable to loadSharedPreferences", e)
}
Log.e(TAG, "Unable to loadSharedPreferences", e)
} catch (e: ClassNotFoundException) {
if (DEBUG) {
Log.e(TAG, "Unable to loadSharedPreferences", e)
}
Log.e(TAG, "Unable to loadSharedPreferences", e)
}
}
}

View File

@@ -10,7 +10,7 @@ import org.schabi.newpipe.error.ErrorInfo;
import org.schabi.newpipe.error.ErrorUtil;
import org.schabi.newpipe.error.UserAction;
import org.schabi.newpipe.local.feed.notifications.NotificationWorker;
import org.schabi.newpipe.util.image.PicassoHelper;
import org.schabi.newpipe.util.PicassoHelper;
import java.util.Optional;

View File

@@ -1,14 +1,8 @@
package org.schabi.newpipe.settings;
import android.content.SharedPreferences;
import android.os.Bundle;
import androidx.annotation.Nullable;
import androidx.preference.Preference;
import androidx.preference.PreferenceManager;
import androidx.preference.SwitchPreferenceCompat;
import org.schabi.newpipe.R;
public class ExoPlayerSettingsFragment extends BasePreferenceFragment {
@@ -16,30 +10,5 @@ public class ExoPlayerSettingsFragment extends BasePreferenceFragment {
public void onCreatePreferences(@Nullable final Bundle savedInstanceState,
@Nullable final String rootKey) {
addPreferencesFromResourceRegistry();
final String disabledMediaTunnelingAutomaticallyKey =
getString(R.string.disabled_media_tunneling_automatically_key);
final SwitchPreferenceCompat disableMediaTunnelingPref =
(SwitchPreferenceCompat) requirePreference(R.string.disable_media_tunneling_key);
final SharedPreferences prefs = PreferenceManager
.getDefaultSharedPreferences(requireContext());
final boolean mediaTunnelingAutomaticallyDisabled =
prefs.getInt(disabledMediaTunnelingAutomaticallyKey, -1) == 1;
final String summaryText = getString(R.string.disable_media_tunneling_summary);
disableMediaTunnelingPref.setSummary(mediaTunnelingAutomaticallyDisabled
? summaryText + " " + getString(R.string.disable_media_tunneling_automatic_info)
: summaryText);
disableMediaTunnelingPref.setOnPreferenceChangeListener((Preference p, Object enabled) -> {
if (Boolean.FALSE.equals(enabled)) {
PreferenceManager.getDefaultSharedPreferences(requireContext())
.edit()
.putInt(disabledMediaTunnelingAutomaticallyKey, 0)
.apply();
// the info text might have been shown before
p.setSummary(R.string.disable_media_tunneling_summary);
}
return true;
});
}
}

View File

@@ -1,7 +1,5 @@
package org.schabi.newpipe.settings;
import static org.schabi.newpipe.extractor.utils.Utils.isNullOrEmpty;
import android.content.Context;
import android.content.SharedPreferences;
import android.os.Build;
@@ -17,6 +15,8 @@ import org.schabi.newpipe.util.DeviceUtils;
import java.io.File;
import java.util.Set;
import static org.schabi.newpipe.extractor.utils.Utils.isNullOrEmpty;
/*
* Created by k3b on 07.01.2016.
*
@@ -44,14 +44,24 @@ public final class NewPipeSettings {
private NewPipeSettings() { }
public static void initSettings(final Context context) {
// check if the last used preference version is set
// to determine whether this is the first app run
final int lastUsedPrefVersion = PreferenceManager.getDefaultSharedPreferences(context)
.getInt(context.getString(R.string.last_used_preferences_version), -1);
final boolean isFirstRun = lastUsedPrefVersion == -1;
// check if there are entries in the prefs to determine whether this is the first app run
Boolean isFirstRun = null;
final Set<String> prefsKeys = PreferenceManager.getDefaultSharedPreferences(context)
.getAll().keySet();
for (final String key: prefsKeys) {
// ACRA stores some info in the prefs during app initialization
// which happens before this method is called. Therefore ignore ACRA-related keys.
if (!key.toLowerCase().startsWith("acra")) {
isFirstRun = false;
break;
}
}
if (isFirstRun == null) {
isFirstRun = true;
}
// first run migrations, then setDefaultValues, since the latter requires the correct types
SettingMigrations.runMigrationsIfNeeded(context, isFirstRun);
SettingMigrations.initMigrations(context, isFirstRun);
// readAgain is true so that if new settings are added their default value is set
PreferenceManager.setDefaultValues(context, R.xml.main_settings, true);
@@ -66,8 +76,6 @@ public final class NewPipeSettings {
saveDefaultVideoDownloadDirectory(context);
saveDefaultAudioDownloadDirectory(context);
disableMediaTunnelingIfNecessary(context, isFirstRun);
}
static void saveDefaultVideoDownloadDirectory(final Context context) {
@@ -144,49 +152,4 @@ public final class NewPipeSettings {
return showSearchSuggestions(context, sharedPreferences,
R.string.show_remote_search_suggestions_key);
}
private static void disableMediaTunnelingIfNecessary(@NonNull final Context context,
final boolean isFirstRun) {
final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
final String disabledTunnelingKey = context.getString(R.string.disable_media_tunneling_key);
final String disabledTunnelingAutomaticallyKey =
context.getString(R.string.disabled_media_tunneling_automatically_key);
final String blacklistVersionKey =
context.getString(R.string.media_tunneling_device_blacklist_version);
final int lastMediaTunnelingUpdate = prefs.getInt(blacklistVersionKey, 0);
final boolean wasDeviceBlacklistUpdated =
DeviceUtils.MEDIA_TUNNELING_DEVICE_BLACKLIST_VERSION != lastMediaTunnelingUpdate;
final boolean wasMediaTunnelingEnabledByUser =
prefs.getInt(disabledTunnelingAutomaticallyKey, -1) == 0
&& !prefs.getBoolean(disabledTunnelingKey, false);
if (Boolean.TRUE.equals(isFirstRun)
|| (wasDeviceBlacklistUpdated && !wasMediaTunnelingEnabledByUser)) {
setMediaTunneling(context);
}
}
/**
* Check if device does not support media tunneling
* and disable that exoplayer feature if necessary.
* @see DeviceUtils#shouldSupportMediaTunneling()
* @param context
*/
public static void setMediaTunneling(@NonNull final Context context) {
final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
if (!DeviceUtils.shouldSupportMediaTunneling()) {
prefs.edit()
.putBoolean(context.getString(R.string.disable_media_tunneling_key), true)
.putInt(context.getString(
R.string.disabled_media_tunneling_automatically_key), 1)
.putInt(context.getString(R.string.media_tunneling_device_blacklist_version),
DeviceUtils.MEDIA_TUNNELING_DEVICE_BLACKLIST_VERSION)
.apply();
} else {
prefs.edit()
.putInt(context.getString(R.string.media_tunneling_device_blacklist_version),
DeviceUtils.MEDIA_TUNNELING_DEVICE_BLACKLIST_VERSION).apply();
}
}
}

View File

@@ -19,7 +19,7 @@ import org.schabi.newpipe.R;
import org.schabi.newpipe.database.subscription.SubscriptionEntity;
import org.schabi.newpipe.error.ErrorUtil;
import org.schabi.newpipe.local.subscription.SubscriptionManager;
import org.schabi.newpipe.util.image.PicassoHelper;
import org.schabi.newpipe.util.PicassoHelper;
import org.schabi.newpipe.util.ThemeHelper;
import java.util.List;

View File

@@ -25,7 +25,7 @@ import org.schabi.newpipe.error.ErrorUtil;
import org.schabi.newpipe.error.UserAction;
import org.schabi.newpipe.local.playlist.LocalPlaylistManager;
import org.schabi.newpipe.local.playlist.RemotePlaylistManager;
import org.schabi.newpipe.util.image.PicassoHelper;
import org.schabi.newpipe.util.PicassoHelper;
import java.util.List;
import java.util.Vector;

View File

@@ -4,7 +4,6 @@ import android.content.Context;
import android.content.SharedPreferences;
import android.util.Log;
import androidx.annotation.NonNull;
import androidx.preference.PreferenceManager;
import org.schabi.newpipe.R;
@@ -31,9 +30,9 @@ public final class SettingMigrations {
private static final String TAG = SettingMigrations.class.toString();
private static SharedPreferences sp;
private static final Migration MIGRATION_0_1 = new Migration(0, 1) {
public static final Migration MIGRATION_0_1 = new Migration(0, 1) {
@Override
public void migrate(@NonNull final Context context) {
public void migrate(final Context context) {
// We changed the content of the dialog which opens when sharing a link to NewPipe
// by removing the "open detail page" option.
// Therefore, show the dialog once again to ensure users need to choose again and are
@@ -45,9 +44,9 @@ public final class SettingMigrations {
}
};
private static final Migration MIGRATION_1_2 = new Migration(1, 2) {
public static final Migration MIGRATION_1_2 = new Migration(1, 2) {
@Override
protected void migrate(@NonNull final Context context) {
protected void migrate(final Context context) {
// The new application workflow introduced in #2907 allows minimizing videos
// while playing to do other stuff within the app.
// For an even better workflow, we minimize a stream when switching the app to play in
@@ -64,9 +63,9 @@ public final class SettingMigrations {
}
};
private static final Migration MIGRATION_2_3 = new Migration(2, 3) {
public static final Migration MIGRATION_2_3 = new Migration(2, 3) {
@Override
protected void migrate(@NonNull final Context context) {
protected void migrate(final Context context) {
// Storage Access Framework implementation was improved in #5415, allowing the modern
// and standard way to access folders and files to be used consistently everywhere.
// We reset the setting to its default value, i.e. "use SAF", since now there are no
@@ -80,9 +79,9 @@ public final class SettingMigrations {
}
};
private static final Migration MIGRATION_3_4 = new Migration(3, 4) {
public static final Migration MIGRATION_3_4 = new Migration(3, 4) {
@Override
protected void migrate(@NonNull final Context context) {
protected void migrate(final Context context) {
// Pull request #3546 added support for choosing the type of search suggestions to
// show, replacing the on-off switch used before, so migrate the previous user choice
@@ -109,9 +108,9 @@ public final class SettingMigrations {
}
};
private static final Migration MIGRATION_4_5 = new Migration(4, 5) {
public static final Migration MIGRATION_4_5 = new Migration(4, 5) {
@Override
protected void migrate(@NonNull final Context context) {
protected void migrate(final Context context) {
final boolean brightness = sp.getBoolean("brightness_gesture_control", true);
final boolean volume = sp.getBoolean("volume_gesture_control", true);
@@ -128,20 +127,6 @@ public final class SettingMigrations {
}
};
public static final Migration MIGRATION_5_6 = new Migration(5, 6) {
@Override
protected void migrate(@NonNull final Context context) {
final boolean loadImages = sp.getBoolean("download_thumbnail_key", true);
sp.edit()
.putString(context.getString(R.string.image_quality_key),
context.getString(loadImages
? R.string.image_quality_default
: R.string.image_quality_none_key))
.apply();
}
};
/**
* List of all implemented migrations.
* <p>
@@ -154,17 +139,15 @@ public final class SettingMigrations {
MIGRATION_2_3,
MIGRATION_3_4,
MIGRATION_4_5,
MIGRATION_5_6,
};
/**
* Version number for preferences. Must be incremented every time a migration is necessary.
*/
private static final int VERSION = 6;
public static final int VERSION = 5;
public static void runMigrationsIfNeeded(@NonNull final Context context,
final boolean isFirstRun) {
public static void initMigrations(final Context context, final boolean isFirstRun) {
// setup migrations and check if there is something to do
sp = PreferenceManager.getDefaultSharedPreferences(context);
final String lastPrefVersionKey = context.getString(R.string.last_used_preferences_version);
@@ -229,7 +212,7 @@ public final class SettingMigrations {
return oldVersion >= currentVersion;
}
protected abstract void migrate(@NonNull Context context);
protected abstract void migrate(Context context);
}

View File

@@ -13,7 +13,6 @@ import androidx.preference.ListPreference;
import com.google.android.material.snackbar.Snackbar;
import org.schabi.newpipe.R;
import org.schabi.newpipe.util.ListHelper;
import org.schabi.newpipe.util.PermissionHelper;
import java.util.LinkedList;
@@ -27,7 +26,7 @@ public class VideoAudioSettingsFragment extends BasePreferenceFragment {
addPreferencesFromResourceRegistry();
updateSeekOptions();
updateResolutionOptions();
listener = (sharedPreferences, key) -> {
// on M and above, if user chooses to minimise to popup player on exit
@@ -49,84 +48,10 @@ public class VideoAudioSettingsFragment extends BasePreferenceFragment {
}
} else if (getString(R.string.use_inexact_seek_key).equals(key)) {
updateSeekOptions();
} else if (getString(R.string.show_higher_resolutions_key).equals(key)) {
updateResolutionOptions();
}
};
}
/**
* Update default resolution, default popup resolution & mobile data resolution options.
* <br />
* Show high resolutions when "Show higher resolution" option is enabled.
* Set default resolution to "best resolution" when "Show higher resolution" option
* is disabled.
*/
private void updateResolutionOptions() {
final Resources resources = getResources();
final boolean showHigherResolutions = getPreferenceManager().getSharedPreferences()
.getBoolean(resources.getString(R.string.show_higher_resolutions_key), false);
// get sorted resolution lists
final List<String> resolutionListDescriptions = ListHelper.getSortedResolutionList(
resources,
R.array.resolution_list_description,
R.array.high_resolution_list_descriptions,
showHigherResolutions);
final List<String> resolutionListValues = ListHelper.getSortedResolutionList(
resources,
R.array.resolution_list_values,
R.array.high_resolution_list_values,
showHigherResolutions);
final List<String> limitDataUsageResolutionValues = ListHelper.getSortedResolutionList(
resources,
R.array.limit_data_usage_values_list,
R.array.high_resolution_limit_data_usage_values_list,
showHigherResolutions);
final List<String> limitDataUsageResolutionDescriptions = ListHelper
.getSortedResolutionList(resources,
R.array.limit_data_usage_description_list,
R.array.high_resolution_list_descriptions,
showHigherResolutions);
// get resolution preferences
final ListPreference defaultResolution = findPreference(
getString(R.string.default_resolution_key));
final ListPreference defaultPopupResolution = findPreference(
getString(R.string.default_popup_resolution_key));
final ListPreference mobileDataResolution = findPreference(
getString(R.string.limit_mobile_data_usage_key));
// update resolution preferences with new resolutions, entries & values for each
defaultResolution.setEntries(resolutionListDescriptions.toArray(new String[0]));
defaultResolution.setEntryValues(resolutionListValues.toArray(new String[0]));
defaultPopupResolution.setEntries(resolutionListDescriptions.toArray(new String[0]));
defaultPopupResolution.setEntryValues(resolutionListValues.toArray(new String[0]));
mobileDataResolution.setEntries(
limitDataUsageResolutionDescriptions.toArray(new String[0]));
mobileDataResolution.setEntryValues(limitDataUsageResolutionValues.toArray(new String[0]));
// if "Show higher resolution" option is disabled,
// set default resolution to "best resolution"
if (!showHigherResolutions) {
if (ListHelper.isHighResolutionSelected(defaultResolution.getValue(),
R.array.high_resolution_list_values,
resources)) {
defaultResolution.setValueIndex(0);
}
if (ListHelper.isHighResolutionSelected(defaultPopupResolution.getValue(),
R.array.high_resolution_list_values,
resources)) {
defaultPopupResolution.setValueIndex(0);
}
if (ListHelper.isHighResolutionSelected(mobileDataResolution.getValue(),
R.array.high_resolution_limit_data_usage_values_list,
resources)) {
mobileDataResolution.setValueIndex(0);
}
}
}
/**
* Update fast-forward/-rewind seek duration options
* according to language and inexact seek setting.

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