Merge pull request #13081 from TeamNewPipe/ktlint

Fix and enable multiple ktlint rules
This commit is contained in:
Aayush Gupta 2026-01-24 00:41:17 +08:00 committed by GitHub
commit 6d2b14fb28
83 changed files with 759 additions and 473 deletions

View file

@ -6,39 +6,13 @@
root = true root = true
[*.{kt,kts}] [*.{kt,kts}]
ktlint_standard_annotation = disabled ktlint_code_style = android_studio
ktlint_standard_argument-list-wrapping = disabled # https://pinterest.github.io/ktlint/latest/rules/standard/#function-naming
ktlint_standard_backing-property-naming = disabled ktlint_function_naming_ignore_when_annotated_with = Composable
ktlint_standard_blank-line-before-declaration = disabled
ktlint_standard_blank-line-between-when-conditions = disabled
ktlint_standard_chain-method-continuation = disabled
ktlint_standard_class-signature = disabled ktlint_standard_class-signature = disabled
ktlint_standard_comment-wrapping = disabled
ktlint_standard_enum-wrapping = disabled
ktlint_standard_function-expression-body = disabled ktlint_standard_function-expression-body = disabled
ktlint_standard_function-literal = disabled
ktlint_standard_function-signature = disabled
ktlint_standard_indent = disabled
ktlint_standard_kdoc = disabled
ktlint_standard_max-line-length = disabled ktlint_standard_max-line-length = disabled
ktlint_standard_mixed-condition-operators = disabled ktlint_standard_mixed-condition-operators = disabled
ktlint_standard_multiline-expression-wrapping = disabled
ktlint_standard_multiline-if-else = disabled
ktlint_standard_no-blank-line-in-list = disabled
ktlint_standard_no-consecutive-comments = disabled
ktlint_standard_no-empty-first-line-in-class-body = disabled
ktlint_standard_no-empty-first-line-in-method-block = disabled
ktlint_standard_no-line-break-after-else = disabled
ktlint_standard_no-semi = disabled
ktlint_standard_no-single-line-block-comment = disabled
ktlint_standard_package-name = disabled ktlint_standard_package-name = disabled
ktlint_standard_parameter-list-wrapping = disabled
ktlint_standard_property-naming = disabled ktlint_standard_property-naming = disabled
ktlint_standard_spacing-between-declarations-with-annotations = disabled
ktlint_standard_spacing-between-declarations-with-comments = disabled
ktlint_standard_statement-wrapping = disabled
ktlint_standard_string-template-indent = disabled
ktlint_standard_trailing-comma-on-call-site = disabled
ktlint_standard_trailing-comma-on-declaration-site = disabled
ktlint_standard_try-catch-finally-spacing = disabled
ktlint_standard_when-entry-bracing = disabled

View file

@ -176,28 +176,32 @@ class DatabaseMigrationTest {
databaseInV7.run { databaseInV7.run {
insert( insert(
"search_history", SQLiteDatabase.CONFLICT_FAIL, "search_history",
SQLiteDatabase.CONFLICT_FAIL,
ContentValues().apply { ContentValues().apply {
put("service_id", serviceId) put("service_id", serviceId)
put("search", defaultSearch1) put("search", defaultSearch1)
} }
) )
insert( insert(
"search_history", SQLiteDatabase.CONFLICT_FAIL, "search_history",
SQLiteDatabase.CONFLICT_FAIL,
ContentValues().apply { ContentValues().apply {
put("service_id", serviceId) put("service_id", serviceId)
put("search", defaultSearch2) put("search", defaultSearch2)
} }
) )
insert( insert(
"search_history", SQLiteDatabase.CONFLICT_FAIL, "search_history",
SQLiteDatabase.CONFLICT_FAIL,
ContentValues().apply { ContentValues().apply {
put("service_id", otherServiceId) put("service_id", otherServiceId)
put("search", defaultSearch1) put("search", defaultSearch1)
} }
) )
insert( insert(
"search_history", SQLiteDatabase.CONFLICT_FAIL, "search_history",
SQLiteDatabase.CONFLICT_FAIL,
ContentValues().apply { ContentValues().apply {
put("service_id", otherServiceId) put("service_id", otherServiceId)
put("search", defaultSearch2) put("search", defaultSearch2)
@ -207,13 +211,17 @@ class DatabaseMigrationTest {
} }
testHelper.runMigrationsAndValidate( testHelper.runMigrationsAndValidate(
AppDatabase.DATABASE_NAME, Migrations.DB_VER_8, AppDatabase.DATABASE_NAME,
true, Migrations.MIGRATION_7_8 Migrations.DB_VER_8,
true,
Migrations.MIGRATION_7_8
) )
testHelper.runMigrationsAndValidate( testHelper.runMigrationsAndValidate(
AppDatabase.DATABASE_NAME, Migrations.DB_VER_9, AppDatabase.DATABASE_NAME,
true, Migrations.MIGRATION_8_9 Migrations.DB_VER_9,
true,
Migrations.MIGRATION_8_9
) )
val migratedDatabaseV8 = getMigratedDatabase() val migratedDatabaseV8 = getMigratedDatabase()
@ -235,7 +243,8 @@ class DatabaseMigrationTest {
val remoteUid2: Long val remoteUid2: Long
databaseInV8.run { databaseInV8.run {
localUid1 = insert( localUid1 = insert(
"playlists", SQLiteDatabase.CONFLICT_FAIL, "playlists",
SQLiteDatabase.CONFLICT_FAIL,
ContentValues().apply { ContentValues().apply {
put("name", DEFAULT_NAME + "1") put("name", DEFAULT_NAME + "1")
put("is_thumbnail_permanent", false) put("is_thumbnail_permanent", false)
@ -243,7 +252,8 @@ class DatabaseMigrationTest {
} }
) )
localUid2 = insert( localUid2 = insert(
"playlists", SQLiteDatabase.CONFLICT_FAIL, "playlists",
SQLiteDatabase.CONFLICT_FAIL,
ContentValues().apply { ContentValues().apply {
put("name", DEFAULT_NAME + "2") put("name", DEFAULT_NAME + "2")
put("is_thumbnail_permanent", false) put("is_thumbnail_permanent", false)
@ -251,25 +261,29 @@ class DatabaseMigrationTest {
} }
) )
delete( delete(
"playlists", "uid = ?", "playlists",
"uid = ?",
Array(1) { localUid1 } Array(1) { localUid1 }
) )
remoteUid1 = insert( remoteUid1 = insert(
"remote_playlists", SQLiteDatabase.CONFLICT_FAIL, "remote_playlists",
SQLiteDatabase.CONFLICT_FAIL,
ContentValues().apply { ContentValues().apply {
put("service_id", DEFAULT_SERVICE_ID) put("service_id", DEFAULT_SERVICE_ID)
put("url", DEFAULT_URL) put("url", DEFAULT_URL)
} }
) )
remoteUid2 = insert( remoteUid2 = insert(
"remote_playlists", SQLiteDatabase.CONFLICT_FAIL, "remote_playlists",
SQLiteDatabase.CONFLICT_FAIL,
ContentValues().apply { ContentValues().apply {
put("service_id", DEFAULT_SECOND_SERVICE_ID) put("service_id", DEFAULT_SECOND_SERVICE_ID)
put("url", DEFAULT_SECOND_URL) put("url", DEFAULT_SECOND_URL)
} }
) )
delete( delete(
"remote_playlists", "uid = ?", "remote_playlists",
"uid = ?",
Array(1) { remoteUid2 } Array(1) { remoteUid2 }
) )
close() close()

View file

@ -4,6 +4,8 @@ import android.content.Context
import androidx.room.Room import androidx.room.Room
import androidx.test.core.app.ApplicationProvider import androidx.test.core.app.ApplicationProvider
import io.reactivex.rxjava3.core.Single import io.reactivex.rxjava3.core.Single
import java.io.IOException
import java.time.OffsetDateTime
import org.junit.After import org.junit.After
import org.junit.Assert.assertEquals import org.junit.Assert.assertEquals
import org.junit.Assert.assertNotNull import org.junit.Assert.assertNotNull
@ -20,8 +22,6 @@ import org.schabi.newpipe.database.subscription.SubscriptionEntity
import org.schabi.newpipe.extractor.ServiceList import org.schabi.newpipe.extractor.ServiceList
import org.schabi.newpipe.extractor.channel.ChannelInfo import org.schabi.newpipe.extractor.channel.ChannelInfo
import org.schabi.newpipe.extractor.stream.StreamType import org.schabi.newpipe.extractor.stream.StreamType
import java.io.IOException
import java.time.OffsetDateTime
class FeedDAOTest { class FeedDAOTest {
private lateinit var db: AppDatabase private lateinit var db: AppDatabase
@ -40,14 +40,21 @@ class FeedDAOTest {
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 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( private val allStreams = listOf(
stream1, stream2, stream3, stream4, stream5, stream6, stream7 stream1,
stream2,
stream3,
stream4,
stream5,
stream6,
stream7
) )
@Before @Before
fun createDb() { fun createDb() {
val context = ApplicationProvider.getApplicationContext<Context>() val context = ApplicationProvider.getApplicationContext<Context>()
db = Room.inMemoryDatabaseBuilder( db = Room.inMemoryDatabaseBuilder(
context, AppDatabase::class.java context,
AppDatabase::class.java
).build() ).build()
feedDAO = db.feedDAO() feedDAO = db.feedDAO()
streamDAO = db.streamDAO() streamDAO = db.streamDAO()
@ -64,7 +71,10 @@ class FeedDAOTest {
fun testUnlinkStreamsOlderThan_KeepOne() { fun testUnlinkStreamsOlderThan_KeepOne() {
setupUnlinkDelete("2023-08-15T00:00:00Z") setupUnlinkDelete("2023-08-15T00:00:00Z")
val streams = feedDAO.getStreams( val streams = feedDAO.getStreams(
FeedGroupEntity.GROUP_ALL_ID, includePlayed = true, includePartiallyPlayed = true, null FeedGroupEntity.GROUP_ALL_ID,
includePlayed = true,
includePartiallyPlayed = true,
null
) )
.blockingGet() .blockingGet()
val allowedStreams = listOf(stream3, stream5, stream6, stream7) val allowedStreams = listOf(stream3, stream5, stream6, stream7)
@ -75,7 +85,10 @@ class FeedDAOTest {
fun testUnlinkStreamsOlderThan_KeepMultiple() { fun testUnlinkStreamsOlderThan_KeepMultiple() {
setupUnlinkDelete("2023-08-01T00:00:00Z") setupUnlinkDelete("2023-08-01T00:00:00Z")
val streams = feedDAO.getStreams( val streams = feedDAO.getStreams(
FeedGroupEntity.GROUP_ALL_ID, includePlayed = true, includePartiallyPlayed = true, null FeedGroupEntity.GROUP_ALL_ID,
includePlayed = true,
includePartiallyPlayed = true,
null
) )
.blockingGet() .blockingGet()
val allowedStreams = listOf(stream3, stream4, stream5, stream6, stream7) val allowedStreams = listOf(stream3, stream4, stream5, stream6, stream7)
@ -111,7 +124,7 @@ class FeedDAOTest {
SubscriptionEntity.from(ChannelInfo(serviceId, "1", "https://youtube.com/channel/1", "https://youtube.com/channel/1", "channel-1")), 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, "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, "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")), SubscriptionEntity.from(ChannelInfo(serviceId, "4", "https://youtube.com/channel/4", "https://youtube.com/channel/4", "channel-4"))
) )
) )
feedDAO.insertAll( feedDAO.insertAll(
@ -122,7 +135,7 @@ class FeedDAOTest {
FeedEntity(4, 2), FeedEntity(4, 2),
FeedEntity(5, 2), FeedEntity(5, 2),
FeedEntity(6, 3), FeedEntity(6, 3),
FeedEntity(7, 4), FeedEntity(7, 4)
) )
) )
} }

View file

@ -1,6 +1,9 @@
package org.schabi.newpipe.local.history package org.schabi.newpipe.local.history
import androidx.test.core.app.ApplicationProvider import androidx.test.core.app.ApplicationProvider
import java.time.LocalDateTime
import java.time.OffsetDateTime
import java.time.ZoneOffset
import org.assertj.core.api.Assertions.assertThat import org.assertj.core.api.Assertions.assertThat
import org.junit.After import org.junit.After
import org.junit.Assert.assertEquals import org.junit.Assert.assertEquals
@ -11,9 +14,6 @@ import org.schabi.newpipe.database.AppDatabase
import org.schabi.newpipe.database.history.model.SearchHistoryEntry import org.schabi.newpipe.database.history.model.SearchHistoryEntry
import org.schabi.newpipe.testUtil.TestDatabase import org.schabi.newpipe.testUtil.TestDatabase
import org.schabi.newpipe.testUtil.TrampolineSchedulerRule import org.schabi.newpipe.testUtil.TrampolineSchedulerRule
import java.time.LocalDateTime
import java.time.OffsetDateTime
import java.time.ZoneOffset
class HistoryRecordManagerTest { class HistoryRecordManagerTest {
@ -54,7 +54,7 @@ class HistoryRecordManagerTest {
SearchHistoryEntry(creationDate = time.minusSeconds(1), serviceId = 0, search = "A"), SearchHistoryEntry(creationDate = time.minusSeconds(1), serviceId = 0, search = "A"),
SearchHistoryEntry(creationDate = time.minusSeconds(2), serviceId = 2, search = "A"), SearchHistoryEntry(creationDate = time.minusSeconds(2), serviceId = 2, search = "A"),
SearchHistoryEntry(creationDate = time.minusSeconds(3), serviceId = 1, search = "B"), SearchHistoryEntry(creationDate = time.minusSeconds(3), serviceId = 1, search = "B"),
SearchHistoryEntry(creationDate = time.minusSeconds(4), serviceId = 0, search = "B"), SearchHistoryEntry(creationDate = time.minusSeconds(4), serviceId = 0, search = "B")
) )
// make sure all 4 were inserted // make sure all 4 were inserted
@ -85,7 +85,7 @@ class HistoryRecordManagerTest {
val entries = listOf( val entries = listOf(
SearchHistoryEntry(creationDate = time.minusSeconds(1), serviceId = 1, search = "A"), SearchHistoryEntry(creationDate = time.minusSeconds(1), serviceId = 1, search = "A"),
SearchHistoryEntry(creationDate = time.minusSeconds(2), serviceId = 2, search = "B"), SearchHistoryEntry(creationDate = time.minusSeconds(2), serviceId = 2, search = "B"),
SearchHistoryEntry(creationDate = time.minusSeconds(3), serviceId = 0, search = "C"), SearchHistoryEntry(creationDate = time.minusSeconds(3), serviceId = 0, search = "C")
) )
// make sure all 3 were inserted // make sure all 3 were inserted
@ -98,7 +98,6 @@ class HistoryRecordManagerTest {
} }
private fun insertShuffledRelatedSearches(relatedSearches: Collection<SearchHistoryEntry>) { private fun insertShuffledRelatedSearches(relatedSearches: Collection<SearchHistoryEntry>) {
// shuffle to make sure the order of items returned by queries depends only on // shuffle to make sure the order of items returned by queries depends only on
// SearchHistoryEntry.creationDate, not on the actual insertion time, so that we can // SearchHistoryEntry.creationDate, not on the actual insertion time, so that we can
// verify that the `ORDER BY` clause does its job // verify that the `ORDER BY` clause does its job
@ -121,7 +120,7 @@ class HistoryRecordManagerTest {
RELATED_SEARCHES_ENTRIES[6].search, // A (even if in two places) RELATED_SEARCHES_ENTRIES[6].search, // A (even if in two places)
RELATED_SEARCHES_ENTRIES[4].search, // B RELATED_SEARCHES_ENTRIES[4].search, // B
RELATED_SEARCHES_ENTRIES[5].search, // AA RELATED_SEARCHES_ENTRIES[5].search, // AA
RELATED_SEARCHES_ENTRIES[2].search, // BA RELATED_SEARCHES_ENTRIES[2].search // BA
) )
} }
@ -136,7 +135,7 @@ class HistoryRecordManagerTest {
SearchHistoryEntry(creationDate = time.minusSeconds(4), serviceId = 3, search = "A"), SearchHistoryEntry(creationDate = time.minusSeconds(4), serviceId = 3, search = "A"),
SearchHistoryEntry(creationDate = time.minusSeconds(3), serviceId = 3, search = "A"), SearchHistoryEntry(creationDate = time.minusSeconds(3), serviceId = 3, search = "A"),
SearchHistoryEntry(creationDate = time.minusSeconds(2), serviceId = 0, search = "A"), SearchHistoryEntry(creationDate = time.minusSeconds(2), serviceId = 0, search = "A"),
SearchHistoryEntry(creationDate = time.minusSeconds(1), serviceId = 2, search = "AA"), SearchHistoryEntry(creationDate = time.minusSeconds(1), serviceId = 2, search = "AA")
) )
insertShuffledRelatedSearches(relatedSearches) insertShuffledRelatedSearches(relatedSearches)
@ -153,7 +152,7 @@ class HistoryRecordManagerTest {
assertThat(searches).containsExactly( assertThat(searches).containsExactly(
RELATED_SEARCHES_ENTRIES[6].search, // A (even if in two places) RELATED_SEARCHES_ENTRIES[6].search, // A (even if in two places)
RELATED_SEARCHES_ENTRIES[5].search, // AA RELATED_SEARCHES_ENTRIES[5].search, // AA
RELATED_SEARCHES_ENTRIES[1].search, // BA RELATED_SEARCHES_ENTRIES[1].search // BA
) )
// also make sure that the string comparison is case insensitive // also make sure that the string comparison is case insensitive
@ -171,7 +170,7 @@ class HistoryRecordManagerTest {
SearchHistoryEntry(creationDate = time.minusSeconds(4), serviceId = 3, search = "A"), SearchHistoryEntry(creationDate = time.minusSeconds(4), serviceId = 3, search = "A"),
SearchHistoryEntry(creationDate = time.minusSeconds(2), serviceId = 0, search = "B"), SearchHistoryEntry(creationDate = time.minusSeconds(2), serviceId = 0, search = "B"),
SearchHistoryEntry(creationDate = time.minusSeconds(3), serviceId = 2, search = "AA"), SearchHistoryEntry(creationDate = time.minusSeconds(3), serviceId = 2, search = "AA"),
SearchHistoryEntry(creationDate = time.minusSeconds(1), serviceId = 1, search = "A"), SearchHistoryEntry(creationDate = time.minusSeconds(1), serviceId = 1, search = "A")
) )
} }
} }

View file

@ -33,8 +33,12 @@ class LocalPlaylistManagerTest {
fun createPlaylist() { fun createPlaylist() {
val NEWPIPE_URL = "https://newpipe.net/" val NEWPIPE_URL = "https://newpipe.net/"
val stream = StreamEntity( val stream = StreamEntity(
serviceId = 1, url = NEWPIPE_URL, title = "title", serviceId = 1,
streamType = StreamType.VIDEO_STREAM, duration = 1, uploader = "uploader", url = NEWPIPE_URL,
title = "title",
streamType = StreamType.VIDEO_STREAM,
duration = 1,
uploader = "uploader",
uploaderUrl = NEWPIPE_URL uploaderUrl = NEWPIPE_URL
) )
@ -58,14 +62,22 @@ class LocalPlaylistManagerTest {
@Test() @Test()
fun createPlaylist_nonExistentStreamsAreUpserted() { fun createPlaylist_nonExistentStreamsAreUpserted() {
val stream = StreamEntity( val stream = StreamEntity(
serviceId = 1, url = "https://newpipe.net/", title = "title", serviceId = 1,
streamType = StreamType.VIDEO_STREAM, duration = 1, uploader = "uploader", url = "https://newpipe.net/",
title = "title",
streamType = StreamType.VIDEO_STREAM,
duration = 1,
uploader = "uploader",
uploaderUrl = "https://newpipe.net/" uploaderUrl = "https://newpipe.net/"
) )
database.streamDAO().insert(stream) database.streamDAO().insert(stream)
val upserted = StreamEntity( val upserted = StreamEntity(
serviceId = 1, url = "https://newpipe.net/2", title = "title2", serviceId = 1,
streamType = StreamType.VIDEO_STREAM, duration = 1, uploader = "uploader", url = "https://newpipe.net/2",
title = "title2",
streamType = StreamType.VIDEO_STREAM,
duration = 1,
uploader = "uploader",
uploaderUrl = "https://newpipe.net/" uploaderUrl = "https://newpipe.net/"
) )

View file

@ -17,21 +17,20 @@ class TrampolineSchedulerRule : TestRule {
private val scheduler = Schedulers.trampoline() private val scheduler = Schedulers.trampoline()
override fun apply(base: Statement, description: Description): Statement = override fun apply(base: Statement, description: Description): Statement = object : Statement() {
object : Statement() { override fun evaluate() {
override fun evaluate() { try {
try { RxJavaPlugins.setComputationSchedulerHandler { scheduler }
RxJavaPlugins.setComputationSchedulerHandler { scheduler } RxJavaPlugins.setIoSchedulerHandler { scheduler }
RxJavaPlugins.setIoSchedulerHandler { scheduler } RxJavaPlugins.setNewThreadSchedulerHandler { scheduler }
RxJavaPlugins.setNewThreadSchedulerHandler { scheduler } RxJavaPlugins.setSingleSchedulerHandler { scheduler }
RxJavaPlugins.setSingleSchedulerHandler { scheduler } RxAndroidPlugins.setInitMainThreadSchedulerHandler { scheduler }
RxAndroidPlugins.setInitMainThreadSchedulerHandler { scheduler }
base.evaluate() base.evaluate()
} finally { } finally {
RxJavaPlugins.reset() RxJavaPlugins.reset()
RxAndroidPlugins.reset() RxAndroidPlugins.reset()
}
} }
} }
}
} }

View file

@ -156,41 +156,51 @@ class StreamItemAdapterTest {
helper.assertInvalidResponse(getResponse(mapOf(Pair("content-length", "mp3"))), 0) helper.assertInvalidResponse(getResponse(mapOf(Pair("content-length", "mp3"))), 0)
helper.assertInvalidResponse( helper.assertInvalidResponse(
getResponse(mapOf(Pair("Content-Disposition", "filename=\"train.png\""))), 1 getResponse(mapOf(Pair("Content-Disposition", "filename=\"train.png\""))),
1
) )
helper.assertInvalidResponse( helper.assertInvalidResponse(
getResponse(mapOf(Pair("Content-Disposition", "form-data; name=\"data.csv\""))), 2 getResponse(mapOf(Pair("Content-Disposition", "form-data; name=\"data.csv\""))),
2
) )
helper.assertInvalidResponse( helper.assertInvalidResponse(
getResponse(mapOf(Pair("Content-Disposition", "form-data; filename=\"data.csv\""))), 3 getResponse(mapOf(Pair("Content-Disposition", "form-data; filename=\"data.csv\""))),
3
) )
helper.assertInvalidResponse( helper.assertInvalidResponse(
getResponse(mapOf(Pair("Content-Disposition", "form-data; name=\"fieldName\"; filename*=\"filename.jpg\""))), 4 getResponse(mapOf(Pair("Content-Disposition", "form-data; name=\"fieldName\"; filename*=\"filename.jpg\""))),
4
) )
helper.assertValidResponse( helper.assertValidResponse(
getResponse(mapOf(Pair("Content-Disposition", "filename=\"train.ogg\""))), getResponse(mapOf(Pair("Content-Disposition", "filename=\"train.ogg\""))),
5, MediaFormat.OGG 5,
MediaFormat.OGG
) )
helper.assertValidResponse( helper.assertValidResponse(
getResponse(mapOf(Pair("Content-Disposition", "some-form-data; filename=\"audio.flac\""))), getResponse(mapOf(Pair("Content-Disposition", "some-form-data; filename=\"audio.flac\""))),
6, MediaFormat.FLAC 6,
MediaFormat.FLAC
) )
helper.assertValidResponse( helper.assertValidResponse(
getResponse(mapOf(Pair("Content-Disposition", "form-data; name=\"audio.aiff\"; filename=\"audio.aiff\""))), getResponse(mapOf(Pair("Content-Disposition", "form-data; name=\"audio.aiff\"; filename=\"audio.aiff\""))),
7, MediaFormat.AIFF 7,
MediaFormat.AIFF
) )
helper.assertValidResponse( 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"))), 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 8,
MediaFormat.M4A
) )
helper.assertValidResponse( helper.assertValidResponse(
getResponse(mapOf(Pair("Content-Disposition", "form-data; name=\"audio.mp3\"; filename=\"audio.opus\"; filename*=UTF-8''alien.opus"))), getResponse(mapOf(Pair("Content-Disposition", "form-data; name=\"audio.mp3\"; filename=\"audio.opus\"; filename*=UTF-8''alien.opus"))),
9, MediaFormat.OPUS 9,
MediaFormat.OPUS
) )
helper.assertValidResponse( helper.assertValidResponse(
getResponse(mapOf(Pair("Content-Disposition", "form-data; name=\"audio.mp3\"; filename=\"audio.opus\"; filename*=\"UTF-8''alien.opus\""))), getResponse(mapOf(Pair("Content-Disposition", "form-data; name=\"audio.mp3\"; filename=\"audio.opus\"; filename*=\"UTF-8''alien.opus\""))),
10, MediaFormat.OPUS 10,
MediaFormat.OPUS
) )
} }
@ -213,16 +223,24 @@ class StreamItemAdapterTest {
helper.assertInvalidResponse(getResponse(mapOf()), 7) helper.assertInvalidResponse(getResponse(mapOf()), 7)
helper.assertValidResponse( helper.assertValidResponse(
getResponse(mapOf(Pair("Content-Type", "audio/flac"))), 8, MediaFormat.FLAC getResponse(mapOf(Pair("Content-Type", "audio/flac"))),
8,
MediaFormat.FLAC
) )
helper.assertValidResponse( helper.assertValidResponse(
getResponse(mapOf(Pair("Content-Type", "audio/wav"))), 9, MediaFormat.WAV getResponse(mapOf(Pair("Content-Type", "audio/wav"))),
9,
MediaFormat.WAV
) )
helper.assertValidResponse( helper.assertValidResponse(
getResponse(mapOf(Pair("Content-Type", "audio/opus"))), 10, MediaFormat.OPUS getResponse(mapOf(Pair("Content-Type", "audio/opus"))),
10,
MediaFormat.OPUS
) )
helper.assertValidResponse( helper.assertValidResponse(
getResponse(mapOf(Pair("Content-Type", "audio/aiff"))), 11, MediaFormat.AIFF getResponse(mapOf(Pair("Content-Type", "audio/aiff"))),
11,
MediaFormat.AIFF
) )
} }
@ -230,39 +248,37 @@ class StreamItemAdapterTest {
* @return a list of video streams, in which their video only property mirrors the provided * @return a list of video streams, in which their video only property mirrors the provided
* [videoOnly] vararg. * [videoOnly] vararg.
*/ */
private fun getVideoStreams(vararg videoOnly: Boolean) = private fun getVideoStreams(vararg videoOnly: Boolean) = StreamInfoWrapper(
StreamItemAdapter.StreamInfoWrapper( videoOnly.map {
videoOnly.map { VideoStream.Builder()
VideoStream.Builder() .setId(Stream.ID_UNKNOWN)
.setId(Stream.ID_UNKNOWN) .setContent("https://example.com", true)
.setContent("https://example.com", true) .setMediaFormat(MediaFormat.MPEG_4)
.setMediaFormat(MediaFormat.MPEG_4) .setResolution("720p")
.setResolution("720p") .setIsVideoOnly(it)
.setIsVideoOnly(it) .build()
.build() },
}, context
context )
)
/** /**
* @return a list of audio streams, containing valid and null elements mirroring the provided * @return a list of audio streams, containing valid and null elements mirroring the provided
* [shouldBeValid] vararg. * [shouldBeValid] vararg.
*/ */
private fun getAudioStreams(vararg shouldBeValid: Boolean) = private fun getAudioStreams(vararg shouldBeValid: Boolean) = getSecondaryStreamsFromList(
getSecondaryStreamsFromList( shouldBeValid.map {
shouldBeValid.map { if (it) {
if (it) { AudioStream.Builder()
AudioStream.Builder() .setId(Stream.ID_UNKNOWN)
.setId(Stream.ID_UNKNOWN) .setContent("https://example.com", true)
.setContent("https://example.com", true) .setMediaFormat(MediaFormat.OPUS)
.setMediaFormat(MediaFormat.OPUS) .setAverageBitrate(192)
.setAverageBitrate(192) .build()
.build() } else {
} else { null
null
}
} }
) }
)
private fun getIncompleteAudioStreams(size: Int): List<AudioStream> { private fun getIncompleteAudioStreams(size: Int): List<AudioStream> {
val list = ArrayList<AudioStream>(size) val list = ArrayList<AudioStream>(size)
@ -292,7 +308,7 @@ class StreamItemAdapterTest {
Assert.assertEquals( Assert.assertEquals(
"normal visibility (pos=[$position]) is not correct", "normal visibility (pos=[$position]) is not correct",
findViewById<View>(R.id.wo_sound_icon).visibility, findViewById<View>(R.id.wo_sound_icon).visibility,
normalVisibility, normalVisibility
) )
} }
spinner.adapter.getDropDownView(position, null, spinner).run { spinner.adapter.getDropDownView(position, null, spinner).run {
@ -307,18 +323,17 @@ class StreamItemAdapterTest {
/** /**
* Helper function that builds a secondary stream list. * Helper function that builds a secondary stream list.
*/ */
private fun <T : Stream> getSecondaryStreamsFromList(streams: List<T?>) = private fun <T : Stream> getSecondaryStreamsFromList(streams: List<T?>) = SparseArrayCompat<SecondaryStreamHelper<T>?>(streams.size).apply {
SparseArrayCompat<SecondaryStreamHelper<T>?>(streams.size).apply { streams.forEachIndexed { index, stream ->
streams.forEachIndexed { index, stream -> val secondaryStreamHelper: SecondaryStreamHelper<T>? = stream?.let {
val secondaryStreamHelper: SecondaryStreamHelper<T>? = stream?.let { SecondaryStreamHelper(
SecondaryStreamHelper( StreamItemAdapter.StreamInfoWrapper(streams, context),
StreamItemAdapter.StreamInfoWrapper(streams, context), it
it )
)
}
put(index, secondaryStreamHelper)
} }
put(index, secondaryStreamHelper)
} }
}
private fun getResponse(headers: Map<String, String>): Response { private fun getResponse(headers: Map<String, String>): Response {
val listHeaders = HashMap<String, List<String>>() val listHeaders = HashMap<String, List<String>>()
@ -345,7 +360,8 @@ class StreamItemAdapterTest {
index: Int index: Int
) { ) {
assertFalse( assertFalse(
"invalid header returns valid value", retrieveMediaFormat(streams[index], response) "invalid header returns valid value",
retrieveMediaFormat(streams[index], response)
) )
assertNull("Media format extracted although stated otherwise", wrapper.getFormat(index)) assertNull("Media format extracted although stated otherwise", wrapper.getFormat(index))
} }
@ -359,7 +375,8 @@ class StreamItemAdapterTest {
format: MediaFormat format: MediaFormat
) { ) {
assertTrue( assertTrue(
"header was not recognized", retrieveMediaFormat(streams[index], response) "header was not recognized",
retrieveMediaFormat(streams[index], response)
) )
assertEquals("Wrong media format extracted", format, wrapper.getFormat(index)) assertEquals("Wrong media format extracted", format, wrapper.getFormat(index))
} }

View file

@ -8,6 +8,7 @@ package org.schabi.newpipe
import android.content.Context import android.content.Context
import androidx.room.Room.databaseBuilder import androidx.room.Room.databaseBuilder
import kotlin.concurrent.Volatile
import org.schabi.newpipe.database.AppDatabase import org.schabi.newpipe.database.AppDatabase
import org.schabi.newpipe.database.Migrations.MIGRATION_1_2 import org.schabi.newpipe.database.Migrations.MIGRATION_1_2
import org.schabi.newpipe.database.Migrations.MIGRATION_2_3 import org.schabi.newpipe.database.Migrations.MIGRATION_2_3
@ -17,7 +18,6 @@ import org.schabi.newpipe.database.Migrations.MIGRATION_5_6
import org.schabi.newpipe.database.Migrations.MIGRATION_6_7 import org.schabi.newpipe.database.Migrations.MIGRATION_6_7
import org.schabi.newpipe.database.Migrations.MIGRATION_7_8 import org.schabi.newpipe.database.Migrations.MIGRATION_7_8
import org.schabi.newpipe.database.Migrations.MIGRATION_8_9 import org.schabi.newpipe.database.Migrations.MIGRATION_8_9
import kotlin.concurrent.Volatile
object NewPipeDatabase { object NewPipeDatabase {

View file

@ -18,10 +18,10 @@ import androidx.work.WorkerParameters
import androidx.work.workDataOf import androidx.work.workDataOf
import com.grack.nanojson.JsonParser import com.grack.nanojson.JsonParser
import com.grack.nanojson.JsonParserException import com.grack.nanojson.JsonParserException
import java.io.IOException
import org.schabi.newpipe.extractor.downloader.Response import org.schabi.newpipe.extractor.downloader.Response
import org.schabi.newpipe.extractor.exceptions.ReCaptchaException import org.schabi.newpipe.extractor.exceptions.ReCaptchaException
import org.schabi.newpipe.util.ReleaseVersionUtil import org.schabi.newpipe.util.ReleaseVersionUtil
import java.io.IOException
class NewVersionWorker( class NewVersionWorker(
context: Context, context: Context,
@ -46,7 +46,8 @@ class NewVersionWorker(
// Show toast stating that the app is up-to-date if the update check was manual. // Show toast stating that the app is up-to-date if the update check was manual.
ContextCompat.getMainExecutor(applicationContext).execute { ContextCompat.getMainExecutor(applicationContext).execute {
Toast.makeText( Toast.makeText(
applicationContext, R.string.app_update_unavailable_toast, applicationContext,
R.string.app_update_unavailable_toast,
Toast.LENGTH_SHORT Toast.LENGTH_SHORT
).show() ).show()
} }
@ -58,7 +59,11 @@ class NewVersionWorker(
val intent = Intent(Intent.ACTION_VIEW, apkLocationUrl?.toUri()) val intent = Intent(Intent.ACTION_VIEW, apkLocationUrl?.toUri())
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
val pendingIntent = PendingIntentCompat.getActivity( val pendingIntent = PendingIntentCompat.getActivity(
applicationContext, 0, intent, 0, false applicationContext,
0,
intent,
0,
false
) )
val channelId = applicationContext.getString(R.string.app_update_notification_channel_id) val channelId = applicationContext.getString(R.string.app_update_notification_channel_id)
val notificationBuilder = NotificationCompat.Builder(applicationContext, channelId) val notificationBuilder = NotificationCompat.Builder(applicationContext, channelId)
@ -71,7 +76,8 @@ class NewVersionWorker(
) )
.setContentText( .setContentText(
applicationContext.getString( applicationContext.getString(
R.string.app_update_available_notification_text, versionName R.string.app_update_available_notification_text,
versionName
) )
) )

View file

@ -116,86 +116,145 @@ class AboutActivity : AppCompatActivity() {
*/ */
private val SOFTWARE_COMPONENTS = arrayListOf( private val SOFTWARE_COMPONENTS = arrayListOf(
SoftwareComponent( SoftwareComponent(
"ACRA", "2013", "Kevin Gaudin", "ACRA",
"https://github.com/ACRA/acra", StandardLicenses.APACHE2 "2013",
"Kevin Gaudin",
"https://github.com/ACRA/acra",
StandardLicenses.APACHE2
), ),
SoftwareComponent( SoftwareComponent(
"AndroidX", "2005 - 2011", "The Android Open Source Project", "AndroidX",
"https://developer.android.com/jetpack", StandardLicenses.APACHE2 "2005 - 2011",
"The Android Open Source Project",
"https://developer.android.com/jetpack",
StandardLicenses.APACHE2
), ),
SoftwareComponent( SoftwareComponent(
"ExoPlayer", "2014 - 2020", "Google, Inc.", "ExoPlayer",
"https://github.com/google/ExoPlayer", StandardLicenses.APACHE2 "2014 - 2020",
"Google, Inc.",
"https://github.com/google/ExoPlayer",
StandardLicenses.APACHE2
), ),
SoftwareComponent( SoftwareComponent(
"GigaGet", "2014 - 2015", "Peter Cai", "GigaGet",
"https://github.com/PaperAirplane-Dev-Team/GigaGet", StandardLicenses.GPL3 "2014 - 2015",
"Peter Cai",
"https://github.com/PaperAirplane-Dev-Team/GigaGet",
StandardLicenses.GPL3
), ),
SoftwareComponent( SoftwareComponent(
"Groupie", "2016", "Lisa Wray", "Groupie",
"https://github.com/lisawray/groupie", StandardLicenses.MIT "2016",
"Lisa Wray",
"https://github.com/lisawray/groupie",
StandardLicenses.MIT
), ),
SoftwareComponent( SoftwareComponent(
"Android-State", "2018", "Evernote", "Android-State",
"https://github.com/Evernote/android-state", StandardLicenses.EPL1 "2018",
"Evernote",
"https://github.com/Evernote/android-state",
StandardLicenses.EPL1
), ),
SoftwareComponent( SoftwareComponent(
"Bridge", "2021", "Livefront", "Bridge",
"https://github.com/livefront/bridge", StandardLicenses.APACHE2 "2021",
"Livefront",
"https://github.com/livefront/bridge",
StandardLicenses.APACHE2
), ),
SoftwareComponent( SoftwareComponent(
"Jsoup", "2009 - 2020", "Jonathan Hedley", "Jsoup",
"https://github.com/jhy/jsoup", StandardLicenses.MIT "2009 - 2020",
"Jonathan Hedley",
"https://github.com/jhy/jsoup",
StandardLicenses.MIT
), ),
SoftwareComponent( SoftwareComponent(
"Markwon", "2019", "Dimitry Ivanov", "Markwon",
"https://github.com/noties/Markwon", StandardLicenses.APACHE2 "2019",
"Dimitry Ivanov",
"https://github.com/noties/Markwon",
StandardLicenses.APACHE2
), ),
SoftwareComponent( SoftwareComponent(
"Material Components for Android", "2016 - 2020", "Google, Inc.", "Material Components for Android",
"2016 - 2020",
"Google, Inc.",
"https://github.com/material-components/material-components-android", "https://github.com/material-components/material-components-android",
StandardLicenses.APACHE2 StandardLicenses.APACHE2
), ),
SoftwareComponent( SoftwareComponent(
"NewPipe Extractor", "2017 - 2020", "Christian Schabesberger", "NewPipe Extractor",
"https://github.com/TeamNewPipe/NewPipeExtractor", StandardLicenses.GPL3 "2017 - 2020",
"Christian Schabesberger",
"https://github.com/TeamNewPipe/NewPipeExtractor",
StandardLicenses.GPL3
), ),
SoftwareComponent( SoftwareComponent(
"NoNonsense-FilePicker", "2016", "Jonas Kalderstam", "NoNonsense-FilePicker",
"https://github.com/spacecowboy/NoNonsense-FilePicker", StandardLicenses.MPL2 "2016",
"Jonas Kalderstam",
"https://github.com/spacecowboy/NoNonsense-FilePicker",
StandardLicenses.MPL2
), ),
SoftwareComponent( SoftwareComponent(
"OkHttp", "2019", "Square, Inc.", "OkHttp",
"https://square.github.io/okhttp/", StandardLicenses.APACHE2 "2019",
"Square, Inc.",
"https://square.github.io/okhttp/",
StandardLicenses.APACHE2
), ),
SoftwareComponent( SoftwareComponent(
"Picasso", "2013", "Square, Inc.", "Picasso",
"https://square.github.io/picasso/", StandardLicenses.APACHE2 "2013",
"Square, Inc.",
"https://square.github.io/picasso/",
StandardLicenses.APACHE2
), ),
SoftwareComponent( SoftwareComponent(
"PrettyTime", "2012 - 2020", "Lincoln Baxter, III", "PrettyTime",
"https://github.com/ocpsoft/prettytime", StandardLicenses.APACHE2 "2012 - 2020",
"Lincoln Baxter, III",
"https://github.com/ocpsoft/prettytime",
StandardLicenses.APACHE2
), ),
SoftwareComponent( SoftwareComponent(
"ProcessPhoenix", "2015", "Jake Wharton", "ProcessPhoenix",
"https://github.com/JakeWharton/ProcessPhoenix", StandardLicenses.APACHE2 "2015",
"Jake Wharton",
"https://github.com/JakeWharton/ProcessPhoenix",
StandardLicenses.APACHE2
), ),
SoftwareComponent( SoftwareComponent(
"RxAndroid", "2015", "The RxAndroid authors", "RxAndroid",
"https://github.com/ReactiveX/RxAndroid", StandardLicenses.APACHE2 "2015",
"The RxAndroid authors",
"https://github.com/ReactiveX/RxAndroid",
StandardLicenses.APACHE2
), ),
SoftwareComponent( SoftwareComponent(
"RxBinding", "2015", "Jake Wharton", "RxBinding",
"https://github.com/JakeWharton/RxBinding", StandardLicenses.APACHE2 "2015",
"Jake Wharton",
"https://github.com/JakeWharton/RxBinding",
StandardLicenses.APACHE2
), ),
SoftwareComponent( SoftwareComponent(
"RxJava", "2016 - 2020", "RxJava Contributors", "RxJava",
"https://github.com/ReactiveX/RxJava", StandardLicenses.APACHE2 "2016 - 2020",
"RxJava Contributors",
"https://github.com/ReactiveX/RxJava",
StandardLicenses.APACHE2
), ),
SoftwareComponent( SoftwareComponent(
"SearchPreference", "2018", "ByteHamster", "SearchPreference",
"https://github.com/ByteHamster/SearchPreference", StandardLicenses.MIT "2018",
), "ByteHamster",
"https://github.com/ByteHamster/SearchPreference",
StandardLicenses.MIT
)
) )
} }
} }

View file

@ -1,8 +1,8 @@
package org.schabi.newpipe.about package org.schabi.newpipe.about
import android.os.Parcelable import android.os.Parcelable
import kotlinx.parcelize.Parcelize
import java.io.Serializable import java.io.Serializable
import kotlinx.parcelize.Parcelize
/** /**
* Class for storing information about a software license. * Class for storing information about a software license.

View file

@ -97,7 +97,8 @@ class LicenseFragment : Fragment() {
.observeOn(AndroidSchedulers.mainThread()) .observeOn(AndroidSchedulers.mainThread())
.subscribe { formattedLicense -> .subscribe { formattedLicense ->
val webViewData = Base64.encodeToString( val webViewData = Base64.encodeToString(
formattedLicense.toByteArray(), Base64.NO_PADDING formattedLicense.toByteArray(),
Base64.NO_PADDING
) )
val webView = WebView(context) val webView = WebView(context)
webView.loadData(webViewData, "text/html; charset=UTF-8", "base64") webView.loadData(webViewData, "text/html; charset=UTF-8", "base64")

View file

@ -1,9 +1,9 @@
package org.schabi.newpipe.about package org.schabi.newpipe.about
import android.content.Context import android.content.Context
import java.io.IOException
import org.schabi.newpipe.R import org.schabi.newpipe.R
import org.schabi.newpipe.util.ThemeHelper import org.schabi.newpipe.util.ThemeHelper
import java.io.IOException
/** /**
* @param context the context to use * @param context the context to use
@ -28,13 +28,16 @@ fun getFormattedLicense(context: Context, license: License): String {
fun getLicenseStylesheet(context: Context): String { fun getLicenseStylesheet(context: Context): String {
val isLightTheme = ThemeHelper.isLightThemeSelected(context) val isLightTheme = ThemeHelper.isLightThemeSelected(context)
val licenseBackgroundColor = getHexRGBColor( val licenseBackgroundColor = getHexRGBColor(
context, if (isLightTheme) R.color.light_license_background_color else R.color.dark_license_background_color context,
if (isLightTheme) R.color.light_license_background_color else R.color.dark_license_background_color
) )
val licenseTextColor = getHexRGBColor( val licenseTextColor = getHexRGBColor(
context, if (isLightTheme) R.color.light_license_text_color else R.color.dark_license_text_color context,
if (isLightTheme) R.color.light_license_text_color else R.color.dark_license_text_color
) )
val youtubePrimaryColor = getHexRGBColor( val youtubePrimaryColor = getHexRGBColor(
context, if (isLightTheme) R.color.light_youtube_primary_color else R.color.dark_youtube_primary_color context,
if (isLightTheme) R.color.light_youtube_primary_color else R.color.dark_youtube_primary_color
) )
return "body{padding:12px 15px;margin:0;background:#$licenseBackgroundColor;color:#$licenseTextColor}" + return "body{padding:12px 15px;margin:0;background:#$licenseBackgroundColor;color:#$licenseTextColor}" +
"a[href]{color:#$youtubePrimaryColor}pre{white-space:pre-wrap}" "a[href]{color:#$youtubePrimaryColor}pre{white-space:pre-wrap}"

View file

@ -1,8 +1,8 @@
package org.schabi.newpipe.about package org.schabi.newpipe.about
import android.os.Parcelable import android.os.Parcelable
import kotlinx.parcelize.Parcelize
import java.io.Serializable import java.io.Serializable
import kotlinx.parcelize.Parcelize
@Parcelize @Parcelize
class SoftwareComponent class SoftwareComponent

View file

@ -1,11 +1,11 @@
package org.schabi.newpipe.database package org.schabi.newpipe.database
import androidx.room.TypeConverter import androidx.room.TypeConverter
import org.schabi.newpipe.extractor.stream.StreamType
import org.schabi.newpipe.local.subscription.FeedGroupIcon
import java.time.Instant import java.time.Instant
import java.time.OffsetDateTime import java.time.OffsetDateTime
import java.time.ZoneOffset import java.time.ZoneOffset
import org.schabi.newpipe.extractor.stream.StreamType
import org.schabi.newpipe.local.subscription.FeedGroupIcon
class Converters { class Converters {
/** /**

View file

@ -14,6 +14,6 @@ interface LocalItem {
PLAYLIST_REMOTE_ITEM, PLAYLIST_REMOTE_ITEM,
PLAYLIST_STREAM_ITEM, PLAYLIST_STREAM_ITEM,
STATISTIC_STREAM_ITEM, STATISTIC_STREAM_ITEM
} }
} }

View file

@ -8,6 +8,7 @@ import androidx.room.Transaction
import androidx.room.Update import androidx.room.Update
import io.reactivex.rxjava3.core.Flowable import io.reactivex.rxjava3.core.Flowable
import io.reactivex.rxjava3.core.Maybe import io.reactivex.rxjava3.core.Maybe
import java.time.OffsetDateTime
import org.schabi.newpipe.database.feed.model.FeedEntity import org.schabi.newpipe.database.feed.model.FeedEntity
import org.schabi.newpipe.database.feed.model.FeedGroupEntity import org.schabi.newpipe.database.feed.model.FeedGroupEntity
import org.schabi.newpipe.database.feed.model.FeedLastUpdatedEntity import org.schabi.newpipe.database.feed.model.FeedLastUpdatedEntity
@ -15,7 +16,6 @@ import org.schabi.newpipe.database.stream.StreamWithState
import org.schabi.newpipe.database.stream.model.StreamStateEntity import org.schabi.newpipe.database.stream.model.StreamStateEntity
import org.schabi.newpipe.database.subscription.NotificationMode import org.schabi.newpipe.database.subscription.NotificationMode
import org.schabi.newpipe.database.subscription.SubscriptionEntity import org.schabi.newpipe.database.subscription.SubscriptionEntity
import java.time.OffsetDateTime
@Dao @Dao
abstract class FeedDAO { abstract class FeedDAO {

View file

@ -19,13 +19,17 @@ import org.schabi.newpipe.database.subscription.SubscriptionEntity
entity = StreamEntity::class, entity = StreamEntity::class,
parentColumns = [StreamEntity.STREAM_ID], parentColumns = [StreamEntity.STREAM_ID],
childColumns = [STREAM_ID], childColumns = [STREAM_ID],
onDelete = ForeignKey.CASCADE, onUpdate = ForeignKey.CASCADE, deferred = true onDelete = ForeignKey.CASCADE,
onUpdate = ForeignKey.CASCADE,
deferred = true
), ),
ForeignKey( ForeignKey(
entity = SubscriptionEntity::class, entity = SubscriptionEntity::class,
parentColumns = [SubscriptionEntity.SUBSCRIPTION_UID], parentColumns = [SubscriptionEntity.SUBSCRIPTION_UID],
childColumns = [SUBSCRIPTION_ID], childColumns = [SUBSCRIPTION_ID],
onDelete = ForeignKey.CASCADE, onUpdate = ForeignKey.CASCADE, deferred = true onDelete = ForeignKey.CASCADE,
onUpdate = ForeignKey.CASCADE,
deferred = true
) )
] ]
) )

View file

@ -18,14 +18,18 @@ import org.schabi.newpipe.database.subscription.SubscriptionEntity
entity = FeedGroupEntity::class, entity = FeedGroupEntity::class,
parentColumns = [FeedGroupEntity.ID], parentColumns = [FeedGroupEntity.ID],
childColumns = [GROUP_ID], childColumns = [GROUP_ID],
onDelete = ForeignKey.CASCADE, onUpdate = ForeignKey.CASCADE, deferred = true onDelete = ForeignKey.CASCADE,
onUpdate = ForeignKey.CASCADE,
deferred = true
), ),
ForeignKey( ForeignKey(
entity = SubscriptionEntity::class, entity = SubscriptionEntity::class,
parentColumns = [SubscriptionEntity.SUBSCRIPTION_UID], parentColumns = [SubscriptionEntity.SUBSCRIPTION_UID],
childColumns = [SUBSCRIPTION_ID], childColumns = [SUBSCRIPTION_ID],
onDelete = ForeignKey.CASCADE, onUpdate = ForeignKey.CASCADE, deferred = true onDelete = ForeignKey.CASCADE,
onUpdate = ForeignKey.CASCADE,
deferred = true
) )
] ]
) )

View file

@ -4,10 +4,10 @@ import androidx.room.ColumnInfo
import androidx.room.Entity import androidx.room.Entity
import androidx.room.ForeignKey import androidx.room.ForeignKey
import androidx.room.PrimaryKey import androidx.room.PrimaryKey
import java.time.OffsetDateTime
import org.schabi.newpipe.database.feed.model.FeedLastUpdatedEntity.Companion.FEED_LAST_UPDATED_TABLE import org.schabi.newpipe.database.feed.model.FeedLastUpdatedEntity.Companion.FEED_LAST_UPDATED_TABLE
import org.schabi.newpipe.database.feed.model.FeedLastUpdatedEntity.Companion.SUBSCRIPTION_ID import org.schabi.newpipe.database.feed.model.FeedLastUpdatedEntity.Companion.SUBSCRIPTION_ID
import org.schabi.newpipe.database.subscription.SubscriptionEntity import org.schabi.newpipe.database.subscription.SubscriptionEntity
import java.time.OffsetDateTime
@Entity( @Entity(
tableName = FEED_LAST_UPDATED_TABLE, tableName = FEED_LAST_UPDATED_TABLE,
@ -16,7 +16,9 @@ import java.time.OffsetDateTime
entity = SubscriptionEntity::class, entity = SubscriptionEntity::class,
parentColumns = [SubscriptionEntity.SUBSCRIPTION_UID], parentColumns = [SubscriptionEntity.SUBSCRIPTION_UID],
childColumns = [SUBSCRIPTION_ID], childColumns = [SUBSCRIPTION_ID],
onDelete = ForeignKey.CASCADE, onUpdate = ForeignKey.CASCADE, deferred = true onDelete = ForeignKey.CASCADE,
onUpdate = ForeignKey.CASCADE,
deferred = true
) )
] ]
) )

View file

@ -29,7 +29,7 @@ data class SearchHistoryEntry @JvmOverloads constructor(
@ColumnInfo(name = ID) @ColumnInfo(name = ID)
@PrimaryKey(autoGenerate = true) @PrimaryKey(autoGenerate = true)
val id: Long = 0, val id: Long = 0
) { ) {
@Ignore @Ignore

View file

@ -11,12 +11,12 @@ import androidx.room.Entity
import androidx.room.ForeignKey import androidx.room.ForeignKey
import androidx.room.ForeignKey.Companion.CASCADE import androidx.room.ForeignKey.Companion.CASCADE
import androidx.room.Index import androidx.room.Index
import java.time.OffsetDateTime
import org.schabi.newpipe.database.history.model.StreamHistoryEntity.Companion.JOIN_STREAM_ID import org.schabi.newpipe.database.history.model.StreamHistoryEntity.Companion.JOIN_STREAM_ID
import org.schabi.newpipe.database.history.model.StreamHistoryEntity.Companion.STREAM_ACCESS_DATE import org.schabi.newpipe.database.history.model.StreamHistoryEntity.Companion.STREAM_ACCESS_DATE
import org.schabi.newpipe.database.history.model.StreamHistoryEntity.Companion.STREAM_HISTORY_TABLE import org.schabi.newpipe.database.history.model.StreamHistoryEntity.Companion.STREAM_HISTORY_TABLE
import org.schabi.newpipe.database.stream.model.StreamEntity import org.schabi.newpipe.database.stream.model.StreamEntity
import org.schabi.newpipe.database.stream.model.StreamEntity.Companion.STREAM_ID import org.schabi.newpipe.database.stream.model.StreamEntity.Companion.STREAM_ID
import java.time.OffsetDateTime
/** /**
* @param streamUid the stream id this history item will refer to * @param streamUid the stream id this history item will refer to

View file

@ -2,10 +2,10 @@ package org.schabi.newpipe.database.history.model
import androidx.room.ColumnInfo import androidx.room.ColumnInfo
import androidx.room.Embedded import androidx.room.Embedded
import java.time.OffsetDateTime
import org.schabi.newpipe.database.stream.model.StreamEntity import org.schabi.newpipe.database.stream.model.StreamEntity
import org.schabi.newpipe.extractor.stream.StreamInfoItem import org.schabi.newpipe.extractor.stream.StreamInfoItem
import org.schabi.newpipe.util.image.ImageStrategy import org.schabi.newpipe.util.image.ImageStrategy
import java.time.OffsetDateTime
data class StreamHistoryEntry( data class StreamHistoryEntry(
@Embedded @Embedded
@ -30,16 +30,15 @@ data class StreamHistoryEntry(
accessDate.isEqual(other.accessDate) accessDate.isEqual(other.accessDate)
} }
fun toStreamInfoItem(): StreamInfoItem = fun toStreamInfoItem(): StreamInfoItem = StreamInfoItem(
StreamInfoItem( streamEntity.serviceId,
streamEntity.serviceId, streamEntity.url,
streamEntity.url, streamEntity.title,
streamEntity.title, streamEntity.streamType
streamEntity.streamType, ).apply {
).apply { duration = streamEntity.duration
duration = streamEntity.duration uploaderName = streamEntity.uploader
uploaderName = streamEntity.uploader uploaderUrl = streamEntity.uploaderUrl
uploaderUrl = streamEntity.uploaderUrl thumbnails = ImageStrategy.dbUrlToImageList(streamEntity.thumbnailUrl)
thumbnails = ImageStrategy.dbUrlToImageList(streamEntity.thumbnailUrl) }
}
} }

View file

@ -37,7 +37,7 @@ data class PlaylistEntity @JvmOverloads constructor(
name = item.orderingName, name = item.orderingName,
isThumbnailPermanent = item.isThumbnailPermanent!!, isThumbnailPermanent = item.isThumbnailPermanent!!,
thumbnailStreamId = item.thumbnailStreamId!!, thumbnailStreamId = item.thumbnailStreamId!!,
displayIndex = item.displayIndex!!, displayIndex = item.displayIndex!!
) )
companion object { companion object {

View file

@ -9,13 +9,13 @@ package org.schabi.newpipe.database.stream
import androidx.room.ColumnInfo import androidx.room.ColumnInfo
import androidx.room.Embedded import androidx.room.Embedded
import androidx.room.Ignore import androidx.room.Ignore
import java.time.OffsetDateTime
import org.schabi.newpipe.database.LocalItem import org.schabi.newpipe.database.LocalItem
import org.schabi.newpipe.database.history.model.StreamHistoryEntity import org.schabi.newpipe.database.history.model.StreamHistoryEntity
import org.schabi.newpipe.database.stream.model.StreamEntity import org.schabi.newpipe.database.stream.model.StreamEntity
import org.schabi.newpipe.database.stream.model.StreamStateEntity.Companion.STREAM_PROGRESS_MILLIS import org.schabi.newpipe.database.stream.model.StreamStateEntity.Companion.STREAM_PROGRESS_MILLIS
import org.schabi.newpipe.extractor.stream.StreamInfoItem import org.schabi.newpipe.extractor.stream.StreamInfoItem
import org.schabi.newpipe.util.image.ImageStrategy import org.schabi.newpipe.util.image.ImageStrategy
import java.time.OffsetDateTime
data class StreamStatisticsEntry( data class StreamStatisticsEntry(
@Embedded @Embedded

View file

@ -8,12 +8,12 @@ import androidx.room.Query
import androidx.room.Transaction import androidx.room.Transaction
import io.reactivex.rxjava3.core.Completable import io.reactivex.rxjava3.core.Completable
import io.reactivex.rxjava3.core.Flowable import io.reactivex.rxjava3.core.Flowable
import java.time.OffsetDateTime
import org.schabi.newpipe.database.BasicDAO import org.schabi.newpipe.database.BasicDAO
import org.schabi.newpipe.database.stream.model.StreamEntity import org.schabi.newpipe.database.stream.model.StreamEntity
import org.schabi.newpipe.database.stream.model.StreamEntity.Companion.STREAM_ID import org.schabi.newpipe.database.stream.model.StreamEntity.Companion.STREAM_ID
import org.schabi.newpipe.extractor.stream.StreamType import org.schabi.newpipe.extractor.stream.StreamType
import org.schabi.newpipe.util.StreamTypeUtil import org.schabi.newpipe.util.StreamTypeUtil
import java.time.OffsetDateTime
@Dao @Dao
abstract class StreamDAO : BasicDAO<StreamEntity> { abstract class StreamDAO : BasicDAO<StreamEntity> {
@ -91,7 +91,6 @@ abstract class StreamDAO : BasicDAO<StreamEntity> {
newerStream.uid = existentMinimalStream.uid newerStream.uid = existentMinimalStream.uid
if (!StreamTypeUtil.isLiveStream(newerStream.streamType)) { if (!StreamTypeUtil.isLiveStream(newerStream.streamType)) {
// Use the existent upload date if the newer stream does not have a better precision // Use the existent upload date if the newer stream does not have a better precision
// (i.e. is an approximation). This is done to prevent unnecessary changes. // (i.e. is an approximation). This is done to prevent unnecessary changes.
val hasBetterPrecision = val hasBetterPrecision =

View file

@ -5,6 +5,8 @@ import androidx.room.Entity
import androidx.room.Ignore import androidx.room.Ignore
import androidx.room.Index import androidx.room.Index
import androidx.room.PrimaryKey import androidx.room.PrimaryKey
import java.io.Serializable
import java.time.OffsetDateTime
import org.schabi.newpipe.database.stream.model.StreamEntity.Companion.STREAM_SERVICE_ID import org.schabi.newpipe.database.stream.model.StreamEntity.Companion.STREAM_SERVICE_ID
import org.schabi.newpipe.database.stream.model.StreamEntity.Companion.STREAM_TABLE import org.schabi.newpipe.database.stream.model.StreamEntity.Companion.STREAM_TABLE
import org.schabi.newpipe.database.stream.model.StreamEntity.Companion.STREAM_URL import org.schabi.newpipe.database.stream.model.StreamEntity.Companion.STREAM_URL
@ -14,8 +16,6 @@ import org.schabi.newpipe.extractor.stream.StreamInfoItem
import org.schabi.newpipe.extractor.stream.StreamType import org.schabi.newpipe.extractor.stream.StreamType
import org.schabi.newpipe.player.playqueue.PlayQueueItem import org.schabi.newpipe.player.playqueue.PlayQueueItem
import org.schabi.newpipe.util.image.ImageStrategy import org.schabi.newpipe.util.image.ImageStrategy
import java.io.Serializable
import java.time.OffsetDateTime
@Entity( @Entity(
tableName = STREAM_TABLE, tableName = STREAM_TABLE,
@ -86,8 +86,12 @@ data class StreamEntity(
@Ignore @Ignore
constructor(item: PlayQueueItem) : this( constructor(item: PlayQueueItem) : this(
serviceId = item.serviceId, url = item.url, title = item.title, serviceId = item.serviceId,
streamType = item.streamType, duration = item.duration, uploader = item.uploader, url = item.url,
title = item.title,
streamType = item.streamType,
duration = item.duration,
uploader = item.uploader,
uploaderUrl = item.uploaderUrl, uploaderUrl = item.uploaderUrl,
thumbnailUrl = ImageStrategy.imageListToDbUrl(item.thumbnails) thumbnailUrl = ImageStrategy.imageListToDbUrl(item.thumbnails)
) )

View file

@ -7,6 +7,7 @@ import androidx.core.content.ContextCompat
import com.google.android.exoplayer2.ExoPlaybackException import com.google.android.exoplayer2.ExoPlaybackException
import com.google.android.exoplayer2.upstream.HttpDataSource import com.google.android.exoplayer2.upstream.HttpDataSource
import com.google.android.exoplayer2.upstream.Loader import com.google.android.exoplayer2.upstream.Loader
import java.net.UnknownHostException
import kotlinx.parcelize.Parcelize import kotlinx.parcelize.Parcelize
import org.schabi.newpipe.R import org.schabi.newpipe.R
import org.schabi.newpipe.extractor.Info import org.schabi.newpipe.extractor.Info
@ -28,7 +29,6 @@ import org.schabi.newpipe.extractor.exceptions.YoutubeMusicPremiumContentExcepti
import org.schabi.newpipe.ktx.isNetworkRelated import org.schabi.newpipe.ktx.isNetworkRelated
import org.schabi.newpipe.player.mediasource.FailedMediaSource import org.schabi.newpipe.player.mediasource.FailedMediaSource
import org.schabi.newpipe.player.resolver.PlaybackResolver import org.schabi.newpipe.player.resolver.PlaybackResolver
import java.net.UnknownHostException
/** /**
* An error has occurred in the app. This class contains plain old parcelable data that can be used * An error has occurred in the app. This class contains plain old parcelable data that can be used
@ -59,7 +59,7 @@ class ErrorInfo private constructor(
* If present, this resource can alternatively be opened in browser (useful if NewPipe is * If present, this resource can alternatively be opened in browser (useful if NewPipe is
* badly broken). * badly broken).
*/ */
val openInBrowserUrl: String?, val openInBrowserUrl: String?
) : Parcelable { ) : Parcelable {
@JvmOverloads @JvmOverloads
@ -68,7 +68,7 @@ class ErrorInfo private constructor(
userAction: UserAction, userAction: UserAction,
request: String, request: String,
serviceId: Int? = null, serviceId: Int? = null,
openInBrowserUrl: String? = null, openInBrowserUrl: String? = null
) : this( ) : this(
throwableToStringList(throwable), throwableToStringList(throwable),
userAction, userAction,
@ -78,7 +78,7 @@ class ErrorInfo private constructor(
isReportable(throwable), isReportable(throwable),
isRetryable(throwable), isRetryable(throwable),
(throwable as? ReCaptchaException)?.url, (throwable as? ReCaptchaException)?.url,
openInBrowserUrl, openInBrowserUrl
) )
@JvmOverloads @JvmOverloads
@ -87,7 +87,7 @@ class ErrorInfo private constructor(
userAction: UserAction, userAction: UserAction,
request: String, request: String,
serviceId: Int? = null, serviceId: Int? = null,
openInBrowserUrl: String? = null, openInBrowserUrl: String? = null
) : this( ) : this(
throwableListToStringList(throwables), throwableListToStringList(throwables),
userAction, userAction,
@ -97,7 +97,7 @@ class ErrorInfo private constructor(
throwables.any(::isReportable), throwables.any(::isReportable),
throwables.isEmpty() || throwables.any(::isRetryable), throwables.isEmpty() || throwables.any(::isRetryable),
throwables.firstNotNullOfOrNull { it as? ReCaptchaException }?.url, throwables.firstNotNullOfOrNull { it as? ReCaptchaException }?.url,
openInBrowserUrl, openInBrowserUrl
) )
// constructor to manually build ErrorInfo when no throwable is available // constructor to manually build ErrorInfo when no throwable is available
@ -118,7 +118,7 @@ class ErrorInfo private constructor(
throwable: Throwable, throwable: Throwable,
userAction: UserAction, userAction: UserAction,
request: String, request: String,
info: Info?, info: Info?
) : ) :
this(throwable, userAction, request, info?.serviceId, info?.url) this(throwable, userAction, request, info?.serviceId, info?.url)
@ -127,7 +127,7 @@ class ErrorInfo private constructor(
throwables: List<Throwable>, throwables: List<Throwable>,
userAction: UserAction, userAction: UserAction,
request: String, request: String,
info: Info?, info: Info?
) : ) :
this(throwables, userAction, request, info?.serviceId, info?.url) this(throwables, userAction, request, info?.serviceId, info?.url)
@ -144,7 +144,7 @@ class ErrorInfo private constructor(
class ErrorMessage( class ErrorMessage(
@StringRes @StringRes
private val stringRes: Int, private val stringRes: Int,
private vararg val formatArgs: String, private vararg val formatArgs: String
) : Parcelable { ) : Parcelable {
fun getString(context: Context): String { fun getString(context: Context): String {
return if (formatArgs.isEmpty()) { return if (formatArgs.isEmpty()) {
@ -160,21 +160,19 @@ class ErrorInfo private constructor(
const val SERVICE_NONE = "<unknown_service>" const val SERVICE_NONE = "<unknown_service>"
private fun getServiceName(serviceId: Int?) = private fun getServiceName(serviceId: Int?) = // not using getNameOfServiceById since we want to accept a nullable serviceId and we
// not using getNameOfServiceById since we want to accept a nullable serviceId and we
// want to default to SERVICE_NONE // want to default to SERVICE_NONE
ServiceList.all().firstOrNull { it.serviceId == serviceId }?.serviceInfo?.name ServiceList.all().firstOrNull { it.serviceId == serviceId }?.serviceInfo?.name
?: SERVICE_NONE ?: SERVICE_NONE
fun throwableToStringList(throwable: Throwable) = arrayOf(throwable.stackTraceToString()) fun throwableToStringList(throwable: Throwable) = arrayOf(throwable.stackTraceToString())
fun throwableListToStringList(throwableList: List<Throwable>) = fun throwableListToStringList(throwableList: List<Throwable>) = throwableList.map { it.stackTraceToString() }.toTypedArray()
throwableList.map { it.stackTraceToString() }.toTypedArray()
fun getMessage( fun getMessage(
throwable: Throwable?, throwable: Throwable?,
action: UserAction?, action: UserAction?,
serviceId: Int?, serviceId: Int?
): ErrorMessage { ): ErrorMessage {
return when { return when {
// player exceptions // player exceptions
@ -193,18 +191,24 @@ class ErrorInfo private constructor(
ErrorMessage(R.string.player_http_invalid_status, cause.responseCode.toString()) ErrorMessage(R.string.player_http_invalid_status, cause.responseCode.toString())
} }
} }
cause is Loader.UnexpectedLoaderException && cause.cause is ExtractionException -> cause is Loader.UnexpectedLoaderException && cause.cause is ExtractionException ->
getMessage(throwable, action, serviceId) getMessage(throwable, action, serviceId)
throwable.type == ExoPlaybackException.TYPE_SOURCE -> throwable.type == ExoPlaybackException.TYPE_SOURCE ->
ErrorMessage(R.string.player_stream_failure) ErrorMessage(R.string.player_stream_failure)
throwable.type == ExoPlaybackException.TYPE_UNEXPECTED -> throwable.type == ExoPlaybackException.TYPE_UNEXPECTED ->
ErrorMessage(R.string.player_recoverable_failure) ErrorMessage(R.string.player_recoverable_failure)
else -> else ->
ErrorMessage(R.string.player_unrecoverable_failure) ErrorMessage(R.string.player_unrecoverable_failure)
} }
} }
throwable is FailedMediaSource.FailedMediaSourceException -> throwable is FailedMediaSource.FailedMediaSourceException ->
getMessage(throwable.cause, action, serviceId) getMessage(throwable.cause, action, serviceId)
throwable is PlaybackResolver.ResolverException -> throwable is PlaybackResolver.ResolverException ->
ErrorMessage(R.string.player_stream_failure) ErrorMessage(R.string.player_stream_failure)
@ -220,34 +224,46 @@ class ErrorInfo private constructor(
) )
} }
?: ErrorMessage(R.string.account_terminated) ?: ErrorMessage(R.string.account_terminated)
throwable is AgeRestrictedContentException -> throwable is AgeRestrictedContentException ->
ErrorMessage(R.string.restricted_video_no_stream) ErrorMessage(R.string.restricted_video_no_stream)
throwable is GeographicRestrictionException -> throwable is GeographicRestrictionException ->
ErrorMessage(R.string.georestricted_content) ErrorMessage(R.string.georestricted_content)
throwable is PaidContentException -> throwable is PaidContentException ->
ErrorMessage(R.string.paid_content) ErrorMessage(R.string.paid_content)
throwable is PrivateContentException -> throwable is PrivateContentException ->
ErrorMessage(R.string.private_content) ErrorMessage(R.string.private_content)
throwable is SoundCloudGoPlusContentException -> throwable is SoundCloudGoPlusContentException ->
ErrorMessage(R.string.soundcloud_go_plus_content) ErrorMessage(R.string.soundcloud_go_plus_content)
throwable is UnsupportedContentInCountryException -> throwable is UnsupportedContentInCountryException ->
ErrorMessage(R.string.unsupported_content_in_country) ErrorMessage(R.string.unsupported_content_in_country)
throwable is YoutubeMusicPremiumContentException -> throwable is YoutubeMusicPremiumContentException ->
ErrorMessage(R.string.youtube_music_premium_content) ErrorMessage(R.string.youtube_music_premium_content)
throwable is SignInConfirmNotBotException -> throwable is SignInConfirmNotBotException ->
ErrorMessage(R.string.sign_in_confirm_not_bot_error, getServiceName(serviceId)) ErrorMessage(R.string.sign_in_confirm_not_bot_error, getServiceName(serviceId))
throwable is ContentNotAvailableException -> throwable is ContentNotAvailableException ->
ErrorMessage(R.string.content_not_available) ErrorMessage(R.string.content_not_available)
// other extractor exceptions // other extractor exceptions
throwable is ContentNotSupportedException -> throwable is ContentNotSupportedException ->
ErrorMessage(R.string.content_not_supported) ErrorMessage(R.string.content_not_supported)
// ReCaptchas will be handled in a special way anyway // ReCaptchas will be handled in a special way anyway
throwable is ReCaptchaException -> throwable is ReCaptchaException ->
ErrorMessage(R.string.recaptcha_request_toast) ErrorMessage(R.string.recaptcha_request_toast)
// test this at the end as many exceptions could be a subclass of IOException // test this at the end as many exceptions could be a subclass of IOException
throwable != null && throwable.isNetworkRelated -> throwable != null && throwable.isNetworkRelated ->
ErrorMessage(R.string.network_error) ErrorMessage(R.string.network_error)
// an extraction exception unrelated to the network // an extraction exception unrelated to the network
// is likely an issue with parsing the website // is likely an issue with parsing the website
throwable is ExtractionException -> throwable is ExtractionException ->
@ -256,16 +272,22 @@ class ErrorInfo private constructor(
// user actions (in case the exception is null or unrecognizable) // user actions (in case the exception is null or unrecognizable)
action == UserAction.UI_ERROR -> action == UserAction.UI_ERROR ->
ErrorMessage(R.string.app_ui_crash) ErrorMessage(R.string.app_ui_crash)
action == UserAction.REQUESTED_COMMENTS -> action == UserAction.REQUESTED_COMMENTS ->
ErrorMessage(R.string.error_unable_to_load_comments) ErrorMessage(R.string.error_unable_to_load_comments)
action == UserAction.SUBSCRIPTION_CHANGE -> action == UserAction.SUBSCRIPTION_CHANGE ->
ErrorMessage(R.string.subscription_change_failed) ErrorMessage(R.string.subscription_change_failed)
action == UserAction.SUBSCRIPTION_UPDATE -> action == UserAction.SUBSCRIPTION_UPDATE ->
ErrorMessage(R.string.subscription_update_failed) ErrorMessage(R.string.subscription_update_failed)
action == UserAction.LOAD_IMAGE -> action == UserAction.LOAD_IMAGE ->
ErrorMessage(R.string.could_not_load_thumbnails) ErrorMessage(R.string.could_not_load_thumbnails)
action == UserAction.DOWNLOAD_OPEN_DIALOG -> action == UserAction.DOWNLOAD_OPEN_DIALOG ->
ErrorMessage(R.string.could_not_setup_download_menu) ErrorMessage(R.string.could_not_setup_download_menu)
else -> else ->
ErrorMessage(R.string.error_snackbar_message) ErrorMessage(R.string.error_snackbar_message)
} }
@ -276,15 +298,19 @@ class ErrorInfo private constructor(
// we don't have an exception, so this is a manually built error, which likely // we don't have an exception, so this is a manually built error, which likely
// indicates that it's important and is thus reportable // indicates that it's important and is thus reportable
null -> true null -> true
// the service explicitly said that content is not available (e.g. age restrictions, // the service explicitly said that content is not available (e.g. age restrictions,
// video deleted, etc.), there is no use in letting users report it // video deleted, etc.), there is no use in letting users report it
is ContentNotAvailableException -> false is ContentNotAvailableException -> false
// we know the content is not supported, no need to let the user report it // we know the content is not supported, no need to let the user report it
is ContentNotSupportedException -> false is ContentNotSupportedException -> false
// happens often when there is no internet connection; we don't use // happens often when there is no internet connection; we don't use
// `throwable.isNetworkRelated` since any `IOException` would make that function // `throwable.isNetworkRelated` since any `IOException` would make that function
// return true, but not all `IOException`s are network related // return true, but not all `IOException`s are network related
is UnknownHostException -> false is UnknownHostException -> false
// by default, this is an unexpected exception, which the user could report // by default, this is an unexpected exception, which the user could report
else -> true else -> true
} }
@ -294,8 +320,10 @@ class ErrorInfo private constructor(
return when (throwable) { return when (throwable) {
// we know the content is not available, retrying won't help // we know the content is not available, retrying won't help
is ContentNotAvailableException -> false is ContentNotAvailableException -> false
// we know the content is not supported, retrying won't help // we know the content is not supported, retrying won't help
is ContentNotSupportedException -> false is ContentNotSupportedException -> false
// by default (including if throwable is null), enable retrying (though the retry // by default (including if throwable is null), enable retrying (though the retry
// button will be shown only if a way to perform the retry is implemented) // button will be shown only if a way to perform the retry is implemented)
else -> true else -> true

View file

@ -11,16 +11,16 @@ import androidx.fragment.app.Fragment
import com.jakewharton.rxbinding4.view.clicks import com.jakewharton.rxbinding4.view.clicks
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
import io.reactivex.rxjava3.disposables.Disposable import io.reactivex.rxjava3.disposables.Disposable
import java.util.concurrent.TimeUnit
import org.schabi.newpipe.MainActivity import org.schabi.newpipe.MainActivity
import org.schabi.newpipe.R import org.schabi.newpipe.R
import org.schabi.newpipe.ktx.animate import org.schabi.newpipe.ktx.animate
import org.schabi.newpipe.util.external_communication.ShareUtils import org.schabi.newpipe.util.external_communication.ShareUtils
import java.util.concurrent.TimeUnit
class ErrorPanelHelper( class ErrorPanelHelper(
private val fragment: Fragment, private val fragment: Fragment,
rootView: View, rootView: View,
onRetry: Runnable?, onRetry: Runnable?
) { ) {
private val context: Context = rootView.context!! private val context: Context = rootView.context!!

View file

@ -46,7 +46,7 @@ class ErrorUtil {
@JvmStatic @JvmStatic
fun openActivity(context: Context, errorInfo: ErrorInfo) { fun openActivity(context: Context, errorInfo: ErrorInfo) {
if (PreferenceManager.getDefaultSharedPreferences(context) if (PreferenceManager.getDefaultSharedPreferences(context)
.getBoolean(MainActivity.KEY_IS_IN_BACKGROUND, true) .getBoolean(MainActivity.KEY_IS_IN_BACKGROUND, true)
) { ) {
createNotification(context, errorInfo) createNotification(context, errorInfo)
} else { } else {

View file

@ -40,5 +40,5 @@ enum class UserAction(val message: String) {
OPEN_INFO_ITEM_DIALOG("open info item dialog"), OPEN_INFO_ITEM_DIALOG("open info item dialog"),
GETTING_MAIN_SCREEN_TAB("getting main screen tab"), GETTING_MAIN_SCREEN_TAB("getting main screen tab"),
PLAY_ON_POPUP("play on popup"), PLAY_ON_POPUP("play on popup"),
SUBSCRIPTIONS("loading subscriptions"); SUBSCRIPTIONS("loading subscriptions")
} }

View file

@ -13,14 +13,17 @@ enum class ItemViewMode {
* Default mode. * Default mode.
*/ */
AUTO, AUTO,
/** /**
* Full width list item with thumb on the left and two line title & uploader in right. * Full width list item with thumb on the left and two line title & uploader in right.
*/ */
LIST, LIST,
/** /**
* Grid mode places two cards per row. * Grid mode places two cards per row.
*/ */
GRID, GRID,
/** /**
* A full width card in phone - portrait. * A full width card in phone - portrait.
*/ */

View file

@ -2,8 +2,8 @@ package org.schabi.newpipe.info_list
import android.util.Log import android.util.Log
import com.xwray.groupie.GroupieAdapter import com.xwray.groupie.GroupieAdapter
import org.schabi.newpipe.extractor.stream.StreamInfo
import kotlin.math.max import kotlin.math.max
import org.schabi.newpipe.extractor.stream.StreamInfo
/** /**
* Custom RecyclerView.Adapter/GroupieAdapter for [StreamSegmentItem] for handling selection state. * Custom RecyclerView.Adapter/GroupieAdapter for [StreamSegmentItem] for handling selection state.

View file

@ -41,7 +41,10 @@ class StreamSegmentItem(
viewHolder.root.findViewById<TextView>(R.id.textViewStartSeconds).text = viewHolder.root.findViewById<TextView>(R.id.textViewStartSeconds).text =
Localization.getDurationString(item.startTimeSeconds.toLong()) Localization.getDurationString(item.startTimeSeconds.toLong())
viewHolder.root.setOnClickListener { onClick.onItemClick(this, item.startTimeSeconds) } viewHolder.root.setOnClickListener { onClick.onItemClick(this, item.startTimeSeconds) }
viewHolder.root.setOnLongClickListener { onClick.onItemLongClick(this, item.startTimeSeconds); true } viewHolder.root.setOnLongClickListener {
onClick.onItemLongClick(this, item.startTimeSeconds)
true
}
viewHolder.root.isSelected = isSelected viewHolder.root.isSelected = isSelected
} }

View file

@ -43,8 +43,14 @@ fun View.animate(
if (DEBUG) { if (DEBUG) {
val id = runCatching { resources.getResourceEntryName(id) }.getOrDefault(id.toString()) val id = runCatching { resources.getResourceEntryName(id) }.getOrDefault(id.toString())
val msg = String.format( val msg = String.format(
"%8s → [%s:%s] [%s %s:%s] execOnEnd=%s", enterOrExit, "%8s → [%s:%s] [%s %s:%s] execOnEnd=%s",
javaClass.simpleName, id, animationType, duration, delay, execOnEnd enterOrExit,
javaClass.simpleName,
id,
animationType,
duration,
delay,
execOnEnd
) )
Log.d(TAG, "animate(): $msg") Log.d(TAG, "animate(): $msg")
} }
@ -287,5 +293,9 @@ private class HideAndExecOnEndListener(private val view: View, execOnEnd: Runnab
} }
enum class AnimationType { enum class AnimationType {
ALPHA, SCALE_AND_ALPHA, LIGHT_SCALE_AND_ALPHA, SLIDE_AND_ALPHA, LIGHT_SLIDE_AND_ALPHA ALPHA,
SCALE_AND_ALPHA,
LIGHT_SCALE_AND_ALPHA,
SLIDE_AND_ALPHA,
LIGHT_SLIDE_AND_ALPHA
} }

View file

@ -7,6 +7,9 @@ import io.reactivex.rxjava3.core.Completable
import io.reactivex.rxjava3.core.Flowable import io.reactivex.rxjava3.core.Flowable
import io.reactivex.rxjava3.core.Maybe import io.reactivex.rxjava3.core.Maybe
import io.reactivex.rxjava3.schedulers.Schedulers import io.reactivex.rxjava3.schedulers.Schedulers
import java.time.LocalDate
import java.time.OffsetDateTime
import java.time.ZoneOffset
import org.schabi.newpipe.MainActivity.DEBUG import org.schabi.newpipe.MainActivity.DEBUG
import org.schabi.newpipe.NewPipeDatabase import org.schabi.newpipe.NewPipeDatabase
import org.schabi.newpipe.database.feed.model.FeedEntity import org.schabi.newpipe.database.feed.model.FeedEntity
@ -18,9 +21,6 @@ import org.schabi.newpipe.database.subscription.NotificationMode
import org.schabi.newpipe.extractor.stream.StreamInfoItem import org.schabi.newpipe.extractor.stream.StreamInfoItem
import org.schabi.newpipe.extractor.stream.StreamType import org.schabi.newpipe.extractor.stream.StreamType
import org.schabi.newpipe.local.subscription.FeedGroupIcon import org.schabi.newpipe.local.subscription.FeedGroupIcon
import java.time.LocalDate
import java.time.OffsetDateTime
import java.time.ZoneOffset
class FeedDatabaseManager(context: Context) { class FeedDatabaseManager(context: Context) {
private val database = NewPipeDatabase.getInstance(context) private val database = NewPipeDatabase.getInstance(context)

View file

@ -53,6 +53,8 @@ import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
import io.reactivex.rxjava3.core.Single import io.reactivex.rxjava3.core.Single
import io.reactivex.rxjava3.disposables.CompositeDisposable import io.reactivex.rxjava3.disposables.CompositeDisposable
import io.reactivex.rxjava3.schedulers.Schedulers import io.reactivex.rxjava3.schedulers.Schedulers
import java.time.OffsetDateTime
import java.util.function.Consumer
import org.schabi.newpipe.NewPipeDatabase import org.schabi.newpipe.NewPipeDatabase
import org.schabi.newpipe.R import org.schabi.newpipe.R
import org.schabi.newpipe.database.feed.model.FeedGroupEntity import org.schabi.newpipe.database.feed.model.FeedGroupEntity
@ -81,8 +83,6 @@ import org.schabi.newpipe.util.ThemeHelper.getGridSpanCountStreams
import org.schabi.newpipe.util.ThemeHelper.getItemViewMode import org.schabi.newpipe.util.ThemeHelper.getItemViewMode
import org.schabi.newpipe.util.ThemeHelper.resolveDrawable import org.schabi.newpipe.util.ThemeHelper.resolveDrawable
import org.schabi.newpipe.util.ThemeHelper.shouldUseGridLayout import org.schabi.newpipe.util.ThemeHelper.shouldUseGridLayout
import java.time.OffsetDateTime
import java.util.function.Consumer
class FeedFragment : BaseStateFragment<FeedState>() { class FeedFragment : BaseStateFragment<FeedState>() {
private var _feedBinding: FragmentFeedBinding? = null private var _feedBinding: FragmentFeedBinding? = null
@ -91,7 +91,10 @@ class FeedFragment : BaseStateFragment<FeedState>() {
private val disposables = CompositeDisposable() private val disposables = CompositeDisposable()
private lateinit var viewModel: FeedViewModel private lateinit var viewModel: FeedViewModel
@State @JvmField var listState: Parcelable? = null
@State
@JvmField
var listState: Parcelable? = null
private var groupId = FeedGroupEntity.GROUP_ALL_ID private var groupId = FeedGroupEntity.GROUP_ALL_ID
private var groupName = "" private var groupName = ""
@ -149,7 +152,6 @@ class FeedFragment : BaseStateFragment<FeedState>() {
if (newState == RecyclerView.SCROLL_STATE_IDLE && if (newState == RecyclerView.SCROLL_STATE_IDLE &&
!recyclerView.canScrollVertically(-1) !recyclerView.canScrollVertically(-1)
) { ) {
if (tryGetNewItemsLoadedButton()?.isVisible == true) { if (tryGetNewItemsLoadedButton()?.isVisible == true) {
hideNewItemsLoaded(true) hideNewItemsLoaded(true)
} }
@ -387,8 +389,13 @@ class FeedFragment : BaseStateFragment<FeedState>() {
if (item is StreamItem && !isRefreshing) { if (item is StreamItem && !isRefreshing) {
val stream = item.streamWithState.stream val stream = item.streamWithState.stream
NavigationHelper.openVideoDetailFragment( NavigationHelper.openVideoDetailFragment(
requireContext(), fm, requireContext(),
stream.serviceId, stream.url, stream.title, null, false fm,
stream.serviceId,
stream.url,
stream.title,
null,
false
) )
} }
} }
@ -500,7 +507,8 @@ class FeedFragment : BaseStateFragment<FeedState>() {
) { ) {
val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(requireContext()) val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(requireContext())
val isFastFeedModeEnabled = sharedPreferences.getBoolean( val isFastFeedModeEnabled = sharedPreferences.getBoolean(
getString(R.string.feed_use_dedicated_fetch_method_key), false getString(R.string.feed_use_dedicated_fetch_method_key),
false
) )
val builder = AlertDialog.Builder(requireContext()) val builder = AlertDialog.Builder(requireContext())
@ -535,7 +543,8 @@ class FeedFragment : BaseStateFragment<FeedState>() {
private fun updateRelativeTimeViews() { private fun updateRelativeTimeViews() {
updateRefreshViewState() updateRefreshViewState()
groupAdapter.notifyItemRangeChanged( groupAdapter.notifyItemRangeChanged(
0, groupAdapter.itemCount, 0,
groupAdapter.itemCount,
StreamItem.UPDATE_RELATIVE_TIME StreamItem.UPDATE_RELATIVE_TIME
) )
} }

View file

@ -1,8 +1,8 @@
package org.schabi.newpipe.local.feed package org.schabi.newpipe.local.feed
import androidx.annotation.StringRes import androidx.annotation.StringRes
import org.schabi.newpipe.local.feed.item.StreamItem
import java.time.OffsetDateTime import java.time.OffsetDateTime
import org.schabi.newpipe.local.feed.item.StreamItem
sealed class FeedState { sealed class FeedState {
data class ProgressState( data class ProgressState(

View file

@ -14,6 +14,8 @@ import io.reactivex.rxjava3.core.Flowable
import io.reactivex.rxjava3.functions.Function6 import io.reactivex.rxjava3.functions.Function6
import io.reactivex.rxjava3.processors.BehaviorProcessor import io.reactivex.rxjava3.processors.BehaviorProcessor
import io.reactivex.rxjava3.schedulers.Schedulers import io.reactivex.rxjava3.schedulers.Schedulers
import java.time.OffsetDateTime
import java.util.concurrent.TimeUnit
import org.schabi.newpipe.App import org.schabi.newpipe.App
import org.schabi.newpipe.R import org.schabi.newpipe.R
import org.schabi.newpipe.database.feed.model.FeedGroupEntity import org.schabi.newpipe.database.feed.model.FeedGroupEntity
@ -25,8 +27,6 @@ import org.schabi.newpipe.local.feed.service.FeedEventManager.Event.IdleEvent
import org.schabi.newpipe.local.feed.service.FeedEventManager.Event.ProgressEvent import org.schabi.newpipe.local.feed.service.FeedEventManager.Event.ProgressEvent
import org.schabi.newpipe.local.feed.service.FeedEventManager.Event.SuccessResultEvent import org.schabi.newpipe.local.feed.service.FeedEventManager.Event.SuccessResultEvent
import org.schabi.newpipe.util.DEFAULT_THROTTLE_TIMEOUT import org.schabi.newpipe.util.DEFAULT_THROTTLE_TIMEOUT
import java.time.OffsetDateTime
import java.util.concurrent.TimeUnit
class FeedViewModel( class FeedViewModel(
private val application: Application, private val application: Application,
@ -64,8 +64,14 @@ class FeedViewModel(
feedDatabaseManager.notLoadedCount(groupId), feedDatabaseManager.notLoadedCount(groupId),
feedDatabaseManager.oldestSubscriptionUpdate(groupId), feedDatabaseManager.oldestSubscriptionUpdate(groupId),
Function6 { t1: FeedEventManager.Event, t2: Boolean, t3: Boolean, t4: Boolean, Function6 {
t5: Long, t6: List<OffsetDateTime?> -> t1: FeedEventManager.Event,
t2: Boolean,
t3: Boolean,
t4: Boolean,
t5: Long,
t6: List<OffsetDateTime?>
->
return@Function6 CombineResultEventHolder(t1, t2, t3, t4, t5, t6.firstOrNull()) return@Function6 CombineResultEventHolder(t1, t2, t3, t4, t5, t6.firstOrNull())
} }
) )
@ -73,12 +79,13 @@ class FeedViewModel(
.subscribeOn(Schedulers.io()) .subscribeOn(Schedulers.io())
.observeOn(Schedulers.io()) .observeOn(Schedulers.io())
.map { (event, showPlayedItems, showPartiallyPlayedItems, showFutureItems, notLoadedCount, oldestUpdate) -> .map { (event, showPlayedItems, showPartiallyPlayedItems, showFutureItems, notLoadedCount, oldestUpdate) ->
val streamItems = if (event is SuccessResultEvent || event is IdleEvent) val streamItems = if (event is SuccessResultEvent || event is IdleEvent) {
feedDatabaseManager feedDatabaseManager
.getStreams(groupId, showPlayedItems, showPartiallyPlayedItems, showFutureItems) .getStreams(groupId, showPlayedItems, showPartiallyPlayedItems, showFutureItems)
.blockingGet(arrayListOf()) .blockingGet(arrayListOf())
else } else {
arrayListOf() arrayListOf()
}
CombineResultDataHolder(event, streamItems, notLoadedCount, oldestUpdate) CombineResultDataHolder(event, streamItems, notLoadedCount, oldestUpdate)
} }
@ -150,17 +157,14 @@ class FeedViewModel(
fun getShowFutureItemsFromPreferences() = getShowFutureItemsFromPreferences(application) fun getShowFutureItemsFromPreferences() = getShowFutureItemsFromPreferences(application)
companion object { companion object {
private fun getShowPlayedItemsFromPreferences(context: Context) = private fun getShowPlayedItemsFromPreferences(context: Context) = PreferenceManager.getDefaultSharedPreferences(context)
PreferenceManager.getDefaultSharedPreferences(context) .getBoolean(context.getString(R.string.feed_show_watched_items_key), true)
.getBoolean(context.getString(R.string.feed_show_watched_items_key), true)
private fun getShowPartiallyPlayedItemsFromPreferences(context: Context) = private fun getShowPartiallyPlayedItemsFromPreferences(context: Context) = PreferenceManager.getDefaultSharedPreferences(context)
PreferenceManager.getDefaultSharedPreferences(context) .getBoolean(context.getString(R.string.feed_show_partially_watched_items_key), true)
.getBoolean(context.getString(R.string.feed_show_partially_watched_items_key), true)
private fun getShowFutureItemsFromPreferences(context: Context) = private fun getShowFutureItemsFromPreferences(context: Context) = PreferenceManager.getDefaultSharedPreferences(context)
PreferenceManager.getDefaultSharedPreferences(context) .getBoolean(context.getString(R.string.feed_show_future_items_key), true)
.getBoolean(context.getString(R.string.feed_show_future_items_key), true)
fun getFactory(context: Context, groupId: Long) = viewModelFactory { fun getFactory(context: Context, groupId: Long) = viewModelFactory {
initializer { initializer {

View file

@ -6,6 +6,8 @@ import android.view.View
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.preference.PreferenceManager import androidx.preference.PreferenceManager
import com.xwray.groupie.viewbinding.BindableItem import com.xwray.groupie.viewbinding.BindableItem
import java.util.concurrent.TimeUnit
import java.util.function.Consumer
import org.schabi.newpipe.MainActivity import org.schabi.newpipe.MainActivity
import org.schabi.newpipe.R import org.schabi.newpipe.R
import org.schabi.newpipe.database.stream.StreamWithState import org.schabi.newpipe.database.stream.StreamWithState
@ -20,8 +22,6 @@ import org.schabi.newpipe.extractor.stream.StreamType.VIDEO_STREAM
import org.schabi.newpipe.util.Localization import org.schabi.newpipe.util.Localization
import org.schabi.newpipe.util.StreamTypeUtil import org.schabi.newpipe.util.StreamTypeUtil
import org.schabi.newpipe.util.image.PicassoHelper import org.schabi.newpipe.util.image.PicassoHelper
import java.util.concurrent.TimeUnit
import java.util.function.Consumer
data class StreamItem( data class StreamItem(
val streamWithState: StreamWithState, val streamWithState: StreamWithState,
@ -132,6 +132,7 @@ data class StreamItem(
viewsAndDate.isEmpty() -> uploadDate!! viewsAndDate.isEmpty() -> uploadDate!!
else -> Localization.concatenateStrings(viewsAndDate, uploadDate) else -> Localization.concatenateStrings(viewsAndDate, uploadDate)
} }
else -> viewsAndDate else -> viewsAndDate
} }
} }

View file

@ -42,7 +42,9 @@ class NotificationHelper(val context: Context) {
fun displayNewStreamsNotifications(data: FeedUpdateInfo) { fun displayNewStreamsNotifications(data: FeedUpdateInfo) {
val newStreams = data.newStreams val newStreams = data.newStreams
val summary = context.resources.getQuantityString( val summary = context.resources.getQuantityString(
R.plurals.new_streams, newStreams.size, newStreams.size R.plurals.new_streams,
newStreams.size,
newStreams.size
) )
val summaryBuilder = NotificationCompat.Builder( val summaryBuilder = NotificationCompat.Builder(
context, context,

View file

@ -16,6 +16,7 @@ import androidx.work.WorkerParameters
import androidx.work.rxjava3.RxWorker import androidx.work.rxjava3.RxWorker
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
import io.reactivex.rxjava3.core.Single import io.reactivex.rxjava3.core.Single
import java.util.concurrent.TimeUnit
import org.schabi.newpipe.App import org.schabi.newpipe.App
import org.schabi.newpipe.R import org.schabi.newpipe.R
import org.schabi.newpipe.error.ErrorInfo import org.schabi.newpipe.error.ErrorInfo
@ -23,7 +24,6 @@ import org.schabi.newpipe.error.ErrorUtil
import org.schabi.newpipe.error.UserAction import org.schabi.newpipe.error.UserAction
import org.schabi.newpipe.local.feed.service.FeedLoadManager import org.schabi.newpipe.local.feed.service.FeedLoadManager
import org.schabi.newpipe.local.feed.service.FeedLoadService import org.schabi.newpipe.local.feed.service.FeedLoadService
import java.util.concurrent.TimeUnit
/* /*
* Worker which checks for new streams of subscribed channels * Worker which checks for new streams of subscribed channels
@ -31,7 +31,7 @@ import java.util.concurrent.TimeUnit
*/ */
class NotificationWorker( class NotificationWorker(
appContext: Context, appContext: Context,
workerParams: WorkerParameters, workerParams: WorkerParameters
) : RxWorker(appContext, workerParams) { ) : RxWorker(appContext, workerParams) {
private val notificationHelper by lazy { private val notificationHelper by lazy {
@ -95,9 +95,8 @@ class NotificationWorker(
private val TAG = NotificationWorker::class.java.simpleName private val TAG = NotificationWorker::class.java.simpleName
private const val WORK_TAG = App.PACKAGE_NAME + "_streams_notifications" private const val WORK_TAG = App.PACKAGE_NAME + "_streams_notifications"
private fun areNotificationsEnabled(context: Context) = private fun areNotificationsEnabled(context: Context) = NotificationHelper.areNewStreamsNotificationsEnabled(context) &&
NotificationHelper.areNewStreamsNotificationsEnabled(context) && NotificationHelper.areNotificationsEnabledOnDevice(context)
NotificationHelper.areNotificationsEnabledOnDevice(context)
/** /**
* Schedules a task for the [NotificationWorker] * Schedules a task for the [NotificationWorker]

View file

@ -2,9 +2,9 @@ package org.schabi.newpipe.local.feed.notifications
import android.content.Context import android.content.Context
import androidx.preference.PreferenceManager import androidx.preference.PreferenceManager
import java.util.concurrent.TimeUnit
import org.schabi.newpipe.R import org.schabi.newpipe.R
import org.schabi.newpipe.ktx.getStringSafe import org.schabi.newpipe.ktx.getStringSafe
import java.util.concurrent.TimeUnit
/** /**
* Information for the Scheduler which checks for new streams. * Information for the Scheduler which checks for new streams.

View file

@ -3,8 +3,8 @@ package org.schabi.newpipe.local.feed.service
import androidx.annotation.StringRes import androidx.annotation.StringRes
import io.reactivex.rxjava3.core.Flowable import io.reactivex.rxjava3.core.Flowable
import io.reactivex.rxjava3.processors.BehaviorProcessor import io.reactivex.rxjava3.processors.BehaviorProcessor
import org.schabi.newpipe.local.feed.service.FeedEventManager.Event.IdleEvent
import java.util.concurrent.atomic.AtomicBoolean import java.util.concurrent.atomic.AtomicBoolean
import org.schabi.newpipe.local.feed.service.FeedEventManager.Event.IdleEvent
object FeedEventManager { object FeedEventManager {
private var processor: BehaviorProcessor<Event> = BehaviorProcessor.create() private var processor: BehaviorProcessor<Event> = BehaviorProcessor.create()

View file

@ -11,6 +11,10 @@ import io.reactivex.rxjava3.core.Single
import io.reactivex.rxjava3.functions.Consumer import io.reactivex.rxjava3.functions.Consumer
import io.reactivex.rxjava3.processors.PublishProcessor import io.reactivex.rxjava3.processors.PublishProcessor
import io.reactivex.rxjava3.schedulers.Schedulers import io.reactivex.rxjava3.schedulers.Schedulers
import java.time.OffsetDateTime
import java.time.ZoneOffset
import java.util.concurrent.atomic.AtomicBoolean
import java.util.concurrent.atomic.AtomicInteger
import org.schabi.newpipe.R import org.schabi.newpipe.R
import org.schabi.newpipe.database.feed.model.FeedGroupEntity import org.schabi.newpipe.database.feed.model.FeedGroupEntity
import org.schabi.newpipe.database.subscription.NotificationMode import org.schabi.newpipe.database.subscription.NotificationMode
@ -27,10 +31,6 @@ import org.schabi.newpipe.util.ChannelTabHelper
import org.schabi.newpipe.util.ExtractorHelper.getChannelInfo import org.schabi.newpipe.util.ExtractorHelper.getChannelInfo
import org.schabi.newpipe.util.ExtractorHelper.getChannelTab import org.schabi.newpipe.util.ExtractorHelper.getChannelTab
import org.schabi.newpipe.util.ExtractorHelper.getMoreChannelTabItems import org.schabi.newpipe.util.ExtractorHelper.getMoreChannelTabItems
import java.time.OffsetDateTime
import java.time.ZoneOffset
import java.util.concurrent.atomic.AtomicBoolean
import java.util.concurrent.atomic.AtomicInteger
class FeedLoadManager(private val context: Context) { class FeedLoadManager(private val context: Context) {
@ -60,7 +60,7 @@ class FeedLoadManager(private val context: Context) {
*/ */
fun startLoading( fun startLoading(
groupId: Long = FeedGroupEntity.GROUP_ALL_ID, groupId: Long = FeedGroupEntity.GROUP_ALL_ID,
ignoreOutdatedThreshold: Boolean = false, ignoreOutdatedThreshold: Boolean = false
): Single<List<Notification<FeedUpdateInfo>>> { ): Single<List<Notification<FeedUpdateInfo>>> {
val defaultSharedPreferences = PreferenceManager.getDefaultSharedPreferences(context) val defaultSharedPreferences = PreferenceManager.getDefaultSharedPreferences(context)
val useFeedExtractor = defaultSharedPreferences.getBoolean( val useFeedExtractor = defaultSharedPreferences.getBoolean(
@ -85,9 +85,12 @@ class FeedLoadManager(private val context: Context) {
FeedGroupEntity.GROUP_ALL_ID -> feedDatabaseManager.outdatedSubscriptions( FeedGroupEntity.GROUP_ALL_ID -> feedDatabaseManager.outdatedSubscriptions(
outdatedThreshold outdatedThreshold
) )
GROUP_NOTIFICATION_ENABLED -> feedDatabaseManager.outdatedSubscriptionsWithNotificationMode( GROUP_NOTIFICATION_ENABLED -> feedDatabaseManager.outdatedSubscriptionsWithNotificationMode(
outdatedThreshold, NotificationMode.ENABLED outdatedThreshold,
NotificationMode.ENABLED
) )
else -> feedDatabaseManager.outdatedSubscriptionsForGroup(groupId, outdatedThreshold) else -> feedDatabaseManager.outdatedSubscriptionsForGroup(groupId, outdatedThreshold)
} }
@ -186,7 +189,8 @@ class FeedLoadManager(private val context: Context) {
val channelInfo = getChannelInfo( val channelInfo = getChannelInfo(
subscriptionEntity.serviceId, subscriptionEntity.serviceId,
subscriptionEntity.url, true subscriptionEntity.url,
true
) )
.onErrorReturn(storeOriginalErrorAndRethrow) .onErrorReturn(storeOriginalErrorAndRethrow)
.blockingGet() .blockingGet()
@ -216,7 +220,8 @@ class FeedLoadManager(private val context: Context) {
) { ) {
val infoItemsPage = getMoreChannelTabItems( val infoItemsPage = getMoreChannelTabItems(
subscriptionEntity.serviceId, subscriptionEntity.serviceId,
linkHandler, channelTabInfo.nextPage linkHandler,
channelTabInfo.nextPage
) )
.blockingGet() .blockingGet()
@ -234,7 +239,7 @@ class FeedLoadManager(private val context: Context) {
subscriptionEntity, subscriptionEntity,
originalInfo!!, originalInfo!!,
streams!!, streams!!,
errors, errors
) )
) )
} catch (e: Throwable) { } catch (e: Throwable) {
@ -305,6 +310,7 @@ class FeedLoadManager(private val context: Context) {
feedDatabaseManager.markAsOutdated(info.uid) feedDatabaseManager.markAsOutdated(info.uid)
} }
} }
notification.isOnError -> { notification.isOnError -> {
val error = notification.error val error = notification.error
feedResultsHolder.addError(error!!) feedResultsHolder.addError(error!!)

View file

@ -36,13 +36,13 @@ import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
import io.reactivex.rxjava3.core.Flowable import io.reactivex.rxjava3.core.Flowable
import io.reactivex.rxjava3.disposables.Disposable import io.reactivex.rxjava3.disposables.Disposable
import io.reactivex.rxjava3.functions.Function import io.reactivex.rxjava3.functions.Function
import java.util.concurrent.TimeUnit
import org.schabi.newpipe.App import org.schabi.newpipe.App
import org.schabi.newpipe.MainActivity.DEBUG import org.schabi.newpipe.MainActivity.DEBUG
import org.schabi.newpipe.R import org.schabi.newpipe.R
import org.schabi.newpipe.database.feed.model.FeedGroupEntity import org.schabi.newpipe.database.feed.model.FeedGroupEntity
import org.schabi.newpipe.local.feed.service.FeedEventManager.Event.ErrorResultEvent import org.schabi.newpipe.local.feed.service.FeedEventManager.Event.ErrorResultEvent
import org.schabi.newpipe.local.feed.service.FeedEventManager.postEvent import org.schabi.newpipe.local.feed.service.FeedEventManager.postEvent
import java.util.concurrent.TimeUnit
class FeedLoadService : Service() { class FeedLoadService : Service() {
companion object { companion object {
@ -94,7 +94,8 @@ class FeedLoadService : Service() {
.doOnSubscribe { .doOnSubscribe {
startForeground(NOTIFICATION_ID, notificationBuilder.build()) startForeground(NOTIFICATION_ID, notificationBuilder.build())
} }
.subscribe { _, error: Throwable? -> // explicitly mark error as nullable .subscribe { _, error: Throwable? ->
// explicitly mark error as nullable
if (error != null) { if (error != null) {
Log.e(TAG, "Error while storing result", error) Log.e(TAG, "Error while storing result", error)
handleError(error) handleError(error)

View file

@ -3,5 +3,5 @@ package org.schabi.newpipe.local.feed.service
data class FeedLoadState( data class FeedLoadState(
val updateDescription: String, val updateDescription: String,
val maxProgress: Int, val maxProgress: Int,
val currentProgress: Int, val currentProgress: Int
) )

View file

@ -25,13 +25,13 @@ data class FeedUpdateInfo(
val description: String?, val description: String?,
val subscriberCount: Long?, val subscriberCount: Long?,
val streams: List<StreamInfoItem>, val streams: List<StreamInfoItem>,
val errors: List<Throwable>, val errors: List<Throwable>
) { ) {
constructor( constructor(
subscription: SubscriptionEntity, subscription: SubscriptionEntity,
info: Info, info: Info,
streams: List<StreamInfoItem>, streams: List<StreamInfoItem>,
errors: List<Throwable>, errors: List<Throwable>
) : this( ) : this(
uid = subscription.uid, uid = subscription.uid,
notificationMode = subscription.notificationMode, notificationMode = subscription.notificationMode,
@ -46,7 +46,7 @@ data class FeedUpdateInfo(
description = (info as? ChannelInfo)?.description, description = (info as? ChannelInfo)?.description,
subscriberCount = (info as? ChannelInfo)?.subscriberCount, subscriberCount = (info as? ChannelInfo)?.subscriberCount,
streams = streams, streams = streams,
errors = errors, errors = errors
) )
/** /**

View file

@ -44,7 +44,6 @@ private fun exportJustUrls(playlist: List<PlaylistStreamEntry>): String {
} }
private fun exportAsYoutubeTempPlaylist(playlist: List<PlaylistStreamEntry>): String { private fun exportAsYoutubeTempPlaylist(playlist: List<PlaylistStreamEntry>): String {
val videoIDs = playlist.asReversed().asSequence() val videoIDs = playlist.asReversed().asSequence()
.mapNotNull { getYouTubeId(it.streamEntity.url) } .mapNotNull { getYouTubeId(it.streamEntity.url) }
.take(50) // YouTube limitation: temp playlists can't have more than 50 items .take(50) // YouTube limitation: temp playlists can't have more than 50 items
@ -64,6 +63,5 @@ private val linkHandler: YoutubeStreamLinkHandlerFactory = YoutubeStreamLinkHand
* @return the video id * @return the video id
*/ */
private fun getYouTubeId(url: String): String? { private fun getYouTubeId(url: String): String? {
return runCatching { linkHandler.getId(url) }.getOrNull() return runCatching { linkHandler.getId(url) }.getOrNull()
} }

View file

@ -27,6 +27,9 @@ import com.xwray.groupie.GroupAdapter
import com.xwray.groupie.Section import com.xwray.groupie.Section
import com.xwray.groupie.viewbinding.GroupieViewHolder import com.xwray.groupie.viewbinding.GroupieViewHolder
import io.reactivex.rxjava3.disposables.CompositeDisposable import io.reactivex.rxjava3.disposables.CompositeDisposable
import java.text.SimpleDateFormat
import java.util.Date
import java.util.Locale
import org.schabi.newpipe.R import org.schabi.newpipe.R
import org.schabi.newpipe.database.feed.model.FeedGroupEntity.Companion.GROUP_ALL_ID import org.schabi.newpipe.database.feed.model.FeedGroupEntity.Companion.GROUP_ALL_ID
import org.schabi.newpipe.databinding.DialogTitleBinding import org.schabi.newpipe.databinding.DialogTitleBinding
@ -62,9 +65,6 @@ import org.schabi.newpipe.util.OnClickGesture
import org.schabi.newpipe.util.ServiceHelper import org.schabi.newpipe.util.ServiceHelper
import org.schabi.newpipe.util.ThemeHelper.getGridSpanCountChannels import org.schabi.newpipe.util.ThemeHelper.getGridSpanCountChannels
import org.schabi.newpipe.util.external_communication.ShareUtils import org.schabi.newpipe.util.external_communication.ShareUtils
import java.text.SimpleDateFormat
import java.util.Date
import java.util.Locale
class SubscriptionFragment : BaseStateFragment<SubscriptionState>() { class SubscriptionFragment : BaseStateFragment<SubscriptionState>() {
private var _binding: FragmentSubscriptionBinding? = null private var _binding: FragmentSubscriptionBinding? = null
@ -276,10 +276,13 @@ class SubscriptionFragment : BaseStateFragment<SubscriptionState>() {
when (item) { when (item) {
is FeedGroupCardItem -> is FeedGroupCardItem ->
NavigationHelper.openFeedFragment(fm, item.groupId, item.name) NavigationHelper.openFeedFragment(fm, item.groupId, item.name)
is FeedGroupCardGridItem -> is FeedGroupCardGridItem ->
NavigationHelper.openFeedFragment(fm, item.groupId, item.name) NavigationHelper.openFeedFragment(fm, item.groupId, item.name)
is FeedGroupAddNewItem -> is FeedGroupAddNewItem ->
FeedGroupDialog.newInstance().show(fm, null) FeedGroupDialog.newInstance().show(fm, null)
is FeedGroupAddNewGridItem -> is FeedGroupAddNewGridItem ->
FeedGroupDialog.newInstance().show(fm, null) FeedGroupDialog.newInstance().show(fm, null)
} }
@ -294,6 +297,7 @@ class SubscriptionFragment : BaseStateFragment<SubscriptionState>() {
when (item) { when (item) {
is FeedGroupCardItem -> is FeedGroupCardItem ->
FeedGroupDialog.newInstance(item.groupId).show(fm, null) FeedGroupDialog.newInstance(item.groupId).show(fm, null)
is FeedGroupCardGridItem -> is FeedGroupCardGridItem ->
FeedGroupDialog.newInstance(item.groupId).show(fm, null) FeedGroupDialog.newInstance(item.groupId).show(fm, null)
} }
@ -309,7 +313,7 @@ class SubscriptionFragment : BaseStateFragment<SubscriptionState>() {
title = getString(R.string.feed_groups_header_title), title = getString(R.string.feed_groups_header_title),
onSortClicked = ::openReorderDialog, onSortClicked = ::openReorderDialog,
onToggleListViewModeClicked = ::toggleListViewMode, onToggleListViewModeClicked = ::toggleListViewMode,
listViewMode = viewModel.getListViewMode(), listViewMode = viewModel.getListViewMode()
) )
add(Section(feedGroupsSortMenuItem, listOf(feedGroupsCarousel))) add(Section(feedGroupsSortMenuItem, listOf(feedGroupsCarousel)))
@ -342,9 +346,14 @@ class SubscriptionFragment : BaseStateFragment<SubscriptionState>() {
val actions = DialogInterface.OnClickListener { _, i -> val actions = DialogInterface.OnClickListener { _, i ->
when (i) { when (i) {
0 -> ShareUtils.shareText( 0 -> ShareUtils.shareText(
requireContext(), selectedItem.name, selectedItem.url, selectedItem.thumbnails requireContext(),
selectedItem.name,
selectedItem.url,
selectedItem.thumbnails
) )
1 -> ShareUtils.openUrlInBrowser(requireContext(), selectedItem.url) 1 -> ShareUtils.openUrlInBrowser(requireContext(), selectedItem.url)
2 -> deleteChannel(selectedItem) 2 -> deleteChannel(selectedItem)
} }
} }
@ -374,7 +383,9 @@ class SubscriptionFragment : BaseStateFragment<SubscriptionState>() {
private val listenerChannelItem = object : OnClickGesture<ChannelInfoItem> { private val listenerChannelItem = object : OnClickGesture<ChannelInfoItem> {
override fun selected(selectedItem: ChannelInfoItem) = NavigationHelper.openChannelFragment( override fun selected(selectedItem: ChannelInfoItem) = NavigationHelper.openChannelFragment(
fm, fm,
selectedItem.serviceId, selectedItem.url, selectedItem.name selectedItem.serviceId,
selectedItem.url,
selectedItem.name
) )
override fun held(selectedItem: ChannelInfoItem) = showLongTapDialog(selectedItem) override fun held(selectedItem: ChannelInfoItem) = showLongTapDialog(selectedItem)
@ -404,6 +415,7 @@ class SubscriptionFragment : BaseStateFragment<SubscriptionState>() {
itemsListState = null itemsListState = null
} }
} }
is SubscriptionState.ErrorState -> { is SubscriptionState.ErrorState -> {
result.error?.let { result.error?.let {
showError(ErrorInfo(result.error, UserAction.SOMETHING_ELSE, "Subscriptions")) showError(ErrorInfo(result.error, UserAction.SOMETHING_ELSE, "Subscriptions"))

View file

@ -37,13 +37,16 @@ class SubscriptionManager(context: Context) {
filterQuery.isNotEmpty() -> { filterQuery.isNotEmpty() -> {
return if (showOnlyUngrouped) { return if (showOnlyUngrouped) {
subscriptionTable.getSubscriptionsOnlyUngroupedFiltered( subscriptionTable.getSubscriptionsOnlyUngroupedFiltered(
currentGroupId, filterQuery currentGroupId,
filterQuery
) )
} else { } else {
subscriptionTable.getSubscriptionsFiltered(filterQuery) subscriptionTable.getSubscriptionsFiltered(filterQuery)
} }
} }
showOnlyUngrouped -> subscriptionTable.getSubscriptionsOnlyUngrouped(currentGroupId) showOnlyUngrouped -> subscriptionTable.getSubscriptionsOnlyUngrouped(currentGroupId)
else -> subscriptionTable.getAll() else -> subscriptionTable.getAll()
} }
} }
@ -67,19 +70,18 @@ class SubscriptionManager(context: Context) {
return listEntities return listEntities
} }
fun updateChannelInfo(info: ChannelInfo): Completable = fun updateChannelInfo(info: ChannelInfo): Completable = subscriptionTable.getSubscription(info.serviceId, info.url)
subscriptionTable.getSubscription(info.serviceId, info.url) .flatMapCompletable {
.flatMapCompletable { Completable.fromRunnable {
Completable.fromRunnable { it.apply {
it.apply { name = info.name
name = info.name avatarUrl = ImageStrategy.imageListToDbUrl(info.avatars)
avatarUrl = ImageStrategy.imageListToDbUrl(info.avatars) description = info.description
description = info.description subscriberCount = info.subscriberCount
subscriberCount = info.subscriberCount
}
subscriptionTable.update(it)
} }
subscriptionTable.update(it)
} }
}
fun updateNotificationMode(serviceId: Int, url: String, @NotificationMode mode: Int): Completable { fun updateNotificationMode(serviceId: Int, url: String, @NotificationMode mode: Int): Completable {
return subscriptionTable().getSubscription(serviceId, url) return subscriptionTable().getSubscription(serviceId, url)

View file

@ -9,6 +9,7 @@ import com.xwray.groupie.Group
import io.reactivex.rxjava3.core.Flowable import io.reactivex.rxjava3.core.Flowable
import io.reactivex.rxjava3.processors.BehaviorProcessor import io.reactivex.rxjava3.processors.BehaviorProcessor
import io.reactivex.rxjava3.schedulers.Schedulers import io.reactivex.rxjava3.schedulers.Schedulers
import java.util.concurrent.TimeUnit
import org.schabi.newpipe.info_list.ItemViewMode import org.schabi.newpipe.info_list.ItemViewMode
import org.schabi.newpipe.local.feed.FeedDatabaseManager import org.schabi.newpipe.local.feed.FeedDatabaseManager
import org.schabi.newpipe.local.subscription.item.ChannelItem import org.schabi.newpipe.local.subscription.item.ChannelItem
@ -16,7 +17,6 @@ import org.schabi.newpipe.local.subscription.item.FeedGroupCardGridItem
import org.schabi.newpipe.local.subscription.item.FeedGroupCardItem import org.schabi.newpipe.local.subscription.item.FeedGroupCardItem
import org.schabi.newpipe.util.DEFAULT_THROTTLE_TIMEOUT import org.schabi.newpipe.util.DEFAULT_THROTTLE_TIMEOUT
import org.schabi.newpipe.util.ThemeHelper.getItemViewMode import org.schabi.newpipe.util.ThemeHelper.getItemViewMode
import java.util.concurrent.TimeUnit
class SubscriptionViewModel(application: Application) : AndroidViewModel(application) { class SubscriptionViewModel(application: Application) : AndroidViewModel(application) {
private var feedDatabaseManager: FeedDatabaseManager = FeedDatabaseManager(application) private var feedDatabaseManager: FeedDatabaseManager = FeedDatabaseManager(application)

View file

@ -23,6 +23,7 @@ import com.livefront.bridge.Bridge
import com.xwray.groupie.GroupieAdapter import com.xwray.groupie.GroupieAdapter
import com.xwray.groupie.OnItemClickListener import com.xwray.groupie.OnItemClickListener
import com.xwray.groupie.Section import com.xwray.groupie.Section
import java.io.Serializable
import org.schabi.newpipe.R import org.schabi.newpipe.R
import org.schabi.newpipe.database.feed.model.FeedGroupEntity import org.schabi.newpipe.database.feed.model.FeedGroupEntity
import org.schabi.newpipe.databinding.DialogFeedGroupCreateBinding import org.schabi.newpipe.databinding.DialogFeedGroupCreateBinding
@ -40,7 +41,6 @@ import org.schabi.newpipe.local.subscription.item.PickerIconItem
import org.schabi.newpipe.local.subscription.item.PickerSubscriptionItem import org.schabi.newpipe.local.subscription.item.PickerSubscriptionItem
import org.schabi.newpipe.util.DeviceUtils import org.schabi.newpipe.util.DeviceUtils
import org.schabi.newpipe.util.ThemeHelper import org.schabi.newpipe.util.ThemeHelper
import java.io.Serializable
class FeedGroupDialog : DialogFragment(), BackPressable { class FeedGroupDialog : DialogFragment(), BackPressable {
private var _feedGroupCreateBinding: DialogFeedGroupCreateBinding? = null private var _feedGroupCreateBinding: DialogFeedGroupCreateBinding? = null
@ -61,16 +61,41 @@ class FeedGroupDialog : DialogFragment(), BackPressable {
data object DeleteScreen : ScreenState() data object DeleteScreen : ScreenState()
} }
@State @JvmField var selectedIcon: FeedGroupIcon? = null @State
@State @JvmField var selectedSubscriptions: HashSet<Long> = HashSet() @JvmField
@State @JvmField var wasSubscriptionSelectionChanged: Boolean = false var selectedIcon: FeedGroupIcon? = null
@State @JvmField var currentScreen: ScreenState = InitialScreen
@State @JvmField var subscriptionsListState: Parcelable? = null @State
@State @JvmField var iconsListState: Parcelable? = null @JvmField
@State @JvmField var wasSearchSubscriptionsVisible = false var selectedSubscriptions: HashSet<Long> = HashSet()
@State @JvmField var subscriptionsCurrentSearchQuery = ""
@State @JvmField var subscriptionsShowOnlyUngrouped = false @State
@JvmField
var wasSubscriptionSelectionChanged: Boolean = false
@State
@JvmField
var currentScreen: ScreenState = InitialScreen
@State
@JvmField
var subscriptionsListState: Parcelable? = null
@State
@JvmField
var iconsListState: Parcelable? = null
@State
@JvmField
var wasSearchSubscriptionsVisible = false
@State
@JvmField
var subscriptionsCurrentSearchQuery = ""
@State
@JvmField
var subscriptionsShowOnlyUngrouped = false
private val subscriptionMainSection = Section() private val subscriptionMainSection = Section()
private val subscriptionEmptyFooter = Section() private val subscriptionEmptyFooter = Section()
@ -153,8 +178,10 @@ class FeedGroupDialog : DialogFragment(), BackPressable {
itemAnimator = null itemAnimator = null
adapter = subscriptionGroupAdapter adapter = subscriptionGroupAdapter
layoutManager = GridLayoutManager( layoutManager = GridLayoutManager(
requireContext(), subscriptionGroupAdapter.spanCount, requireContext(),
RecyclerView.VERTICAL, false subscriptionGroupAdapter.spanCount,
RecyclerView.VERTICAL,
false
).apply { ).apply {
spanSizeLookup = subscriptionGroupAdapter.spanSizeLookup spanSizeLookup = subscriptionGroupAdapter.spanSizeLookup
} }
@ -362,7 +389,8 @@ class FeedGroupDialog : DialogFragment(), BackPressable {
val selectedCount = this.selectedSubscriptions.size val selectedCount = this.selectedSubscriptions.size
val selectedCountText = resources.getQuantityString( val selectedCountText = resources.getQuantityString(
R.plurals.feed_group_dialog_selection_count, R.plurals.feed_group_dialog_selection_count,
selectedCount, selectedCount selectedCount,
selectedCount
) )
feedGroupCreateBinding.selectedSubscriptionCountView.text = selectedCountText feedGroupCreateBinding.selectedSubscriptionCountView.text = selectedCountText
feedGroupCreateBinding.subscriptionsHeaderInfo.text = selectedCountText feedGroupCreateBinding.subscriptionsHeaderInfo.text = selectedCountText

View file

@ -55,7 +55,8 @@ class FeedGroupDialogViewModel(
private var subscriptionsDisposable = Flowable private var subscriptionsDisposable = Flowable
.combineLatest( .combineLatest(
subscriptionsFlowable, feedDatabaseManager.subscriptionIdsForGroup(groupId) subscriptionsFlowable,
feedDatabaseManager.subscriptionIdsForGroup(groupId)
) { t1: List<PickerSubscriptionItem>, t2: List<Long> -> t1 to t2.toSet() } ) { t1: List<PickerSubscriptionItem>, t2: List<Long> -> t1 to t2.toSet() }
.subscribeOn(Schedulers.io()) .subscribeOn(Schedulers.io())
.subscribe(mutableSubscriptionsLiveData::postValue) .subscribe(mutableSubscriptionsLiveData::postValue)
@ -125,7 +126,10 @@ class FeedGroupDialogViewModel(
) = viewModelFactory { ) = viewModelFactory {
initializer { initializer {
FeedGroupDialogViewModel( FeedGroupDialogViewModel(
context.applicationContext, groupId, initialQuery, initialShowOnlyUngrouped context.applicationContext,
groupId,
initialQuery,
initialShowOnlyUngrouped
) )
} }
} }

View file

@ -15,6 +15,7 @@ import com.evernote.android.state.State
import com.livefront.bridge.Bridge import com.livefront.bridge.Bridge
import com.xwray.groupie.GroupieAdapter import com.xwray.groupie.GroupieAdapter
import com.xwray.groupie.TouchCallback import com.xwray.groupie.TouchCallback
import java.util.Collections
import org.schabi.newpipe.R import org.schabi.newpipe.R
import org.schabi.newpipe.database.feed.model.FeedGroupEntity import org.schabi.newpipe.database.feed.model.FeedGroupEntity
import org.schabi.newpipe.databinding.DialogFeedGroupReorderBinding import org.schabi.newpipe.databinding.DialogFeedGroupReorderBinding
@ -22,7 +23,6 @@ import org.schabi.newpipe.local.subscription.dialog.FeedGroupReorderDialogViewMo
import org.schabi.newpipe.local.subscription.dialog.FeedGroupReorderDialogViewModel.DialogEvent.SuccessEvent import org.schabi.newpipe.local.subscription.dialog.FeedGroupReorderDialogViewModel.DialogEvent.SuccessEvent
import org.schabi.newpipe.local.subscription.item.FeedGroupReorderItem import org.schabi.newpipe.local.subscription.item.FeedGroupReorderItem
import org.schabi.newpipe.util.ThemeHelper import org.schabi.newpipe.util.ThemeHelper
import java.util.Collections
class FeedGroupReorderDialog : DialogFragment() { class FeedGroupReorderDialog : DialogFragment() {
private var _binding: DialogFeedGroupReorderBinding? = null private var _binding: DialogFeedGroupReorderBinding? = null

View file

@ -43,7 +43,10 @@ class ChannelItem(
gesturesListener?.run { gesturesListener?.run {
viewHolder.root.setOnClickListener { selected(infoItem) } viewHolder.root.setOnClickListener { selected(infoItem) }
viewHolder.root.setOnLongClickListener { held(infoItem); true } viewHolder.root.setOnLongClickListener {
held(infoItem)
true
}
} }
} }

View file

@ -10,7 +10,7 @@ import org.schabi.newpipe.local.subscription.FeedGroupIcon
data class FeedGroupCardGridItem( data class FeedGroupCardGridItem(
val groupId: Long = FeedGroupEntity.GROUP_ALL_ID, val groupId: Long = FeedGroupEntity.GROUP_ALL_ID,
val name: String, val name: String,
val icon: FeedGroupIcon, val icon: FeedGroupIcon
) : BindableItem<FeedGroupCardGridItemBinding>() { ) : BindableItem<FeedGroupCardGridItemBinding>() {
constructor (feedGroupEntity: FeedGroupEntity) : this(feedGroupEntity.uid, feedGroupEntity.name, feedGroupEntity.icon) constructor (feedGroupEntity: FeedGroupEntity) : this(feedGroupEntity.uid, feedGroupEntity.name, feedGroupEntity.icon)

View file

@ -18,7 +18,7 @@ import org.schabi.newpipe.player.ui.VideoPlayerUi
* and provides some abstract methods to make it easier separating the logic from the UI. * and provides some abstract methods to make it easier separating the logic from the UI.
*/ */
abstract class BasePlayerGestureListener( abstract class BasePlayerGestureListener(
private val playerUi: VideoPlayerUi, private val playerUi: VideoPlayerUi
) : GestureDetector.SimpleOnGestureListener(), View.OnTouchListener { ) : GestureDetector.SimpleOnGestureListener(), View.OnTouchListener {
protected val player: Player = playerUi.player protected val player: Player = playerUi.player
@ -86,8 +86,9 @@ abstract class BasePlayerGestureListener(
// /////////////////////////////////////////////////////////////////// // ///////////////////////////////////////////////////////////////////
override fun onDown(e: MotionEvent): Boolean { override fun onDown(e: MotionEvent): Boolean {
if (DEBUG) if (DEBUG) {
Log.d(TAG, "onDown called with e = [$e]") Log.d(TAG, "onDown called with e = [$e]")
}
if (isDoubleTapping && isDoubleTapEnabled) { if (isDoubleTapping && isDoubleTapEnabled) {
doubleTapControls?.onDoubleTapProgressDown(getDisplayPortion(e)) doubleTapControls?.onDoubleTapProgressDown(getDisplayPortion(e))
@ -108,8 +109,9 @@ abstract class BasePlayerGestureListener(
} }
override fun onDoubleTap(e: MotionEvent): Boolean { override fun onDoubleTap(e: MotionEvent): Boolean {
if (DEBUG) if (DEBUG) {
Log.d(TAG, "onDoubleTap called with e = [$e]") Log.d(TAG, "onDoubleTap called with e = [$e]")
}
onDoubleTap(e, getDisplayPortion(e)) onDoubleTap(e, getDisplayPortion(e))
return true return true
@ -136,8 +138,9 @@ abstract class BasePlayerGestureListener(
private fun startMultiDoubleTap(e: MotionEvent) { private fun startMultiDoubleTap(e: MotionEvent) {
if (!isDoubleTapping) { if (!isDoubleTapping) {
if (DEBUG) if (DEBUG) {
Log.d(TAG, "startMultiDoubleTap called with e = [$e]") Log.d(TAG, "startMultiDoubleTap called with e = [$e]")
}
keepInDoubleTapMode() keepInDoubleTapMode()
doubleTapControls?.onDoubleTapStarted(getDisplayPortion(e)) doubleTapControls?.onDoubleTapStarted(getDisplayPortion(e))
@ -145,8 +148,9 @@ abstract class BasePlayerGestureListener(
} }
fun keepInDoubleTapMode() { fun keepInDoubleTapMode() {
if (DEBUG) if (DEBUG) {
Log.d(TAG, "keepInDoubleTapMode called") Log.d(TAG, "keepInDoubleTapMode called")
}
isDoubleTapping = true isDoubleTapping = true
doubleTapHandler.removeCallbacksAndMessages(DOUBLE_TAP) doubleTapHandler.removeCallbacksAndMessages(DOUBLE_TAP)
@ -161,8 +165,9 @@ abstract class BasePlayerGestureListener(
} }
fun endMultiDoubleTap() { fun endMultiDoubleTap() {
if (DEBUG) if (DEBUG) {
Log.d(TAG, "endMultiDoubleTap called") Log.d(TAG, "endMultiDoubleTap called")
}
isDoubleTapping = false isDoubleTapping = false
doubleTapHandler.removeCallbacksAndMessages(DOUBLE_TAP) doubleTapHandler.removeCallbacksAndMessages(DOUBLE_TAP)

View file

@ -1,5 +1,9 @@
package org.schabi.newpipe.player.gesture package org.schabi.newpipe.player.gesture
enum class DisplayPortion { enum class DisplayPortion {
LEFT, MIDDLE, RIGHT, LEFT_HALF, RIGHT_HALF LEFT,
MIDDLE,
RIGHT,
LEFT_HALF,
RIGHT_HALF
} }

View file

@ -8,6 +8,7 @@ import android.widget.ProgressBar
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.content.res.AppCompatResources import androidx.appcompat.content.res.AppCompatResources
import androidx.core.view.isVisible import androidx.core.view.isVisible
import kotlin.math.abs
import org.schabi.newpipe.MainActivity import org.schabi.newpipe.MainActivity
import org.schabi.newpipe.R import org.schabi.newpipe.R
import org.schabi.newpipe.ktx.AnimationType import org.schabi.newpipe.ktx.AnimationType
@ -17,7 +18,6 @@ import org.schabi.newpipe.player.helper.AudioReactor
import org.schabi.newpipe.player.helper.PlayerHelper import org.schabi.newpipe.player.helper.PlayerHelper
import org.schabi.newpipe.player.ui.MainPlayerUi import org.schabi.newpipe.player.ui.MainPlayerUi
import org.schabi.newpipe.util.ThemeHelper.getAndroidDimenPx import org.schabi.newpipe.util.ThemeHelper.getAndroidDimenPx
import kotlin.math.abs
/** /**
* GestureListener for the player * GestureListener for the player
@ -42,24 +42,29 @@ class MainPlayerGestureListener(
v.parent?.requestDisallowInterceptTouchEvent(playerUi.isFullscreen) v.parent?.requestDisallowInterceptTouchEvent(playerUi.isFullscreen)
true true
} }
MotionEvent.ACTION_UP -> { MotionEvent.ACTION_UP -> {
v.parent?.requestDisallowInterceptTouchEvent(false) v.parent?.requestDisallowInterceptTouchEvent(false)
false false
} }
else -> true else -> true
} }
} }
override fun onSingleTapConfirmed(e: MotionEvent): Boolean { override fun onSingleTapConfirmed(e: MotionEvent): Boolean {
if (DEBUG) if (DEBUG) {
Log.d(TAG, "onSingleTapConfirmed() called with: e = [$e]") Log.d(TAG, "onSingleTapConfirmed() called with: e = [$e]")
}
if (isDoubleTapping) if (isDoubleTapping) {
return true return true
}
super.onSingleTapConfirmed(e) super.onSingleTapConfirmed(e)
if (player.currentState != Player.STATE_BLOCKED) if (player.currentState != Player.STATE_BLOCKED) {
onSingleTap() onSingleTap()
}
return true return true
} }
@ -195,6 +200,7 @@ class MainPlayerGestureListener(
when (PlayerHelper.getActionForRightGestureSide(player.context)) { when (PlayerHelper.getActionForRightGestureSide(player.context)) {
player.context.getString(R.string.volume_control_key) -> player.context.getString(R.string.volume_control_key) ->
onScrollVolume(distanceY) onScrollVolume(distanceY)
player.context.getString(R.string.brightness_control_key) -> player.context.getString(R.string.brightness_control_key) ->
onScrollBrightness(distanceY) onScrollBrightness(distanceY)
} }
@ -202,6 +208,7 @@ class MainPlayerGestureListener(
when (PlayerHelper.getActionForLeftGestureSide(player.context)) { when (PlayerHelper.getActionForLeftGestureSide(player.context)) {
player.context.getString(R.string.volume_control_key) -> player.context.getString(R.string.volume_control_key) ->
onScrollVolume(distanceY) onScrollVolume(distanceY)
player.context.getString(R.string.brightness_control_key) -> player.context.getString(R.string.brightness_control_key) ->
onScrollBrightness(distanceY) onScrollBrightness(distanceY)
} }

View file

@ -5,17 +5,17 @@ import android.view.MotionEvent
import android.view.View import android.view.View
import android.view.ViewConfiguration import android.view.ViewConfiguration
import androidx.core.view.isVisible import androidx.core.view.isVisible
import org.schabi.newpipe.MainActivity
import org.schabi.newpipe.ktx.AnimationType
import org.schabi.newpipe.ktx.animate
import org.schabi.newpipe.player.ui.PopupPlayerUi
import kotlin.math.abs import kotlin.math.abs
import kotlin.math.hypot import kotlin.math.hypot
import kotlin.math.max import kotlin.math.max
import kotlin.math.min import kotlin.math.min
import org.schabi.newpipe.MainActivity
import org.schabi.newpipe.ktx.AnimationType
import org.schabi.newpipe.ktx.animate
import org.schabi.newpipe.player.ui.PopupPlayerUi
class PopupPlayerGestureListener( class PopupPlayerGestureListener(
private val playerUi: PopupPlayerUi, private val playerUi: PopupPlayerUi
) : BasePlayerGestureListener(playerUi) { ) : BasePlayerGestureListener(playerUi) {
private var isMoving = false private var isMoving = false
@ -205,13 +205,16 @@ class PopupPlayerGestureListener(
} }
override fun onSingleTapConfirmed(e: MotionEvent): Boolean { override fun onSingleTapConfirmed(e: MotionEvent): Boolean {
if (DEBUG) if (DEBUG) {
Log.d(TAG, "onSingleTapConfirmed() called with: e = [$e]") Log.d(TAG, "onSingleTapConfirmed() called with: e = [$e]")
}
if (isDoubleTapping) if (isDoubleTapping) {
return true return true
if (player.exoPlayerIsNull()) }
if (player.exoPlayerIsNull()) {
return false return false
}
onSingleTap() onSingleTap()
return true return true

View file

@ -17,6 +17,7 @@ import io.reactivex.rxjava3.core.Flowable
import io.reactivex.rxjava3.core.Single import io.reactivex.rxjava3.core.Single
import io.reactivex.rxjava3.disposables.CompositeDisposable import io.reactivex.rxjava3.disposables.CompositeDisposable
import io.reactivex.rxjava3.schedulers.Schedulers import io.reactivex.rxjava3.schedulers.Schedulers
import java.util.function.Consumer
import org.schabi.newpipe.MainActivity.DEBUG import org.schabi.newpipe.MainActivity.DEBUG
import org.schabi.newpipe.NewPipeDatabase import org.schabi.newpipe.NewPipeDatabase
import org.schabi.newpipe.R import org.schabi.newpipe.R
@ -37,7 +38,6 @@ import org.schabi.newpipe.local.playlist.RemotePlaylistManager
import org.schabi.newpipe.util.ExtractorHelper import org.schabi.newpipe.util.ExtractorHelper
import org.schabi.newpipe.util.ServiceHelper import org.schabi.newpipe.util.ServiceHelper
import org.schabi.newpipe.util.image.ImageStrategy import org.schabi.newpipe.util.image.ImageStrategy
import java.util.function.Consumer
/** /**
* This class is used to cleanly separate the Service implementation (in * This class is used to cleanly separate the Service implementation (in
@ -47,7 +47,8 @@ import java.util.function.Consumer
*/ */
class MediaBrowserImpl( class MediaBrowserImpl(
private val context: Context, private val context: Context,
notifyChildrenChanged: Consumer<String>, // parentId // parentId
notifyChildrenChanged: Consumer<String>
) { ) {
private val packageValidator = PackageValidator(context) private val packageValidator = PackageValidator(context)
private val database = NewPipeDatabase.getInstance(context) private val database = NewPipeDatabase.getInstance(context)
@ -89,7 +90,8 @@ class MediaBrowserImpl(
val extras = Bundle() val extras = Bundle()
extras.putBoolean( extras.putBoolean(
MediaConstants.BROWSER_SERVICE_EXTRAS_KEY_SEARCH_SUPPORTED, true MediaConstants.BROWSER_SERVICE_EXTRAS_KEY_SEARCH_SUPPORTED,
true
) )
return MediaBrowserServiceCompat.BrowserRoot(ID_ROOT, extras) return MediaBrowserServiceCompat.BrowserRoot(ID_ROOT, extras)
} }
@ -137,7 +139,7 @@ class MediaBrowserImpl(
) )
} }
when (/*val uriType = */path.removeAt(0)) { when (path.removeAt(0)) {
ID_BOOKMARKS -> { ID_BOOKMARKS -> {
if (path.isEmpty()) { if (path.isEmpty()) {
return populateBookmarks() return populateBookmarks()
@ -204,12 +206,12 @@ class MediaBrowserImpl(
val extras = Bundle() val extras = Bundle()
extras.putString( extras.putString(
MediaConstants.DESCRIPTION_EXTRAS_KEY_CONTENT_STYLE_GROUP_TITLE, MediaConstants.DESCRIPTION_EXTRAS_KEY_CONTENT_STYLE_GROUP_TITLE,
context.resources.getString(R.string.tab_bookmarks), context.resources.getString(R.string.tab_bookmarks)
) )
builder.setExtras(extras) builder.setExtras(extras)
return MediaBrowserCompat.MediaItem( return MediaBrowserCompat.MediaItem(
builder.build(), builder.build(),
MediaBrowserCompat.MediaItem.FLAG_BROWSABLE, MediaBrowserCompat.MediaItem.FLAG_BROWSABLE
) )
} }
@ -266,7 +268,7 @@ class MediaBrowserImpl(
private fun createLocalPlaylistStreamMediaItem( private fun createLocalPlaylistStreamMediaItem(
playlistId: Long, playlistId: Long,
item: PlaylistStreamEntry, item: PlaylistStreamEntry,
index: Int, index: Int
): MediaBrowserCompat.MediaItem { ): MediaBrowserCompat.MediaItem {
val builder = MediaDescriptionCompat.Builder() val builder = MediaDescriptionCompat.Builder()
builder.setMediaId(createMediaIdForPlaylistIndex(false, playlistId, index)) builder.setMediaId(createMediaIdForPlaylistIndex(false, playlistId, index))
@ -283,7 +285,7 @@ class MediaBrowserImpl(
private fun createRemotePlaylistStreamMediaItem( private fun createRemotePlaylistStreamMediaItem(
playlistId: Long, playlistId: Long,
item: StreamInfoItem, item: StreamInfoItem,
index: Int, index: Int
): MediaBrowserCompat.MediaItem { ): MediaBrowserCompat.MediaItem {
val builder = MediaDescriptionCompat.Builder() val builder = MediaDescriptionCompat.Builder()
builder.setMediaId(createMediaIdForPlaylistIndex(true, playlistId, index)) builder.setMediaId(createMediaIdForPlaylistIndex(true, playlistId, index))
@ -303,7 +305,7 @@ class MediaBrowserImpl(
private fun createMediaIdForPlaylistIndex( private fun createMediaIdForPlaylistIndex(
isRemote: Boolean, isRemote: Boolean,
playlistId: Long, playlistId: Long,
index: Int, index: Int
): String { ): String {
return buildLocalPlaylistItemMediaId(isRemote, playlistId) return buildLocalPlaylistItemMediaId(isRemote, playlistId)
.appendPath(index.toString()) .appendPath(index.toString())

View file

@ -14,6 +14,8 @@ import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
import io.reactivex.rxjava3.core.Single import io.reactivex.rxjava3.core.Single
import io.reactivex.rxjava3.disposables.Disposable import io.reactivex.rxjava3.disposables.Disposable
import io.reactivex.rxjava3.schedulers.Schedulers import io.reactivex.rxjava3.schedulers.Schedulers
import java.util.function.BiConsumer
import java.util.function.Consumer
import org.schabi.newpipe.MainActivity import org.schabi.newpipe.MainActivity
import org.schabi.newpipe.NewPipeDatabase import org.schabi.newpipe.NewPipeDatabase
import org.schabi.newpipe.R import org.schabi.newpipe.R
@ -30,8 +32,6 @@ import org.schabi.newpipe.player.playqueue.SinglePlayQueue
import org.schabi.newpipe.util.ChannelTabHelper import org.schabi.newpipe.util.ChannelTabHelper
import org.schabi.newpipe.util.ExtractorHelper import org.schabi.newpipe.util.ExtractorHelper
import org.schabi.newpipe.util.NavigationHelper import org.schabi.newpipe.util.NavigationHelper
import java.util.function.BiConsumer
import java.util.function.Consumer
/** /**
* This class is used to cleanly separate the Service implementation (in * This class is used to cleanly separate the Service implementation (in
@ -51,7 +51,7 @@ class MediaBrowserPlaybackPreparer(
private val context: Context, private val context: Context,
private val setMediaSessionError: BiConsumer<String, Int>, // error string, error code private val setMediaSessionError: BiConsumer<String, Int>, // error string, error code
private val clearMediaSessionError: Runnable, private val clearMediaSessionError: Runnable,
private val onPrepare: Consumer<Boolean>, private val onPrepare: Consumer<Boolean>
) : PlaybackPreparer { ) : PlaybackPreparer {
private val database = NewPipeDatabase.getInstance(context) private val database = NewPipeDatabase.getInstance(context)
private var disposable: Disposable? = null private var disposable: Disposable? = null
@ -146,7 +146,7 @@ class MediaBrowserPlaybackPreparer(
throw parseError(mediaId) throw parseError(mediaId)
} }
return when (/*val uriType = */path.removeAt(0)) { return when (path.removeAt(0)) {
ID_BOOKMARKS -> extractPlayQueueFromPlaylistMediaId( ID_BOOKMARKS -> extractPlayQueueFromPlaylistMediaId(
mediaId, mediaId,
path, path,
@ -172,7 +172,7 @@ class MediaBrowserPlaybackPreparer(
private fun extractPlayQueueFromPlaylistMediaId( private fun extractPlayQueueFromPlaylistMediaId(
mediaId: String, mediaId: String,
path: MutableList<String>, path: MutableList<String>,
url: String?, url: String?
): Single<PlayQueue> { ): Single<PlayQueue> {
if (path.isEmpty()) { if (path.isEmpty()) {
throw parseError(mediaId) throw parseError(mediaId)
@ -185,10 +185,11 @@ class MediaBrowserPlaybackPreparer(
} }
val playlistId = path[0].toLong() val playlistId = path[0].toLong()
val index = path[1].toInt() val index = path[1].toInt()
return if (playlistType == ID_LOCAL) return if (playlistType == ID_LOCAL) {
extractLocalPlayQueue(playlistId, index) extractLocalPlayQueue(playlistId, index)
else } else {
extractRemotePlayQueue(playlistId, index) extractRemotePlayQueue(playlistId, index)
}
} }
ID_URL -> { ID_URL -> {
@ -208,7 +209,7 @@ class MediaBrowserPlaybackPreparer(
@Throws(ContentNotAvailableException::class) @Throws(ContentNotAvailableException::class)
private fun extractPlayQueueFromHistoryMediaId( private fun extractPlayQueueFromHistoryMediaId(
mediaId: String, mediaId: String,
path: List<String>, path: List<String>
): Single<PlayQueue> { ): Single<PlayQueue> {
if (path.size != 1) { if (path.size != 1) {
throw parseError(mediaId) throw parseError(mediaId)
@ -229,14 +230,14 @@ class MediaBrowserPlaybackPreparer(
private fun extractPlayQueueFromInfoItemMediaId( private fun extractPlayQueueFromInfoItemMediaId(
mediaId: String, mediaId: String,
path: List<String>, path: List<String>,
url: String, url: String
): Single<PlayQueue> { ): Single<PlayQueue> {
if (path.size != 2) { if (path.size != 2) {
throw parseError(mediaId) throw parseError(mediaId)
} }
val serviceId = path[1].toInt() val serviceId = path[1].toInt()
return when (/*val infoItemType = */infoItemTypeFromString(path[0])) { return when (infoItemTypeFromString(path[0])) {
InfoType.STREAM -> ExtractorHelper.getStreamInfo(serviceId, url, false) InfoType.STREAM -> ExtractorHelper.getStreamInfo(serviceId, url, false)
.map { SinglePlayQueue(it) } .map { SinglePlayQueue(it) }

View file

@ -30,9 +30,9 @@ import android.support.v4.media.session.MediaSessionCompat
import android.util.Log import android.util.Log
import androidx.core.app.NotificationManagerCompat import androidx.core.app.NotificationManagerCompat
import androidx.media.MediaBrowserServiceCompat import androidx.media.MediaBrowserServiceCompat
import org.schabi.newpipe.BuildConfig
import java.security.MessageDigest import java.security.MessageDigest
import java.security.NoSuchAlgorithmException import java.security.NoSuchAlgorithmException
import org.schabi.newpipe.BuildConfig
/** /**
* Validates that the calling package is authorized to browse a [MediaBrowserServiceCompat]. * Validates that the calling package is authorized to browse a [MediaBrowserServiceCompat].
@ -94,18 +94,22 @@ internal class PackageValidator(context: Context) {
val isCallerKnown = when { val isCallerKnown = when {
// If it's our own app making the call, allow it. // If it's our own app making the call, allow it.
callingUid == Process.myUid() -> true callingUid == Process.myUid() -> true
// If the system is making the call, allow it. // If the system is making the call, allow it.
callingUid == Process.SYSTEM_UID -> true callingUid == Process.SYSTEM_UID -> true
// If the app was signed by the same certificate as the platform itself, also allow it. // If the app was signed by the same certificate as the platform itself, also allow it.
callerSignature == platformSignature -> true callerSignature == platformSignature -> true
/**
/*
* [MEDIA_CONTENT_CONTROL] permission is only available to system applications, and * [MEDIA_CONTENT_CONTROL] permission is only available to system applications, and
* while it isn't required to allow these apps to connect to a * while it isn't required to allow these apps to connect to a
* [MediaBrowserServiceCompat], allowing this ensures optimal compatability with apps * [MediaBrowserServiceCompat], allowing this ensures optimal compatability with apps
* such as Android TV and the Google Assistant. * such as Android TV and the Google Assistant.
*/ */
callerPackageInfo.permissions.contains(MEDIA_CONTENT_CONTROL) -> true callerPackageInfo.permissions.contains(MEDIA_CONTENT_CONTROL) -> true
/**
/*
* If the calling app has a notification listener it is able to retrieve notifications * If the calling app has a notification listener it is able to retrieve notifications
* and can connect to an active [MediaSessionCompat]. * and can connect to an active [MediaSessionCompat].
* *
@ -169,11 +173,10 @@ internal class PackageValidator(context: Context) {
*/ */
@Suppress("deprecation") @Suppress("deprecation")
@SuppressLint("PackageManagerGetSignatures") @SuppressLint("PackageManagerGetSignatures")
private fun getPackageInfo(callingPackage: String): PackageInfo? = private fun getPackageInfo(callingPackage: String): PackageInfo? = packageManager.getPackageInfo(
packageManager.getPackageInfo( callingPackage,
callingPackage, PackageManager.GET_SIGNATURES or PackageManager.GET_PERMISSIONS
PackageManager.GET_SIGNATURES or PackageManager.GET_PERMISSIONS )
)
/** /**
* Gets the signature of a given package's [PackageInfo]. * Gets the signature of a given package's [PackageInfo].
@ -185,23 +188,21 @@ internal class PackageValidator(context: Context) {
* returns `null` as the signature. * returns `null` as the signature.
*/ */
@Suppress("deprecation") @Suppress("deprecation")
private fun getSignature(packageInfo: PackageInfo): String? = private fun getSignature(packageInfo: PackageInfo): String? = if (packageInfo.signatures == null || packageInfo.signatures!!.size != 1) {
if (packageInfo.signatures == null || packageInfo.signatures!!.size != 1) { // Security best practices dictate that an app should be signed with exactly one (1)
// Security best practices dictate that an app should be signed with exactly one (1) // signature. Because of this, if there are multiple signatures, reject it.
// signature. Because of this, if there are multiple signatures, reject it. null
null } else {
} else { val certificate = packageInfo.signatures!![0].toByteArray()
val certificate = packageInfo.signatures!![0].toByteArray() getSignatureSha256(certificate)
getSignatureSha256(certificate) }
}
/** /**
* Finds the Android platform signing key signature. This key is never null. * Finds the Android platform signing key signature. This key is never null.
*/ */
private fun getSystemSignature(): String = private fun getSystemSignature(): String = getPackageInfo(ANDROID_PLATFORM)?.let { platformInfo ->
getPackageInfo(ANDROID_PLATFORM)?.let { platformInfo -> getSignature(platformInfo)
getSignature(platformInfo) } ?: throw IllegalStateException("Platform signature not found")
} ?: throw IllegalStateException("Platform signature not found")
/** /**
* Creates a SHA-256 signature given a certificate byte array. * Creates a SHA-256 signature given a certificate byte array.

View file

@ -8,6 +8,7 @@ import java.io.File
class BackupFileLocator(private val homeDir: File) { class BackupFileLocator(private val homeDir: File) {
companion object { companion object {
const val FILE_NAME_DB = "newpipe.db" const val FILE_NAME_DB = "newpipe.db"
@Deprecated( @Deprecated(
"Serializing preferences with Java's ObjectOutputStream is vulnerable to injections", "Serializing preferences with Java's ObjectOutputStream is vulnerable to injections",
replaceWith = ReplaceWith("FILE_NAME_JSON_PREFS") replaceWith = ReplaceWith("FILE_NAME_JSON_PREFS")

View file

@ -5,13 +5,13 @@ import com.grack.nanojson.JsonArray
import com.grack.nanojson.JsonParser import com.grack.nanojson.JsonParser
import com.grack.nanojson.JsonParserException import com.grack.nanojson.JsonParserException
import com.grack.nanojson.JsonWriter import com.grack.nanojson.JsonWriter
import org.schabi.newpipe.streams.io.SharpOutputStream
import org.schabi.newpipe.streams.io.StoredFileHelper
import org.schabi.newpipe.util.ZipHelper
import java.io.FileNotFoundException import java.io.FileNotFoundException
import java.io.IOException import java.io.IOException
import java.io.ObjectOutputStream import java.io.ObjectOutputStream
import java.util.zip.ZipOutputStream import java.util.zip.ZipOutputStream
import org.schabi.newpipe.streams.io.SharpOutputStream
import org.schabi.newpipe.streams.io.StoredFileHelper
import org.schabi.newpipe.util.ZipHelper
class ImportExportManager(private val fileLocator: BackupFileLocator) { class ImportExportManager(private val fileLocator: BackupFileLocator) {
companion object { companion object {
@ -31,7 +31,7 @@ class ImportExportManager(private val fileLocator: BackupFileLocator) {
ZipHelper.addFileToZip( ZipHelper.addFileToZip(
outZip, outZip,
BackupFileLocator.FILE_NAME_DB, BackupFileLocator.FILE_NAME_DB,
fileLocator.db.path, fileLocator.db.path
) )
// add the legacy vulnerable serialized preferences (will be removed in the future) // add the legacy vulnerable serialized preferences (will be removed in the future)
@ -78,7 +78,7 @@ class ImportExportManager(private val fileLocator: BackupFileLocator) {
val success = ZipHelper.extractFileFromZip( val success = ZipHelper.extractFileFromZip(
file, file,
BackupFileLocator.FILE_NAME_DB, BackupFileLocator.FILE_NAME_DB,
fileLocator.db.path, fileLocator.db.path
) )
if (success) { if (success) {
@ -122,10 +122,15 @@ class ImportExportManager(private val fileLocator: BackupFileLocator) {
for ((key, value) in entries) { for ((key, value) in entries) {
when (value) { when (value) {
is Boolean -> editor.putBoolean(key, value) is Boolean -> editor.putBoolean(key, value)
is Float -> editor.putFloat(key, value) is Float -> editor.putFloat(key, value)
is Int -> editor.putInt(key, value) is Int -> editor.putInt(key, value)
is Long -> editor.putLong(key, value) is Long -> editor.putLong(key, value)
is String -> editor.putString(key, value) is String -> editor.putString(key, value)
is Set<*> -> { is Set<*> -> {
// There are currently only Sets with type String possible // There are currently only Sets with type String possible
@Suppress("UNCHECKED_CAST") @Suppress("UNCHECKED_CAST")
@ -159,10 +164,15 @@ class ImportExportManager(private val fileLocator: BackupFileLocator) {
for ((key, value) in jsonObject) { for ((key, value) in jsonObject) {
when (value) { when (value) {
is Boolean -> editor.putBoolean(key, value) is Boolean -> editor.putBoolean(key, value)
is Float -> editor.putFloat(key, value) is Float -> editor.putFloat(key, value)
is Int -> editor.putInt(key, value) is Int -> editor.putInt(key, value)
is Long -> editor.putLong(key, value) is Long -> editor.putLong(key, value)
is String -> editor.putString(key, value) is String -> editor.putString(key, value)
is JsonArray -> { is JsonArray -> {
editor.putStringSet(key, value.mapNotNull { e -> e as? String }.toSet()) editor.putStringSet(key, value.mapNotNull { e -> e as? String }.toSet())
} }

View file

@ -45,7 +45,7 @@ class NotificationModeConfigFragment : Fragment() {
override fun onCreateView( override fun onCreateView(
inflater: LayoutInflater, inflater: LayoutInflater,
container: ViewGroup?, container: ViewGroup?,
savedInstanceState: Bundle?, savedInstanceState: Bundle?
): View { ): View {
_binding = FragmentChannelsNotificationsBinding.inflate(inflater, container, false) _binding = FragmentChannelsNotificationsBinding.inflate(inflater, container, false)
return binding.root return binding.root
@ -88,6 +88,7 @@ class NotificationModeConfigFragment : Fragment() {
toggleAll() toggleAll()
true true
} }
else -> super.onOptionsItemSelected(item) else -> super.onOptionsItemSelected(item)
} }
} }

View file

@ -7,9 +7,9 @@ package org.schabi.newpipe.util
import android.content.Context import android.content.Context
import androidx.preference.PreferenceManager import androidx.preference.PreferenceManager
import java.util.regex.Matcher
import org.schabi.newpipe.R import org.schabi.newpipe.R
import org.schabi.newpipe.ktx.getStringSafe import org.schabi.newpipe.ktx.getStringSafe
import java.util.regex.Matcher
object FilenameUtils { object FilenameUtils {
private const val CHARSET_MOST_SPECIAL = "[\\n\\r|?*<\":\\\\>/']+" private const val CHARSET_MOST_SPECIAL = "[\\n\\r|?*<\":\\\\>/']+"
@ -31,10 +31,12 @@ object FilenameUtils {
val defaultCharset = context.getString(R.string.default_file_charset_value) val defaultCharset = context.getString(R.string.default_file_charset_value)
val replacementChar = sharedPreferences.getStringSafe( val replacementChar = sharedPreferences.getStringSafe(
context.getString(R.string.settings_file_replacement_character_key), "_" context.getString(R.string.settings_file_replacement_character_key),
"_"
) )
val selectedCharset = sharedPreferences.getStringSafe( val selectedCharset = sharedPreferences.getStringSafe(
context.getString(R.string.settings_file_charset_key), "" context.getString(R.string.settings_file_charset_key),
""
).ifEmpty { defaultCharset } ).ifEmpty { defaultCharset }
val charset = when (selectedCharset) { val charset = when (selectedCharset) {

View file

@ -2,13 +2,13 @@ package org.schabi.newpipe.util
import android.content.pm.PackageManager import android.content.pm.PackageManager
import androidx.core.content.pm.PackageInfoCompat import androidx.core.content.pm.PackageInfoCompat
import java.time.Instant
import java.time.ZonedDateTime
import java.time.format.DateTimeFormatter
import org.schabi.newpipe.App import org.schabi.newpipe.App
import org.schabi.newpipe.error.ErrorInfo import org.schabi.newpipe.error.ErrorInfo
import org.schabi.newpipe.error.ErrorUtil.Companion.createNotification import org.schabi.newpipe.error.ErrorUtil.Companion.createNotification
import org.schabi.newpipe.error.UserAction import org.schabi.newpipe.error.UserAction
import java.time.Instant
import java.time.ZonedDateTime
import java.time.format.DateTimeFormatter
object ReleaseVersionUtil { object ReleaseVersionUtil {
// Public key of the certificate that is used in NewPipe release versions // Public key of the certificate that is used in NewPipe release versions
@ -26,7 +26,8 @@ object ReleaseVersionUtil {
PackageInfoCompat.hasSignatures(app.packageManager, app.packageName, certificates, false) PackageInfoCompat.hasSignatures(app.packageManager, app.packageName, certificates, false)
} catch (e: PackageManager.NameNotFoundException) { } catch (e: PackageManager.NameNotFoundException) {
createNotification( createNotification(
app, ErrorInfo(e, UserAction.CHECK_FOR_NEW_APP_VERSION, "Could not find package info") app,
ErrorInfo(e, UserAction.CHECK_FOR_NEW_APP_VERSION, "Could not find package info")
) )
false false
} }

View file

@ -5,9 +5,9 @@
package org.schabi.newpipe.util.image package org.schabi.newpipe.util.image
import kotlin.math.abs
import org.schabi.newpipe.extractor.Image import org.schabi.newpipe.extractor.Image
import org.schabi.newpipe.extractor.Image.ResolutionLevel import org.schabi.newpipe.extractor.Image.ResolutionLevel
import kotlin.math.abs
object ImageStrategy { object ImageStrategy {
// when preferredImageQuality is LOW or MEDIUM, images are sorted by how close their preferred // when preferredImageQuality is LOW or MEDIUM, images are sorted by how close their preferred
@ -68,7 +68,7 @@ object ImageStrategy {
val initialComparator = val initialComparator =
Comparator // the first step splits the images into groups of resolution levels Comparator // the first step splits the images into groups of resolution levels
.comparingInt { i: Image -> .comparingInt { i: Image ->
return@comparingInt when (i.estimatedResolutionLevel) { return@comparingInt when (i.estimatedResolutionLevel) {
// avoid unknowns as much as possible // avoid unknowns as much as possible
ResolutionLevel.UNKNOWN -> 3 ResolutionLevel.UNKNOWN -> 3
@ -92,6 +92,7 @@ object ImageStrategy {
// the same number for those. // the same number for those.
val finalComparator = when (nonNoneQuality) { val finalComparator = when (nonNoneQuality) {
PreferredImageQuality.NONE -> initialComparator PreferredImageQuality.NONE -> initialComparator
PreferredImageQuality.LOW -> initialComparator.thenComparingDouble { image -> PreferredImageQuality.LOW -> initialComparator.thenComparingDouble { image ->
val pixelCount = estimatePixelCount(image, widthOverHeight) val pixelCount = estimatePixelCount(image, widthOverHeight)
abs(pixelCount - BEST_LOW_H * BEST_LOW_H * widthOverHeight) abs(pixelCount - BEST_LOW_H * BEST_LOW_H * widthOverHeight)

View file

@ -6,8 +6,9 @@ class PoTokenException(message: String) : Exception(message)
class BadWebViewException(message: String) : Exception(message) class BadWebViewException(message: String) : Exception(message)
fun buildExceptionForJsError(error: String): Exception { fun buildExceptionForJsError(error: String): Exception {
return if (error.contains("SyntaxError")) return if (error.contains("SyntaxError")) {
BadWebViewException(error) BadWebViewException(error)
else } else {
PoTokenException(error) PoTokenException(error)
}
} }

View file

@ -37,7 +37,9 @@ object PoTokenProviderImpl : PoTokenProvider {
webViewBadImpl = true webViewBadImpl = true
return null return null
} }
null -> throw e null -> throw e
else -> throw cause // includes PoTokenException else -> throw cause // includes PoTokenException
} }
} }
@ -58,7 +60,6 @@ object PoTokenProviderImpl : PoTokenProvider {
webPoTokenGenerator!!.isExpired() webPoTokenGenerator!!.isExpired()
if (shouldRecreate) { if (shouldRecreate) {
val innertubeClientRequestInfo = InnertubeClientRequestInfo.ofWebClient() val innertubeClientRequestInfo = InnertubeClientRequestInfo.ofWebClient()
innertubeClientRequestInfo.clientInfo.clientVersion = innertubeClientRequestInfo.clientInfo.clientVersion =
YoutubeParsingHelper.getClientVersion() YoutubeParsingHelper.getClientVersion()

View file

@ -16,14 +16,14 @@ import io.reactivex.rxjava3.core.Single
import io.reactivex.rxjava3.core.SingleEmitter import io.reactivex.rxjava3.core.SingleEmitter
import io.reactivex.rxjava3.disposables.CompositeDisposable import io.reactivex.rxjava3.disposables.CompositeDisposable
import io.reactivex.rxjava3.schedulers.Schedulers import io.reactivex.rxjava3.schedulers.Schedulers
import java.time.Instant
import org.schabi.newpipe.BuildConfig import org.schabi.newpipe.BuildConfig
import org.schabi.newpipe.DownloaderImpl import org.schabi.newpipe.DownloaderImpl
import java.time.Instant
class PoTokenWebView private constructor( class PoTokenWebView private constructor(
context: Context, context: Context,
// to be used exactly once only during initialization! // to be used exactly once only during initialization!
private val generatorEmitter: SingleEmitter<PoTokenGenerator>, private val generatorEmitter: SingleEmitter<PoTokenGenerator>
) : PoTokenGenerator { ) : PoTokenGenerator {
private val webView = WebView(context) private val webView = WebView(context)
private val disposables = CompositeDisposable() // used only during initialization private val disposables = CompositeDisposable() // used only during initialization
@ -93,7 +93,7 @@ class PoTokenWebView private constructor(
), ),
"text/html", "text/html",
"utf-8", "utf-8",
null, null
) )
}, },
this::onInitializationErrorCloseAndCancel this::onInitializationErrorCloseAndCancel
@ -113,7 +113,7 @@ class PoTokenWebView private constructor(
makeBotguardServiceRequest( makeBotguardServiceRequest(
"https://www.youtube.com/api/jnn/v1/Create", "https://www.youtube.com/api/jnn/v1/Create",
"[ \"$REQUEST_KEY\" ]", "[ \"$REQUEST_KEY\" ]"
) { responseBody -> ) { responseBody ->
val parsedChallengeData = parseChallengeData(responseBody) val parsedChallengeData = parseChallengeData(responseBody)
webView.evaluateJavascript( webView.evaluateJavascript(
@ -156,7 +156,7 @@ class PoTokenWebView private constructor(
} }
makeBotguardServiceRequest( makeBotguardServiceRequest(
"https://www.youtube.com/api/jnn/v1/GenerateIT", "https://www.youtube.com/api/jnn/v1/GenerateIT",
"[ \"$REQUEST_KEY\", \"$botguardResponse\" ]", "[ \"$REQUEST_KEY\", \"$botguardResponse\" ]"
) { responseBody -> ) { responseBody ->
if (BuildConfig.DEBUG) { if (BuildConfig.DEBUG) {
Log.d(TAG, "GenerateIT response: $responseBody") Log.d(TAG, "GenerateIT response: $responseBody")
@ -179,16 +179,15 @@ class PoTokenWebView private constructor(
//endregion //endregion
//region Obtaining poTokens //region Obtaining poTokens
override fun generatePoToken(identifier: String): Single<String> = override fun generatePoToken(identifier: String): Single<String> = Single.create { emitter ->
Single.create { emitter -> if (BuildConfig.DEBUG) {
if (BuildConfig.DEBUG) { Log.d(TAG, "generatePoToken() called with identifier $identifier")
Log.d(TAG, "generatePoToken() called with identifier $identifier") }
} runOnMainThread(emitter) {
runOnMainThread(emitter) { addPoTokenEmitter(identifier, emitter)
addPoTokenEmitter(identifier, emitter) val u8Identifier = stringToU8(identifier)
val u8Identifier = stringToU8(identifier) webView.evaluateJavascript(
webView.evaluateJavascript( """try {
"""try {
identifier = "$identifier" identifier = "$identifier"
u8Identifier = $u8Identifier u8Identifier = $u8Identifier
poTokenU8 = obtainPoToken(webPoSignalOutput, integrityToken, u8Identifier) poTokenU8 = obtainPoToken(webPoSignalOutput, integrityToken, u8Identifier)
@ -200,10 +199,10 @@ class PoTokenWebView private constructor(
$JS_INTERFACE.onObtainPoTokenResult(identifier, poTokenU8String) $JS_INTERFACE.onObtainPoTokenResult(identifier, poTokenU8String)
} catch (error) { } catch (error) {
$JS_INTERFACE.onObtainPoTokenError(identifier, error + "\n" + error.stack) $JS_INTERFACE.onObtainPoTokenError(identifier, error + "\n" + error.stack)
}""", }"""
) {} ) {}
}
} }
}
/** /**
* Called by the JavaScript snippet from [generatePoToken] when an error occurs in calling the * Called by the JavaScript snippet from [generatePoToken] when an error occurs in calling the
@ -245,6 +244,7 @@ class PoTokenWebView private constructor(
//endregion //endregion
//region Handling multiple emitters //region Handling multiple emitters
/** /**
* Adds the ([identifier], [emitter]) pair to the [poTokenEmitters] list. This makes it so that * Adds the ([identifier], [emitter]) pair to the [poTokenEmitters] list. This makes it so that
* multiple poToken requests can be generated invparallel, and the results will be notified to * multiple poToken requests can be generated invparallel, and the results will be notified to
@ -283,6 +283,7 @@ class PoTokenWebView private constructor(
//endregion //endregion
//region Utils //region Utils
/** /**
* Makes a POST request to [url] with the given [data] by setting the correct headers. Calls * Makes a POST request to [url] with the given [data] by setting the correct headers. Calls
* [onInitializationErrorCloseAndCancel] in case of any network errors and also if the response * [onInitializationErrorCloseAndCancel] in case of any network errors and also if the response
@ -294,7 +295,7 @@ class PoTokenWebView private constructor(
private fun makeBotguardServiceRequest( private fun makeBotguardServiceRequest(
url: String, url: String,
data: String, data: String,
handleResponseBody: (String) -> Unit, handleResponseBody: (String) -> Unit
) { ) {
disposables.add( disposables.add(
Single.fromCallable { Single.fromCallable {
@ -306,7 +307,7 @@ class PoTokenWebView private constructor(
"Accept" to listOf("application/json"), "Accept" to listOf("application/json"),
"Content-Type" to listOf("application/json+protobuf"), "Content-Type" to listOf("application/json+protobuf"),
"x-goog-api-key" to listOf(GOOGLE_API_KEY), "x-goog-api-key" to listOf(GOOGLE_API_KEY),
"x-user-agent" to listOf("grpc-web-javascript/0.1"), "x-user-agent" to listOf("grpc-web-javascript/0.1")
), ),
data.toByteArray() data.toByteArray()
) )
@ -363,6 +364,7 @@ class PoTokenWebView private constructor(
companion object : PoTokenGenerator.Factory { companion object : PoTokenGenerator.Factory {
private val TAG = PoTokenWebView::class.simpleName private val TAG = PoTokenWebView::class.simpleName
// Public API key used by BotGuard, which has been got by looking at BotGuard requests // Public API key used by BotGuard, which has been got by looking at BotGuard requests
private const val GOOGLE_API_KEY = "AIzaSyDyT5W0Jh49F30Pqqtyfdf7pDLFKLJoAnw" // NOSONAR private const val GOOGLE_API_KEY = "AIzaSyDyT5W0Jh49F30Pqqtyfdf7pDLFKLJoAnw" // NOSONAR
private const val REQUEST_KEY = "O43z0dpjhgX20SCx4KAo" private const val REQUEST_KEY = "O43z0dpjhgX20SCx4KAo"
@ -370,14 +372,13 @@ class PoTokenWebView private constructor(
"AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.3" "AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.3"
private const val JS_INTERFACE = "PoTokenWebView" private const val JS_INTERFACE = "PoTokenWebView"
override fun newPoTokenGenerator(context: Context): Single<PoTokenGenerator> = override fun newPoTokenGenerator(context: Context): Single<PoTokenGenerator> = Single.create { emitter ->
Single.create { emitter -> runOnMainThread(emitter) {
runOnMainThread(emitter) { val potWv = PoTokenWebView(context, emitter)
val potWv = PoTokenWebView(context, emitter) potWv.loadHtmlAndObtainBotguard(context)
potWv.loadHtmlAndObtainBotguard(context) emitter.setDisposable(potWv.disposables)
emitter.setDisposable(potWv.disposables)
}
} }
}
/** /**
* Runs [runnable] on the main thread using `Handler(Looper.getMainLooper()).post()`, and * Runs [runnable] on the main thread using `Handler(Looper.getMainLooper()).post()`, and
@ -385,7 +386,7 @@ class PoTokenWebView private constructor(
*/ */
private fun runOnMainThread( private fun runOnMainThread(
emitterIfPostFails: SingleEmitter<out Any>, emitterIfPostFails: SingleEmitter<out Any>,
runnable: Runnable, runnable: Runnable
) { ) {
if (!Handler(Looper.getMainLooper()).post(runnable)) { if (!Handler(Looper.getMainLooper()).post(runnable)) {
emitterIfPostFails.onError(PoTokenException("Could not run on main thread")) emitterIfPostFails.onError(PoTokenException("Could not run on main thread"))

View file

@ -53,8 +53,10 @@ class TimestampLongPressClickableSpan(
when (relatedInfoService) { when (relatedInfoService) {
ServiceList.YouTube -> ServiceList.YouTube ->
return relatedStreamUrl + "&t=" + timestampMatchDTO.seconds() return relatedStreamUrl + "&t=" + timestampMatchDTO.seconds()
ServiceList.SoundCloud, ServiceList.MediaCCC -> ServiceList.SoundCloud, ServiceList.MediaCCC ->
return relatedStreamUrl + "#t=" + timestampMatchDTO.seconds() return relatedStreamUrl + "#t=" + timestampMatchDTO.seconds()
ServiceList.PeerTube -> ServiceList.PeerTube ->
return relatedStreamUrl + "?start=" + timestampMatchDTO.seconds() return relatedStreamUrl + "?start=" + timestampMatchDTO.seconds()
} }

View file

@ -52,8 +52,9 @@ class PlayerFastSeekOverlay(context: Context, attrs: AttributeSet?) :
private var initTap: Boolean = false private var initTap: Boolean = false
override fun onDoubleTapStarted(portion: DisplayPortion) { override fun onDoubleTapStarted(portion: DisplayPortion) {
if (DEBUG) if (DEBUG) {
Log.d(TAG, "onDoubleTapStarted called with portion = [$portion]") Log.d(TAG, "onDoubleTapStarted called with portion = [$portion]")
}
initTap = false initTap = false
@ -64,7 +65,7 @@ class PlayerFastSeekOverlay(context: Context, attrs: AttributeSet?) :
val shouldForward: Boolean = val shouldForward: Boolean =
performListener?.getFastSeekDirection(portion)?.directionAsBoolean ?: return performListener?.getFastSeekDirection(portion)?.directionAsBoolean ?: return
if (DEBUG) if (DEBUG) {
Log.d( Log.d(
TAG, TAG,
"onDoubleTapProgressDown called with " + "onDoubleTapProgressDown called with " +
@ -72,6 +73,7 @@ class PlayerFastSeekOverlay(context: Context, attrs: AttributeSet?) :
"wasForwarding = [$wasForwarding], " + "wasForwarding = [$wasForwarding], " +
"initTap = [$initTap], " "initTap = [$initTap], "
) )
}
/* /*
* Check if a initial tap occurred or if direction was switched * Check if a initial tap occurred or if direction was switched
@ -97,8 +99,9 @@ class PlayerFastSeekOverlay(context: Context, attrs: AttributeSet?) :
} }
override fun onDoubleTapFinished() { override fun onDoubleTapFinished() {
if (DEBUG) if (DEBUG) {
Log.d(TAG, "onDoubleTapFinished called with initTap = [$initTap]") Log.d(TAG, "onDoubleTapFinished called with initTap = [$initTap]")
}
if (initTap) performListener?.onDoubleTapEnd() if (initTap) performListener?.onDoubleTapEnd()
initTap = false initTap = false
@ -112,8 +115,10 @@ class PlayerFastSeekOverlay(context: Context, attrs: AttributeSet?) :
clone(rootConstraintLayout) clone(rootConstraintLayout)
clear(secondsView.id, if (forward) START else END) clear(secondsView.id, if (forward) START else END)
connect( connect(
secondsView.id, if (forward) END else START, secondsView.id,
PARENT_ID, if (forward) END else START if (forward) END else START,
PARENT_ID,
if (forward) END else START
) )
secondsView.startAnimation() secondsView.startAnimation()
applyTo(rootConstraintLayout) applyTo(rootConstraintLayout)
@ -123,6 +128,7 @@ class PlayerFastSeekOverlay(context: Context, attrs: AttributeSet?) :
interface PerformListener { interface PerformListener {
fun onDoubleTap() fun onDoubleTap()
fun onDoubleTapEnd() fun onDoubleTapEnd()
/** /**
* Determines if the playback should forward/rewind or do nothing. * Determines if the playback should forward/rewind or do nothing.
*/ */
@ -132,7 +138,7 @@ class PlayerFastSeekOverlay(context: Context, attrs: AttributeSet?) :
enum class FastSeekDirection(val directionAsBoolean: Boolean?) { enum class FastSeekDirection(val directionAsBoolean: Boolean?) {
NONE(null), NONE(null),
FORWARD(true), FORWARD(true),
BACKWARD(false); BACKWARD(false)
} }
} }

View file

@ -29,7 +29,9 @@ class SecondsView(context: Context, attrs: AttributeSet?) : LinearLayout(context
var seconds: Int = 0 var seconds: Int = 0
set(value) { set(value) {
binding.tvSeconds.text = context.resources.getQuantityString( binding.tvSeconds.text = context.resources.getQuantityString(
R.plurals.seconds, value, value R.plurals.seconds,
value,
value
) )
field = value field = value
} }

View file

@ -1,13 +1,13 @@
package us.shandian.giga.get package us.shandian.giga.get
import android.os.Parcelable import android.os.Parcelable
import java.io.Serializable
import kotlinx.parcelize.Parcelize import kotlinx.parcelize.Parcelize
import org.schabi.newpipe.extractor.MediaFormat import org.schabi.newpipe.extractor.MediaFormat
import org.schabi.newpipe.extractor.stream.AudioStream import org.schabi.newpipe.extractor.stream.AudioStream
import org.schabi.newpipe.extractor.stream.Stream import org.schabi.newpipe.extractor.stream.Stream
import org.schabi.newpipe.extractor.stream.SubtitlesStream import org.schabi.newpipe.extractor.stream.SubtitlesStream
import org.schabi.newpipe.extractor.stream.VideoStream import org.schabi.newpipe.extractor.stream.VideoStream
import java.io.Serializable
@Parcelize @Parcelize
class MissionRecoveryInfo( class MissionRecoveryInfo(
@ -25,16 +25,19 @@ class MissionRecoveryInfo(
isDesired2 = false isDesired2 = false
kind = 'a' kind = 'a'
} }
is VideoStream -> { is VideoStream -> {
desired = stream.getResolution() desired = stream.getResolution()
isDesired2 = stream.isVideoOnly() isDesired2 = stream.isVideoOnly()
kind = 'v' kind = 'v'
} }
is SubtitlesStream -> { is SubtitlesStream -> {
desired = stream.languageTag desired = stream.languageTag
isDesired2 = stream.isAutoGenerated isDesired2 = stream.isAutoGenerated
kind = 's' kind = 's'
} }
else -> throw RuntimeException("Unknown stream kind") else -> throw RuntimeException("Unknown stream kind")
} }
} }
@ -48,14 +51,17 @@ class MissionRecoveryInfo(
str.append("audio") str.append("audio")
info = "bitrate=$desiredBitrate" info = "bitrate=$desiredBitrate"
} }
'v' -> { 'v' -> {
str.append("video") str.append("video")
info = "quality=$desired videoOnly=$isDesired2" info = "quality=$desired videoOnly=$isDesired2"
} }
's' -> { 's' -> {
str.append("subtitles") str.append("subtitles")
info = "language=$desired autoGenerated=$isDesired2" info = "language=$desired autoGenerated=$isDesired2"
} }
else -> { else -> {
info = "" info = ""
str.append("other") str.append("other")

View file

@ -1,14 +1,14 @@
package org.schabi.newpipe package org.schabi.newpipe
import java.time.Instant
import java.time.ZoneId
import java.time.format.DateTimeFormatter
import kotlin.math.abs
import org.junit.Assert.assertFalse import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue import org.junit.Assert.assertTrue
import org.junit.Test import org.junit.Test
import org.schabi.newpipe.util.ReleaseVersionUtil.coerceUpdateCheckExpiry import org.schabi.newpipe.util.ReleaseVersionUtil.coerceUpdateCheckExpiry
import org.schabi.newpipe.util.ReleaseVersionUtil.isLastUpdateCheckExpired import org.schabi.newpipe.util.ReleaseVersionUtil.isLastUpdateCheckExpired
import java.time.Instant
import java.time.ZoneId
import java.time.format.DateTimeFormatter
import kotlin.math.abs
class NewVersionManagerTest { class NewVersionManagerTest {

View file

@ -1,12 +1,12 @@
package org.schabi.newpipe.ktx package org.schabi.newpipe.ktx
import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue
import org.junit.Test
import java.io.IOException import java.io.IOException
import java.io.InterruptedIOException import java.io.InterruptedIOException
import java.net.SocketException import java.net.SocketException
import javax.net.ssl.SSLException import javax.net.ssl.SSLException
import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue
import org.junit.Test
class ThrowableExtensionsTest { class ThrowableExtensionsTest {
@Test fun `assignable causes`() { @Test fun `assignable causes`() {

View file

@ -15,7 +15,8 @@ class FeedGroupIconTest {
assertEquals( assertEquals(
"Gap between ids detected (current item: ${currentIcon.name} - ${currentIcon.id} → should be: $shouldBeId)", "Gap between ids detected (current item: ${currentIcon.name} - ${currentIcon.id} → should be: $shouldBeId)",
shouldBeId, currentIcon.id shouldBeId,
currentIcon.id
) )
} }
} }

View file

@ -1,6 +1,9 @@
package org.schabi.newpipe.settings package org.schabi.newpipe.settings
import android.content.SharedPreferences import android.content.SharedPreferences
import java.io.File
import java.io.IOException
import java.nio.file.Files
import org.junit.Assert import org.junit.Assert
import org.junit.Test import org.junit.Test
import org.mockito.Mockito import org.mockito.Mockito
@ -8,9 +11,6 @@ import org.schabi.newpipe.settings.export.BackupFileLocator
import org.schabi.newpipe.settings.export.ImportExportManager import org.schabi.newpipe.settings.export.ImportExportManager
import org.schabi.newpipe.streams.io.StoredFileHelper import org.schabi.newpipe.streams.io.StoredFileHelper
import us.shandian.giga.io.FileStream import us.shandian.giga.io.FileStream
import java.io.File
import java.io.IOException
import java.nio.file.Files
class ImportAllCombinationsTest { class ImportAllCombinationsTest {
@ -21,7 +21,7 @@ class ImportAllCombinationsTest {
private enum class Ser(val id: String) { private enum class Ser(val id: String) {
YES("ser"), YES("ser"),
VULNERABLE("vulnser"), VULNERABLE("vulnser"),
NO("noser"); NO("noser")
} }
private data class FailData( private data class FailData(
@ -29,7 +29,7 @@ class ImportAllCombinationsTest {
val containsSer: Ser, val containsSer: Ser,
val containsJson: Boolean, val containsJson: Boolean,
val filename: String, val filename: String,
val throwable: Throwable, val throwable: Throwable
) )
private fun testZipCombination( private fun testZipCombination(
@ -37,7 +37,7 @@ class ImportAllCombinationsTest {
containsSer: Ser, containsSer: Ser,
containsJson: Boolean, containsJson: Boolean,
filename: String, filename: String,
runTest: (test: () -> Unit) -> Unit, runTest: (test: () -> Unit) -> Unit
) { ) {
val zipFile = File(classloader.getResource(filename)?.file!!) val zipFile = File(classloader.getResource(filename)?.file!!)
val zip = Mockito.mock(StoredFileHelper::class.java, Mockito.withSettings().stubOnly()) val zip = Mockito.mock(StoredFileHelper::class.java, Mockito.withSettings().stubOnly())
@ -93,6 +93,7 @@ class ImportAllCombinationsTest {
Mockito.verify(editor, Mockito.atLeastOnce()) Mockito.verify(editor, Mockito.atLeastOnce())
.putInt(Mockito.anyString(), Mockito.anyInt()) .putInt(Mockito.anyString(), Mockito.anyInt())
} }
Ser.VULNERABLE -> runTest { Ser.VULNERABLE -> runTest {
Assert.assertTrue(ImportExportManager(fileLocator).exportHasSerializedPrefs(zip)) Assert.assertTrue(ImportExportManager(fileLocator).exportHasSerializedPrefs(zip))
Assert.assertThrows(ClassNotFoundException::class.java) { Assert.assertThrows(ClassNotFoundException::class.java) {
@ -102,6 +103,7 @@ class ImportAllCombinationsTest {
Mockito.verify(editor, Mockito.never()).clear() Mockito.verify(editor, Mockito.never()).clear()
Mockito.verify(editor, Mockito.never()).commit() Mockito.verify(editor, Mockito.never()).commit()
} }
Ser.NO -> runTest { Ser.NO -> runTest {
Assert.assertFalse(ImportExportManager(fileLocator).exportHasSerializedPrefs(zip)) Assert.assertFalse(ImportExportManager(fileLocator).exportHasSerializedPrefs(zip))
Assert.assertThrows(IOException::class.java) { Assert.assertThrows(IOException::class.java) {
@ -152,15 +154,18 @@ class ImportAllCombinationsTest {
for (containsSer in Ser.entries) { for (containsSer in Ser.entries) {
for (containsJson in listOf(true, false)) { for (containsJson in listOf(true, false)) {
val filename = "settings/${if (containsDb) "db" else "nodb"}_${ val filename = "settings/${if (containsDb) "db" else "nodb"}_${
containsSer.id}_${if (containsJson) "json" else "nojson"}.zip" containsSer.id}_${if (containsJson) "json" else "nojson"}.zip"
testZipCombination(containsDb, containsSer, containsJson, filename) { test -> testZipCombination(containsDb, containsSer, containsJson, filename) { test ->
try { try {
test() test()
} catch (e: Throwable) { } catch (e: Throwable) {
failedAssertions.add( failedAssertions.add(
FailData( FailData(
containsDb, containsSer, containsJson, containsDb,
filename, e containsSer,
containsJson,
filename,
e
) )
) )
} }
@ -173,7 +178,7 @@ class ImportAllCombinationsTest {
for (a in failedAssertions) { for (a in failedAssertions) {
println( println(
"Assertion failed with containsDb=${a.containsDb}, containsSer=${ "Assertion failed with containsDb=${a.containsDb}, containsSer=${
a.containsSer}, containsJson=${a.containsJson}, filename=${a.filename}:" a.containsSer}, containsJson=${a.containsJson}, filename=${a.filename}:"
) )
a.throwable.printStackTrace() a.throwable.printStackTrace()
println() println()

View file

@ -2,6 +2,10 @@ package org.schabi.newpipe.settings
import android.content.SharedPreferences import android.content.SharedPreferences
import com.grack.nanojson.JsonParser import com.grack.nanojson.JsonParser
import java.io.File
import java.io.ObjectInputStream
import java.nio.file.Files
import java.util.zip.ZipFile
import org.junit.Assert.assertEquals import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse import org.junit.Assert.assertFalse
import org.junit.Assert.assertThrows import org.junit.Assert.assertThrows
@ -23,10 +27,6 @@ import org.schabi.newpipe.settings.export.BackupFileLocator
import org.schabi.newpipe.settings.export.ImportExportManager import org.schabi.newpipe.settings.export.ImportExportManager
import org.schabi.newpipe.streams.io.StoredFileHelper import org.schabi.newpipe.streams.io.StoredFileHelper
import us.shandian.giga.io.FileStream import us.shandian.giga.io.FileStream
import java.io.File
import java.io.ObjectInputStream
import java.nio.file.Files
import java.util.zip.ZipFile
@RunWith(MockitoJUnitRunner::class) @RunWith(MockitoJUnitRunner::class)
class ImportExportManagerTest { class ImportExportManagerTest {

View file

@ -1,12 +1,12 @@
package org.schabi.newpipe.util package org.schabi.newpipe.util
import org.junit.Assert.assertEquals
import org.junit.Test
import org.ocpsoft.prettytime.PrettyTime
import java.time.LocalDate import java.time.LocalDate
import java.time.OffsetDateTime import java.time.OffsetDateTime
import java.time.ZoneOffset import java.time.ZoneOffset
import java.util.Locale import java.util.Locale
import org.junit.Assert.assertEquals
import org.junit.Test
import org.ocpsoft.prettytime.PrettyTime
class LocalizationTest { class LocalizationTest {
@Test(expected = NullPointerException::class) @Test(expected = NullPointerException::class)