Merge pull request #13081 from TeamNewPipe/ktlint
Fix and enable multiple ktlint rules
This commit is contained in:
commit
6d2b14fb28
83 changed files with 759 additions and 473 deletions
|
|
@ -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
|
|
||||||
|
|
|
||||||
|
|
@ -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()
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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")
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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/"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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()
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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))
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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.
|
||||||
|
|
|
||||||
|
|
@ -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")
|
||||||
|
|
|
||||||
|
|
@ -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}"
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,6 @@ interface LocalItem {
|
||||||
PLAYLIST_REMOTE_ITEM,
|
PLAYLIST_REMOTE_ITEM,
|
||||||
|
|
||||||
PLAYLIST_STREAM_ITEM,
|
PLAYLIST_STREAM_ITEM,
|
||||||
STATISTIC_STREAM_ITEM,
|
STATISTIC_STREAM_ITEM
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
)
|
)
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
)
|
)
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
)
|
)
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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)
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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 =
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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!!
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
||||||
|
|
|
||||||
|
|
@ -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")
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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.
|
||||||
*/
|
*/
|
||||||
|
|
|
||||||
|
|
@ -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.
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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(
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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]
|
||||||
|
|
|
||||||
|
|
@ -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.
|
||||||
|
|
|
||||||
|
|
@ -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()
|
||||||
|
|
|
||||||
|
|
@ -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!!)
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
)
|
)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -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()
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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"))
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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())
|
||||||
|
|
|
||||||
|
|
@ -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) }
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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.
|
||||||
|
|
|
||||||
|
|
@ -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")
|
||||||
|
|
|
||||||
|
|
@ -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())
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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) {
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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()
|
||||||
|
|
|
||||||
|
|
@ -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"))
|
||||||
|
|
|
||||||
|
|
@ -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()
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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")
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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`() {
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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()
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue