mirror of
https://github.com/TeamNewPipe/NewPipe
synced 2026-03-04 23:09:44 +00:00
Compare commits
195 Commits
cmp
...
release-0.
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e6e5fc70f2 | ||
|
|
8fa6f9670d | ||
|
|
1fbf9fc025 | ||
|
|
98a883d377 | ||
|
|
8b500c7b83 | ||
|
|
6dddcf3805 | ||
|
|
bf1633265c | ||
|
|
e22b046326 | ||
|
|
56fb31d0fd | ||
|
|
042f9460b0 | ||
|
|
9f1e2c6fd0 | ||
|
|
66237abb3c | ||
|
|
dd65db56a9 | ||
|
|
195a76bb08 | ||
|
|
06e4548c14 | ||
|
|
9a292e33f9 | ||
|
|
d1ad1e843f | ||
|
|
8902ce8678 | ||
|
|
9f193a3b00 | ||
|
|
232d023c02 | ||
|
|
423f95a65d | ||
|
|
caebf8461a | ||
|
|
52a9b60180 | ||
|
|
4bad882668 | ||
|
|
63e19890bf | ||
|
|
08b7da4b2b | ||
|
|
402a0aaaa3 | ||
|
|
57364109f4 | ||
|
|
4481dd7fe6 | ||
|
|
273b09a3e8 | ||
|
|
35b70c5e9e | ||
|
|
653b33bdb9 | ||
|
|
5eb5f7533d | ||
|
|
15829c882b | ||
|
|
10b943f37e | ||
|
|
3d43e52afb | ||
|
|
f64e40e958 | ||
|
|
2182ff12b7 | ||
|
|
b3f4cb8114 | ||
|
|
deb6b4230d | ||
|
|
2f3a993f8e | ||
|
|
582f852e7a | ||
|
|
89cb87b2a9 | ||
|
|
8c016c95d2 | ||
|
|
5b009453f2 | ||
|
|
feb442492b | ||
|
|
834f136102 | ||
|
|
01e77e2e26 | ||
|
|
4cfd36ce5b | ||
|
|
51c9ed774c | ||
|
|
9f8055f018 | ||
|
|
239f6c97a0 | ||
|
|
7e7ad1e410 | ||
|
|
95367dd338 | ||
|
|
70cdaf5550 | ||
|
|
3815f5f593 | ||
|
|
6c69a5409b | ||
|
|
840664a8d7 | ||
|
|
54090ca7b6 | ||
|
|
8d45b6b8c9 | ||
|
|
c3dbed54e5 | ||
|
|
8968aab578 | ||
|
|
d7a4435e94 | ||
|
|
4a7eaed3a7 | ||
|
|
869a3cea9b | ||
|
|
3a0a3a42af | ||
|
|
59e5018c2d | ||
|
|
e6e0be772a | ||
|
|
224a5d0cb9 | ||
|
|
c6fc94e7bd | ||
|
|
1eedfd7eee | ||
|
|
2c7654a579 | ||
|
|
09a746dd6a | ||
|
|
d665a4f016 | ||
|
|
6cf932b2a7 | ||
|
|
48467669b6 | ||
|
|
780e6a4848 | ||
|
|
13186c0b15 | ||
|
|
edfdbe805f | ||
|
|
451409fc3b | ||
|
|
289d22eed7 | ||
|
|
21f446a78e | ||
|
|
6214ae33f3 | ||
|
|
37cef825a2 | ||
|
|
dab8e056e9 | ||
|
|
020dbdc82a | ||
|
|
5d7934249f | ||
|
|
d6be966db3 | ||
|
|
5e1a1989be | ||
|
|
56a043669a | ||
|
|
85abc58158 | ||
|
|
8578bd9f95 | ||
|
|
13577f5421 | ||
|
|
dc8a62914a | ||
|
|
74d5a8b173 | ||
|
|
d2a8955964 | ||
|
|
955844b3e1 | ||
|
|
5112acf008 | ||
|
|
fcb77fed93 | ||
|
|
2027b6dbc7 | ||
|
|
90d5d5f4de | ||
|
|
045e91df1c | ||
|
|
e74907561e | ||
|
|
1554f77762 | ||
|
|
118def08b4 | ||
|
|
725cb70cbd | ||
|
|
5525d206dc | ||
|
|
83f9646eec | ||
|
|
85d43fe45e | ||
|
|
8d6e68d6f4 | ||
|
|
07fe1e758a | ||
|
|
15b5cef6c2 | ||
|
|
ae60f7d7eb | ||
|
|
739b6ae57b | ||
|
|
cc33b685a5 | ||
|
|
d051e8ecc8 | ||
|
|
51e62f09ba | ||
|
|
8a2c47bc12 | ||
|
|
a7aad63bbb | ||
|
|
fd192b4f3f | ||
|
|
19e94bd30c | ||
|
|
7758a27694 | ||
|
|
a3301dcfb1 | ||
|
|
d045b27cea | ||
|
|
4f70235ee8 | ||
|
|
54f9bcb03e | ||
|
|
addf1e23b3 | ||
|
|
a40d7ff70e | ||
|
|
d53f7acfa4 | ||
|
|
ee52b08546 | ||
|
|
864725bf0f | ||
|
|
c2723096ab | ||
|
|
eb7351c858 | ||
|
|
216867c597 | ||
|
|
1d8ea0181f | ||
|
|
4648cac9c6 | ||
|
|
0578e7fde0 | ||
|
|
c670ad80ee | ||
|
|
077f34c922 | ||
|
|
635b306de0 | ||
|
|
11af6a2902 | ||
|
|
7e3657831c | ||
|
|
c0613b5e54 | ||
|
|
bffee48bcb | ||
|
|
49e95d95a1 | ||
|
|
dc160da034 | ||
|
|
5155b24ed6 | ||
|
|
4e0d542994 | ||
|
|
817fccb7a3 | ||
|
|
8d9af62736 | ||
|
|
8f32532acd | ||
|
|
0611d650e7 | ||
|
|
d1f6337c6e | ||
|
|
d7dffb7a90 | ||
|
|
d0f32b3842 | ||
|
|
25b133946d | ||
|
|
460cadf694 | ||
|
|
2fd2822053 | ||
|
|
8ac8424cab | ||
|
|
6b2a1cedef | ||
|
|
8c5f13ab5c | ||
|
|
eb97366e41 | ||
|
|
81ddd5a115 | ||
|
|
7d5647b0ba | ||
|
|
9b7874ff51 | ||
|
|
ff2390b144 | ||
|
|
81fd089a32 | ||
|
|
1466dd17b1 | ||
|
|
555cd3acb7 | ||
|
|
ea105e9026 | ||
|
|
e86846ba6a | ||
|
|
dcb2460c81 | ||
|
|
6190db7d2f | ||
|
|
c60339fc58 | ||
|
|
80a543e7ab | ||
|
|
c76d14dfd4 | ||
|
|
d6f3dee9f4 | ||
|
|
247cbf3d6f | ||
|
|
0641c19388 | ||
|
|
2c808b0e86 | ||
|
|
f23d8eff57 | ||
|
|
ed4b77b5aa | ||
|
|
675dbd35d0 | ||
|
|
eec3ac166a | ||
|
|
bcb7469d30 | ||
|
|
40d7dcf3d3 | ||
|
|
16415d13ed | ||
|
|
3ab414252f | ||
|
|
7d95ec71aa | ||
|
|
cea5dd474b | ||
|
|
6ddd4a7e63 | ||
|
|
4f0e62e599 | ||
|
|
1eeba8daa7 | ||
|
|
738338d092 | ||
|
|
e758b5f890 |
@@ -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
|
|
||||||
|
|||||||
2
.github/workflows/backport-pr.yml
vendored
2
.github/workflows/backport-pr.yml
vendored
@@ -22,7 +22,7 @@ jobs:
|
|||||||
github.event.comment.author_association == 'MEMBER'
|
github.event.comment.author_association == 'MEMBER'
|
||||||
)
|
)
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v6
|
||||||
- name: Get backport metadata
|
- name: Get backport metadata
|
||||||
# the target branch is the first argument after `/backport`
|
# the target branch is the first argument after `/backport`
|
||||||
env:
|
env:
|
||||||
|
|||||||
2
.github/workflows/ci.yml
vendored
2
.github/workflows/ci.yml
vendored
@@ -38,7 +38,7 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v6
|
- uses: actions/checkout@v6
|
||||||
- uses: gradle/actions/wrapper-validation@v4
|
- uses: gradle/actions/wrapper-validation@v5
|
||||||
|
|
||||||
- name: create and checkout branch
|
- name: create and checkout branch
|
||||||
# push events already checked out the branch
|
# push events already checked out the branch
|
||||||
|
|||||||
@@ -3,6 +3,8 @@
|
|||||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import com.android.build.api.dsl.ApplicationExtension
|
||||||
|
|
||||||
plugins {
|
plugins {
|
||||||
alias(libs.plugins.android.application)
|
alias(libs.plugins.android.application)
|
||||||
alias(libs.plugins.jetbrains.kotlin.android)
|
alias(libs.plugins.jetbrains.kotlin.android)
|
||||||
@@ -32,7 +34,7 @@ kotlin {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
android {
|
configure<ApplicationExtension> {
|
||||||
compileSdk = 36
|
compileSdk = 36
|
||||||
namespace = "org.schabi.newpipe"
|
namespace = "org.schabi.newpipe"
|
||||||
|
|
||||||
@@ -42,9 +44,9 @@ android {
|
|||||||
minSdk = 21
|
minSdk = 21
|
||||||
targetSdk = 35
|
targetSdk = 35
|
||||||
|
|
||||||
versionCode = System.getProperty("versionCodeOverride")?.toInt() ?: 1006
|
versionCode = System.getProperty("versionCodeOverride")?.toInt() ?: 1009
|
||||||
|
|
||||||
versionName = "0.28.1"
|
versionName = "0.28.4"
|
||||||
System.getProperty("versionNameSuffix")?.let { versionNameSuffix = it }
|
System.getProperty("versionNameSuffix")?.let { versionNameSuffix = it }
|
||||||
|
|
||||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||||
@@ -77,19 +79,18 @@ android {
|
|||||||
resValue("string", "app_name", "NewPipe $suffix")
|
resValue("string", "app_name", "NewPipe $suffix")
|
||||||
}
|
}
|
||||||
isMinifyEnabled = true
|
isMinifyEnabled = true
|
||||||
isShrinkResources = false // disabled to fix F-Droid"s reproducible build
|
isShrinkResources = true
|
||||||
proguardFiles(getDefaultProguardFile("proguard-android.txt"), "proguard-rules.pro")
|
proguardFiles(
|
||||||
|
getDefaultProguardFile("proguard-android-optimize.txt"),
|
||||||
|
"proguard-rules.pro"
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
lint {
|
lint {
|
||||||
checkReleaseBuilds = false
|
lintConfig = file("lint.xml")
|
||||||
// Or, if you prefer, you can continue to check for errors in release builds,
|
// Continue the debug build even when errors are found
|
||||||
// but continue the build even when errors are found:
|
|
||||||
abortOnError = false
|
abortOnError = false
|
||||||
// suppress false warning ("Resource IDs will be non-final in Android Gradle Plugin version
|
|
||||||
// 5.0, avoid using them in switch case statements"), which affects only library projects
|
|
||||||
disable += "NonConstantResourceId"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
compileOptions {
|
compileOptions {
|
||||||
@@ -100,7 +101,7 @@ android {
|
|||||||
|
|
||||||
sourceSets {
|
sourceSets {
|
||||||
getByName("androidTest") {
|
getByName("androidTest") {
|
||||||
assets.srcDir("$projectDir/schemas")
|
assets.directories += "$projectDir/schemas"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -111,6 +112,7 @@ android {
|
|||||||
buildFeatures {
|
buildFeatures {
|
||||||
viewBinding = true
|
viewBinding = true
|
||||||
buildConfig = true
|
buildConfig = true
|
||||||
|
resValues = true
|
||||||
}
|
}
|
||||||
|
|
||||||
packaging {
|
packaging {
|
||||||
@@ -134,6 +136,13 @@ ksp {
|
|||||||
// Custom dependency configuration for ktlint
|
// Custom dependency configuration for ktlint
|
||||||
val ktlint by configurations.creating
|
val ktlint by configurations.creating
|
||||||
|
|
||||||
|
// https://checkstyle.org/#JRE_and_JDK
|
||||||
|
tasks.withType<Checkstyle>().configureEach {
|
||||||
|
javaLauncher = javaToolchains.launcherFor {
|
||||||
|
languageVersion = JavaLanguageVersion.of(21)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
checkstyle {
|
checkstyle {
|
||||||
configDirectory = rootProject.file("checkstyle")
|
configDirectory = rootProject.file("checkstyle")
|
||||||
isIgnoreFailures = false
|
isIgnoreFailures = false
|
||||||
@@ -263,7 +272,8 @@ dependencies {
|
|||||||
implementation(libs.lisawray.groupie.viewbinding)
|
implementation(libs.lisawray.groupie.viewbinding)
|
||||||
|
|
||||||
// Image loading
|
// Image loading
|
||||||
implementation(libs.squareup.picasso)
|
implementation(libs.coil.compose)
|
||||||
|
implementation(libs.coil.network.okhttp)
|
||||||
|
|
||||||
// Markdown library for Android
|
// Markdown library for Android
|
||||||
implementation(libs.noties.markwon.core)
|
implementation(libs.noties.markwon.core)
|
||||||
|
|||||||
10
app/lint.xml
Normal file
10
app/lint.xml
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!--
|
||||||
|
~ SPDX-FileCopyrightText: 2026 NewPipe e.V. <https://newpipe-ev.de>
|
||||||
|
~ SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
-->
|
||||||
|
<lint>
|
||||||
|
<issue id="MissingTranslation" severity="ignore" />
|
||||||
|
<issue id="MissingQuantity" severity="ignore" />
|
||||||
|
<issue id="ImpliedQuantity" severity="ignore" />
|
||||||
|
</lint>
|
||||||
5
app/proguard-rules.pro
vendored
5
app/proguard-rules.pro
vendored
@@ -39,3 +39,8 @@
|
|||||||
|
|
||||||
## For some reason NotificationModeConfigFragment wasn't kept (only referenced in a preference xml)
|
## For some reason NotificationModeConfigFragment wasn't kept (only referenced in a preference xml)
|
||||||
-keep class org.schabi.newpipe.settings.notifications.** { *; }
|
-keep class org.schabi.newpipe.settings.notifications.** { *; }
|
||||||
|
|
||||||
|
# Prevent R8 from stripping or renaming Protobuf internal fields
|
||||||
|
-keepclassmembers class * extends com.google.protobuf.GeneratedMessageLite {
|
||||||
|
<fields>;
|
||||||
|
}
|
||||||
|
|||||||
@@ -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,9 +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
|
|
||||||
import kotlin.streams.toList
|
|
||||||
|
|
||||||
class FeedDAOTest {
|
class FeedDAOTest {
|
||||||
private lateinit var db: AppDatabase
|
private lateinit var db: AppDatabase
|
||||||
@@ -41,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()
|
||||||
@@ -65,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)
|
||||||
@@ -76,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)
|
||||||
@@ -112,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(
|
||||||
@@ -123,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))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,285 +0,0 @@
|
|||||||
package org.schabi.newpipe;
|
|
||||||
|
|
||||||
import android.app.Application;
|
|
||||||
import android.content.Context;
|
|
||||||
import android.content.SharedPreferences;
|
|
||||||
import android.util.Log;
|
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
|
||||||
import androidx.core.app.NotificationChannelCompat;
|
|
||||||
import androidx.core.app.NotificationManagerCompat;
|
|
||||||
import androidx.preference.PreferenceManager;
|
|
||||||
|
|
||||||
import com.jakewharton.processphoenix.ProcessPhoenix;
|
|
||||||
|
|
||||||
import org.acra.ACRA;
|
|
||||||
import org.acra.config.CoreConfigurationBuilder;
|
|
||||||
import org.schabi.newpipe.error.ReCaptchaActivity;
|
|
||||||
import org.schabi.newpipe.extractor.NewPipe;
|
|
||||||
import org.schabi.newpipe.extractor.downloader.Downloader;
|
|
||||||
import org.schabi.newpipe.extractor.services.youtube.extractors.YoutubeStreamExtractor;
|
|
||||||
import org.schabi.newpipe.ktx.ExceptionUtils;
|
|
||||||
import org.schabi.newpipe.settings.NewPipeSettings;
|
|
||||||
import org.schabi.newpipe.util.BridgeStateSaverInitializer;
|
|
||||||
import org.schabi.newpipe.util.Localization;
|
|
||||||
import org.schabi.newpipe.util.ServiceHelper;
|
|
||||||
import org.schabi.newpipe.util.StateSaver;
|
|
||||||
import org.schabi.newpipe.util.image.ImageStrategy;
|
|
||||||
import org.schabi.newpipe.util.image.PicassoHelper;
|
|
||||||
import org.schabi.newpipe.util.image.PreferredImageQuality;
|
|
||||||
import org.schabi.newpipe.util.potoken.PoTokenProviderImpl;
|
|
||||||
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.io.InterruptedIOException;
|
|
||||||
import java.net.SocketException;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Objects;
|
|
||||||
|
|
||||||
import io.reactivex.rxjava3.exceptions.CompositeException;
|
|
||||||
import io.reactivex.rxjava3.exceptions.MissingBackpressureException;
|
|
||||||
import io.reactivex.rxjava3.exceptions.OnErrorNotImplementedException;
|
|
||||||
import io.reactivex.rxjava3.exceptions.UndeliverableException;
|
|
||||||
import io.reactivex.rxjava3.functions.Consumer;
|
|
||||||
import io.reactivex.rxjava3.plugins.RxJavaPlugins;
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Copyright (C) Hans-Christoph Steiner 2016 <hans@eds.org>
|
|
||||||
* App.java is part of NewPipe.
|
|
||||||
*
|
|
||||||
* NewPipe is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* NewPipe is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with NewPipe. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
public class App extends Application {
|
|
||||||
public static final String PACKAGE_NAME = BuildConfig.APPLICATION_ID;
|
|
||||||
private static final String TAG = App.class.toString();
|
|
||||||
|
|
||||||
private boolean isFirstRun = false;
|
|
||||||
private boolean notificationsRequested = false;
|
|
||||||
|
|
||||||
private static App app;
|
|
||||||
|
|
||||||
@NonNull
|
|
||||||
public static App getApp() {
|
|
||||||
return app;
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean getNotificationsRequested() {
|
|
||||||
return notificationsRequested;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setNotificationsRequested() {
|
|
||||||
notificationsRequested = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void attachBaseContext(final Context base) {
|
|
||||||
super.attachBaseContext(base);
|
|
||||||
initACRA();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onCreate() {
|
|
||||||
super.onCreate();
|
|
||||||
|
|
||||||
app = this;
|
|
||||||
|
|
||||||
if (ProcessPhoenix.isPhoenixProcess(this)) {
|
|
||||||
Log.i(TAG, "This is a phoenix process! "
|
|
||||||
+ "Aborting initialization of App[onCreate]");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// check if the last used preference version is set
|
|
||||||
// to determine whether this is the first app run
|
|
||||||
final int lastUsedPrefVersion = PreferenceManager.getDefaultSharedPreferences(this)
|
|
||||||
.getInt(getString(R.string.last_used_preferences_version), -1);
|
|
||||||
isFirstRun = lastUsedPrefVersion == -1;
|
|
||||||
|
|
||||||
// Initialize settings first because other initializations can use its values
|
|
||||||
NewPipeSettings.initSettings(this);
|
|
||||||
|
|
||||||
NewPipe.init(getDownloader(),
|
|
||||||
Localization.getPreferredLocalization(this),
|
|
||||||
Localization.getPreferredContentCountry(this));
|
|
||||||
Localization.initPrettyTime(Localization.resolvePrettyTime());
|
|
||||||
|
|
||||||
BridgeStateSaverInitializer.init(this);
|
|
||||||
StateSaver.init(this);
|
|
||||||
initNotificationChannels();
|
|
||||||
|
|
||||||
ServiceHelper.initServices(this);
|
|
||||||
|
|
||||||
// Initialize image loader
|
|
||||||
final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this);
|
|
||||||
PicassoHelper.init(this);
|
|
||||||
ImageStrategy.setPreferredImageQuality(PreferredImageQuality.fromPreferenceKey(this,
|
|
||||||
prefs.getString(getString(R.string.image_quality_key),
|
|
||||||
getString(R.string.image_quality_default))));
|
|
||||||
PicassoHelper.setIndicatorsEnabled(MainActivity.DEBUG
|
|
||||||
&& prefs.getBoolean(getString(R.string.show_image_indicators_key), false));
|
|
||||||
|
|
||||||
configureRxJavaErrorHandler();
|
|
||||||
|
|
||||||
YoutubeStreamExtractor.setPoTokenProvider(PoTokenProviderImpl.INSTANCE);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onTerminate() {
|
|
||||||
super.onTerminate();
|
|
||||||
PicassoHelper.terminate();
|
|
||||||
}
|
|
||||||
|
|
||||||
protected Downloader getDownloader() {
|
|
||||||
final DownloaderImpl downloader = DownloaderImpl.init(null);
|
|
||||||
setCookiesToDownloader(downloader);
|
|
||||||
return downloader;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected void setCookiesToDownloader(final DownloaderImpl downloader) {
|
|
||||||
final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(
|
|
||||||
getApplicationContext());
|
|
||||||
final String key = getApplicationContext().getString(R.string.recaptcha_cookies_key);
|
|
||||||
downloader.setCookie(ReCaptchaActivity.RECAPTCHA_COOKIES_KEY, prefs.getString(key, null));
|
|
||||||
downloader.updateYoutubeRestrictedModeCookies(getApplicationContext());
|
|
||||||
}
|
|
||||||
|
|
||||||
private void configureRxJavaErrorHandler() {
|
|
||||||
// https://github.com/ReactiveX/RxJava/wiki/What's-different-in-2.0#error-handling
|
|
||||||
RxJavaPlugins.setErrorHandler(new Consumer<Throwable>() {
|
|
||||||
@Override
|
|
||||||
public void accept(@NonNull final Throwable throwable) {
|
|
||||||
Log.e(TAG, "RxJavaPlugins.ErrorHandler called with -> : "
|
|
||||||
+ "throwable = [" + throwable.getClass().getName() + "]");
|
|
||||||
|
|
||||||
final Throwable actualThrowable;
|
|
||||||
if (throwable instanceof UndeliverableException) {
|
|
||||||
// As UndeliverableException is a wrapper,
|
|
||||||
// get the cause of it to get the "real" exception
|
|
||||||
actualThrowable = Objects.requireNonNull(throwable.getCause());
|
|
||||||
} else {
|
|
||||||
actualThrowable = throwable;
|
|
||||||
}
|
|
||||||
|
|
||||||
final List<Throwable> errors;
|
|
||||||
if (actualThrowable instanceof CompositeException) {
|
|
||||||
errors = ((CompositeException) actualThrowable).getExceptions();
|
|
||||||
} else {
|
|
||||||
errors = List.of(actualThrowable);
|
|
||||||
}
|
|
||||||
|
|
||||||
for (final Throwable error : errors) {
|
|
||||||
if (isThrowableIgnored(error)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (isThrowableCritical(error)) {
|
|
||||||
reportException(error);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Out-of-lifecycle exceptions should only be reported if a debug user wishes so,
|
|
||||||
// When exception is not reported, log it
|
|
||||||
if (isDisposedRxExceptionsReported()) {
|
|
||||||
reportException(actualThrowable);
|
|
||||||
} else {
|
|
||||||
Log.e(TAG, "RxJavaPlugin: Undeliverable Exception received: ", actualThrowable);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean isThrowableIgnored(@NonNull final Throwable throwable) {
|
|
||||||
// Don't crash the application over a simple network problem
|
|
||||||
return ExceptionUtils.hasAssignableCause(throwable,
|
|
||||||
// network api cancellation
|
|
||||||
IOException.class, SocketException.class,
|
|
||||||
// blocking code disposed
|
|
||||||
InterruptedException.class, InterruptedIOException.class);
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean isThrowableCritical(@NonNull final Throwable throwable) {
|
|
||||||
// Though these exceptions cannot be ignored
|
|
||||||
return ExceptionUtils.hasAssignableCause(throwable,
|
|
||||||
NullPointerException.class, IllegalArgumentException.class, // bug in app
|
|
||||||
OnErrorNotImplementedException.class, MissingBackpressureException.class,
|
|
||||||
IllegalStateException.class); // bug in operator
|
|
||||||
}
|
|
||||||
|
|
||||||
private void reportException(@NonNull final Throwable throwable) {
|
|
||||||
// Throw uncaught exception that will trigger the report system
|
|
||||||
Thread.currentThread().getUncaughtExceptionHandler()
|
|
||||||
.uncaughtException(Thread.currentThread(), throwable);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Called in {@link #attachBaseContext(Context)} after calling the {@code super} method.
|
|
||||||
* Should be overridden if MultiDex is enabled, since it has to be initialized before ACRA.
|
|
||||||
*/
|
|
||||||
protected void initACRA() {
|
|
||||||
if (ACRA.isACRASenderServiceProcess()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
final CoreConfigurationBuilder acraConfig = new CoreConfigurationBuilder()
|
|
||||||
.withBuildConfigClass(BuildConfig.class);
|
|
||||||
ACRA.init(this, acraConfig);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void initNotificationChannels() {
|
|
||||||
// Keep the importance below DEFAULT to avoid making noise on every notification update for
|
|
||||||
// the main and update channels
|
|
||||||
final List<NotificationChannelCompat> notificationChannelCompats = List.of(
|
|
||||||
new NotificationChannelCompat.Builder(getString(R.string.notification_channel_id),
|
|
||||||
NotificationManagerCompat.IMPORTANCE_LOW)
|
|
||||||
.setName(getString(R.string.notification_channel_name))
|
|
||||||
.setDescription(getString(R.string.notification_channel_description))
|
|
||||||
.build(),
|
|
||||||
new NotificationChannelCompat
|
|
||||||
.Builder(getString(R.string.app_update_notification_channel_id),
|
|
||||||
NotificationManagerCompat.IMPORTANCE_LOW)
|
|
||||||
.setName(getString(R.string.app_update_notification_channel_name))
|
|
||||||
.setDescription(
|
|
||||||
getString(R.string.app_update_notification_channel_description))
|
|
||||||
.build(),
|
|
||||||
new NotificationChannelCompat.Builder(getString(R.string.hash_channel_id),
|
|
||||||
NotificationManagerCompat.IMPORTANCE_HIGH)
|
|
||||||
.setName(getString(R.string.hash_channel_name))
|
|
||||||
.setDescription(getString(R.string.hash_channel_description))
|
|
||||||
.build(),
|
|
||||||
new NotificationChannelCompat.Builder(getString(R.string.error_report_channel_id),
|
|
||||||
NotificationManagerCompat.IMPORTANCE_LOW)
|
|
||||||
.setName(getString(R.string.error_report_channel_name))
|
|
||||||
.setDescription(getString(R.string.error_report_channel_description))
|
|
||||||
.build(),
|
|
||||||
new NotificationChannelCompat
|
|
||||||
.Builder(getString(R.string.streams_notification_channel_id),
|
|
||||||
NotificationManagerCompat.IMPORTANCE_DEFAULT)
|
|
||||||
.setName(getString(R.string.streams_notification_channel_name))
|
|
||||||
.setDescription(
|
|
||||||
getString(R.string.streams_notification_channel_description))
|
|
||||||
.build()
|
|
||||||
);
|
|
||||||
|
|
||||||
final NotificationManagerCompat notificationManager = NotificationManagerCompat.from(this);
|
|
||||||
notificationManager.createNotificationChannelsCompat(notificationChannelCompats);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected boolean isDisposedRxExceptionsReported() {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean isFirstRun() {
|
|
||||||
return isFirstRun;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
293
app/src/main/java/org/schabi/newpipe/App.kt
Normal file
293
app/src/main/java/org/schabi/newpipe/App.kt
Normal file
@@ -0,0 +1,293 @@
|
|||||||
|
package org.schabi.newpipe
|
||||||
|
|
||||||
|
import android.app.ActivityManager
|
||||||
|
import android.app.Application
|
||||||
|
import android.content.Context
|
||||||
|
import android.util.Log
|
||||||
|
import androidx.core.app.NotificationChannelCompat
|
||||||
|
import androidx.core.app.NotificationManagerCompat
|
||||||
|
import androidx.core.content.getSystemService
|
||||||
|
import androidx.preference.PreferenceManager
|
||||||
|
import coil3.ImageLoader
|
||||||
|
import coil3.SingletonImageLoader
|
||||||
|
import coil3.network.okhttp.OkHttpNetworkFetcherFactory
|
||||||
|
import coil3.request.allowRgb565
|
||||||
|
import coil3.request.crossfade
|
||||||
|
import coil3.util.DebugLogger
|
||||||
|
import com.jakewharton.processphoenix.ProcessPhoenix
|
||||||
|
import io.reactivex.rxjava3.exceptions.CompositeException
|
||||||
|
import io.reactivex.rxjava3.exceptions.MissingBackpressureException
|
||||||
|
import io.reactivex.rxjava3.exceptions.OnErrorNotImplementedException
|
||||||
|
import io.reactivex.rxjava3.exceptions.UndeliverableException
|
||||||
|
import io.reactivex.rxjava3.functions.Consumer
|
||||||
|
import io.reactivex.rxjava3.plugins.RxJavaPlugins
|
||||||
|
import java.io.IOException
|
||||||
|
import java.io.InterruptedIOException
|
||||||
|
import java.net.SocketException
|
||||||
|
import org.acra.ACRA.init
|
||||||
|
import org.acra.ACRA.isACRASenderServiceProcess
|
||||||
|
import org.acra.config.CoreConfigurationBuilder
|
||||||
|
import org.schabi.newpipe.error.ReCaptchaActivity
|
||||||
|
import org.schabi.newpipe.extractor.NewPipe
|
||||||
|
import org.schabi.newpipe.extractor.downloader.Downloader
|
||||||
|
import org.schabi.newpipe.extractor.services.youtube.extractors.YoutubeStreamExtractor
|
||||||
|
import org.schabi.newpipe.ktx.hasAssignableCause
|
||||||
|
import org.schabi.newpipe.settings.NewPipeSettings
|
||||||
|
import org.schabi.newpipe.util.BridgeStateSaverInitializer
|
||||||
|
import org.schabi.newpipe.util.Localization
|
||||||
|
import org.schabi.newpipe.util.ServiceHelper
|
||||||
|
import org.schabi.newpipe.util.StateSaver
|
||||||
|
import org.schabi.newpipe.util.image.ImageStrategy
|
||||||
|
import org.schabi.newpipe.util.image.PreferredImageQuality
|
||||||
|
import org.schabi.newpipe.util.potoken.PoTokenProviderImpl
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Copyright (C) Hans-Christoph Steiner 2016 <hans@eds.org>
|
||||||
|
* App.kt is part of NewPipe.
|
||||||
|
*
|
||||||
|
* NewPipe is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* NewPipe is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with NewPipe. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
open class App :
|
||||||
|
Application(),
|
||||||
|
SingletonImageLoader.Factory {
|
||||||
|
var isFirstRun = false
|
||||||
|
private set
|
||||||
|
var notificationsRequested = false
|
||||||
|
private set
|
||||||
|
|
||||||
|
fun setNotificationsRequested() {
|
||||||
|
notificationsRequested = true
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun attachBaseContext(base: Context?) {
|
||||||
|
super.attachBaseContext(base)
|
||||||
|
initACRA()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreate() {
|
||||||
|
super.onCreate()
|
||||||
|
|
||||||
|
instance = this
|
||||||
|
|
||||||
|
if (ProcessPhoenix.isPhoenixProcess(this)) {
|
||||||
|
Log.i(TAG, "This is a phoenix process! Aborting initialization of App[onCreate]")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// check if the last used preference version is set
|
||||||
|
// to determine whether this is the first app run
|
||||||
|
val lastUsedPrefVersion =
|
||||||
|
PreferenceManager
|
||||||
|
.getDefaultSharedPreferences(this)
|
||||||
|
.getInt(getString(R.string.last_used_preferences_version), -1)
|
||||||
|
isFirstRun = lastUsedPrefVersion == -1
|
||||||
|
|
||||||
|
// Initialize settings first because other initializations can use its values
|
||||||
|
NewPipeSettings.initSettings(this)
|
||||||
|
|
||||||
|
NewPipe.init(
|
||||||
|
getDownloader(),
|
||||||
|
Localization.getPreferredLocalization(this),
|
||||||
|
Localization.getPreferredContentCountry(this)
|
||||||
|
)
|
||||||
|
Localization.initPrettyTime(Localization.resolvePrettyTime())
|
||||||
|
|
||||||
|
BridgeStateSaverInitializer.init(this)
|
||||||
|
StateSaver.init(this)
|
||||||
|
initNotificationChannels()
|
||||||
|
|
||||||
|
ServiceHelper.initServices(this)
|
||||||
|
|
||||||
|
// Initialize image loader
|
||||||
|
val prefs = PreferenceManager.getDefaultSharedPreferences(this)
|
||||||
|
ImageStrategy.setPreferredImageQuality(
|
||||||
|
PreferredImageQuality.fromPreferenceKey(
|
||||||
|
this,
|
||||||
|
prefs.getString(
|
||||||
|
getString(R.string.image_quality_key),
|
||||||
|
getString(R.string.image_quality_default)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
configureRxJavaErrorHandler()
|
||||||
|
|
||||||
|
YoutubeStreamExtractor.setPoTokenProvider(PoTokenProviderImpl)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun newImageLoader(context: Context): ImageLoader = ImageLoader
|
||||||
|
.Builder(this)
|
||||||
|
.logger(if (BuildConfig.DEBUG) DebugLogger() else null)
|
||||||
|
.allowRgb565(getSystemService<ActivityManager>()!!.isLowRamDevice)
|
||||||
|
.crossfade(true)
|
||||||
|
.components {
|
||||||
|
add(OkHttpNetworkFetcherFactory(callFactory = DownloaderImpl.getInstance().client))
|
||||||
|
}.build()
|
||||||
|
|
||||||
|
protected open fun getDownloader(): Downloader {
|
||||||
|
val downloader = DownloaderImpl.init(null)
|
||||||
|
setCookiesToDownloader(downloader)
|
||||||
|
return downloader
|
||||||
|
}
|
||||||
|
|
||||||
|
protected fun setCookiesToDownloader(downloader: DownloaderImpl) {
|
||||||
|
val prefs = PreferenceManager.getDefaultSharedPreferences(this)
|
||||||
|
val key = getString(R.string.recaptcha_cookies_key)
|
||||||
|
downloader.setCookie(ReCaptchaActivity.RECAPTCHA_COOKIES_KEY, prefs.getString(key, null))
|
||||||
|
downloader.updateYoutubeRestrictedModeCookies(this)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun configureRxJavaErrorHandler() {
|
||||||
|
// https://github.com/ReactiveX/RxJava/wiki/What's-different-in-2.0#error-handling
|
||||||
|
RxJavaPlugins.setErrorHandler(
|
||||||
|
object : Consumer<Throwable> {
|
||||||
|
override fun accept(throwable: Throwable) {
|
||||||
|
Log.e(TAG, "RxJavaPlugins.ErrorHandler called with -> : throwable = [${throwable.javaClass.getName()}]")
|
||||||
|
|
||||||
|
// As UndeliverableException is a wrapper,
|
||||||
|
// get the cause of it to get the "real" exception
|
||||||
|
val actualThrowable = (throwable as? UndeliverableException)?.cause ?: throwable
|
||||||
|
|
||||||
|
val errors = (actualThrowable as? CompositeException)?.exceptions ?: listOf(actualThrowable)
|
||||||
|
|
||||||
|
for (error in errors) {
|
||||||
|
if (isThrowableIgnored(error)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (isThrowableCritical(error)) {
|
||||||
|
reportException(error)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Out-of-lifecycle exceptions should only be reported if a debug user wishes so,
|
||||||
|
// When exception is not reported, log it
|
||||||
|
if (isDisposedRxExceptionsReported()) {
|
||||||
|
reportException(actualThrowable)
|
||||||
|
} else {
|
||||||
|
Log.e(TAG, "RxJavaPlugin: Undeliverable Exception received: ", actualThrowable)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun isThrowableIgnored(throwable: Throwable): Boolean {
|
||||||
|
// Don't crash the application over a simple network problem
|
||||||
|
return throwable // network api cancellation
|
||||||
|
.hasAssignableCause(
|
||||||
|
IOException::class.java,
|
||||||
|
SocketException::class.java, // blocking code disposed
|
||||||
|
InterruptedException::class.java,
|
||||||
|
InterruptedIOException::class.java
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun isThrowableCritical(throwable: Throwable): Boolean {
|
||||||
|
// Though these exceptions cannot be ignored
|
||||||
|
return throwable
|
||||||
|
.hasAssignableCause(
|
||||||
|
// bug in app
|
||||||
|
NullPointerException::class.java,
|
||||||
|
IllegalArgumentException::class.java,
|
||||||
|
OnErrorNotImplementedException::class.java,
|
||||||
|
MissingBackpressureException::class.java,
|
||||||
|
// bug in operator
|
||||||
|
IllegalStateException::class.java
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun reportException(throwable: Throwable) {
|
||||||
|
// Throw uncaught exception that will trigger the report system
|
||||||
|
Thread
|
||||||
|
.currentThread()
|
||||||
|
.uncaughtExceptionHandler
|
||||||
|
.uncaughtException(Thread.currentThread(), throwable)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called in [.attachBaseContext] after calling the `super` method.
|
||||||
|
* Should be overridden if MultiDex is enabled, since it has to be initialized before ACRA.
|
||||||
|
*/
|
||||||
|
protected fun initACRA() {
|
||||||
|
if (isACRASenderServiceProcess()) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
val acraConfig =
|
||||||
|
CoreConfigurationBuilder()
|
||||||
|
.withBuildConfigClass(BuildConfig::class.java)
|
||||||
|
init(this, acraConfig)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun initNotificationChannels() {
|
||||||
|
// Keep the importance below DEFAULT to avoid making noise on every notification update for
|
||||||
|
// the main and update channels
|
||||||
|
val mainChannel =
|
||||||
|
NotificationChannelCompat
|
||||||
|
.Builder(
|
||||||
|
getString(R.string.notification_channel_id),
|
||||||
|
NotificationManagerCompat.IMPORTANCE_LOW
|
||||||
|
).setName(getString(R.string.notification_channel_name))
|
||||||
|
.setDescription(getString(R.string.notification_channel_description))
|
||||||
|
.build()
|
||||||
|
val appUpdateChannel =
|
||||||
|
NotificationChannelCompat
|
||||||
|
.Builder(
|
||||||
|
getString(R.string.app_update_notification_channel_id),
|
||||||
|
NotificationManagerCompat.IMPORTANCE_LOW
|
||||||
|
).setName(getString(R.string.app_update_notification_channel_name))
|
||||||
|
.setDescription(getString(R.string.app_update_notification_channel_description))
|
||||||
|
.build()
|
||||||
|
val hashChannel =
|
||||||
|
NotificationChannelCompat
|
||||||
|
.Builder(
|
||||||
|
getString(R.string.hash_channel_id),
|
||||||
|
NotificationManagerCompat.IMPORTANCE_HIGH
|
||||||
|
).setName(getString(R.string.hash_channel_name))
|
||||||
|
.setDescription(getString(R.string.hash_channel_description))
|
||||||
|
.build()
|
||||||
|
val errorReportChannel =
|
||||||
|
NotificationChannelCompat
|
||||||
|
.Builder(
|
||||||
|
getString(R.string.error_report_channel_id),
|
||||||
|
NotificationManagerCompat.IMPORTANCE_LOW
|
||||||
|
).setName(getString(R.string.error_report_channel_name))
|
||||||
|
.setDescription(getString(R.string.error_report_channel_description))
|
||||||
|
.build()
|
||||||
|
val newStreamChannel =
|
||||||
|
NotificationChannelCompat
|
||||||
|
.Builder(
|
||||||
|
getString(R.string.streams_notification_channel_id),
|
||||||
|
NotificationManagerCompat.IMPORTANCE_DEFAULT
|
||||||
|
).setName(getString(R.string.streams_notification_channel_name))
|
||||||
|
.setDescription(getString(R.string.streams_notification_channel_description))
|
||||||
|
.build()
|
||||||
|
|
||||||
|
val channels = listOf(mainChannel, appUpdateChannel, hashChannel, errorReportChannel, newStreamChannel)
|
||||||
|
|
||||||
|
NotificationManagerCompat.from(this).createNotificationChannelsCompat(channels)
|
||||||
|
}
|
||||||
|
|
||||||
|
protected open fun isDisposedRxExceptionsReported(): Boolean = false
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
const val PACKAGE_NAME: String = BuildConfig.APPLICATION_ID
|
||||||
|
private val TAG = App::class.java.toString()
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
|
lateinit var instance: App
|
||||||
|
private set
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -48,6 +48,11 @@ public final class DownloaderImpl extends Downloader {
|
|||||||
this.mCookies = new HashMap<>();
|
this.mCookies = new HashMap<>();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
public OkHttpClient getClient() {
|
||||||
|
return client;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* It's recommended to call exactly once in the entire lifetime of the application.
|
* It's recommended to call exactly once in the entire lifetime of the application.
|
||||||
*
|
*
|
||||||
@@ -161,9 +166,7 @@ public final class DownloaderImpl extends Downloader {
|
|||||||
|
|
||||||
String responseBodyToReturn = null;
|
String responseBodyToReturn = null;
|
||||||
try (ResponseBody body = response.body()) {
|
try (ResponseBody body = response.body()) {
|
||||||
if (body != null) {
|
responseBodyToReturn = body.string();
|
||||||
responseBodyToReturn = body.string();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
final String latestUrl = response.request().url().toString();
|
final String latestUrl = response.request().url().toString();
|
||||||
|
|||||||
@@ -1,50 +0,0 @@
|
|||||||
package org.schabi.newpipe;
|
|
||||||
|
|
||||||
import android.annotation.SuppressLint;
|
|
||||||
import android.app.Activity;
|
|
||||||
import android.content.Intent;
|
|
||||||
import android.os.Bundle;
|
|
||||||
|
|
||||||
import org.schabi.newpipe.util.NavigationHelper;
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Copyright (C) Hans-Christoph Steiner 2016 <hans@eds.org>
|
|
||||||
* ExitActivity.java is part of NewPipe.
|
|
||||||
*
|
|
||||||
* NewPipe is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* NewPipe is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with NewPipe. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
public class ExitActivity extends Activity {
|
|
||||||
|
|
||||||
public static void exitAndRemoveFromRecentApps(final Activity activity) {
|
|
||||||
final Intent intent = new Intent(activity, ExitActivity.class);
|
|
||||||
|
|
||||||
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK
|
|
||||||
| Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS
|
|
||||||
| Intent.FLAG_ACTIVITY_CLEAR_TASK
|
|
||||||
| Intent.FLAG_ACTIVITY_NO_ANIMATION);
|
|
||||||
|
|
||||||
activity.startActivity(intent);
|
|
||||||
}
|
|
||||||
|
|
||||||
@SuppressLint("NewApi")
|
|
||||||
@Override
|
|
||||||
protected void onCreate(final Bundle savedInstanceState) {
|
|
||||||
super.onCreate(savedInstanceState);
|
|
||||||
|
|
||||||
finishAndRemoveTask();
|
|
||||||
|
|
||||||
NavigationHelper.restartApp(this);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
36
app/src/main/java/org/schabi/newpipe/ExitActivity.kt
Normal file
36
app/src/main/java/org/schabi/newpipe/ExitActivity.kt
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: 2016-2026 NewPipe contributors <https://newpipe.net>
|
||||||
|
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.schabi.newpipe
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint
|
||||||
|
import android.app.Activity
|
||||||
|
import android.content.Intent
|
||||||
|
import android.os.Bundle
|
||||||
|
import org.schabi.newpipe.util.NavigationHelper
|
||||||
|
|
||||||
|
class ExitActivity : Activity() {
|
||||||
|
@SuppressLint("NewApi")
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
finishAndRemoveTask()
|
||||||
|
NavigationHelper.restartApp(this)
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
@JvmStatic
|
||||||
|
fun exitAndRemoveFromRecentApps(activity: Activity) {
|
||||||
|
val intent = Intent(activity, ExitActivity::class.java)
|
||||||
|
intent.addFlags(
|
||||||
|
Intent.FLAG_ACTIVITY_NEW_TASK
|
||||||
|
or Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS
|
||||||
|
or Intent.FLAG_ACTIVITY_CLEAR_TASK
|
||||||
|
or Intent.FLAG_ACTIVITY_NO_ANIMATION
|
||||||
|
)
|
||||||
|
|
||||||
|
activity.startActivity(intent)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -20,6 +20,7 @@
|
|||||||
|
|
||||||
package org.schabi.newpipe;
|
package org.schabi.newpipe;
|
||||||
|
|
||||||
|
import android.app.AlertDialog;
|
||||||
import android.content.BroadcastReceiver;
|
import android.content.BroadcastReceiver;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
@@ -96,6 +97,8 @@ import org.schabi.newpipe.util.ThemeHelper;
|
|||||||
import org.schabi.newpipe.util.external_communication.ShareUtils;
|
import org.schabi.newpipe.util.external_communication.ShareUtils;
|
||||||
import org.schabi.newpipe.views.FocusOverlayView;
|
import org.schabi.newpipe.views.FocusOverlayView;
|
||||||
|
|
||||||
|
import java.time.Instant;
|
||||||
|
import java.time.temporal.ChronoUnit;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
@@ -191,11 +194,17 @@ public class MainActivity extends AppCompatActivity {
|
|||||||
NotificationWorker.initialize(this);
|
NotificationWorker.initialize(this);
|
||||||
}
|
}
|
||||||
if (!UpdateSettingsFragment.wasUserAskedForConsent(this)
|
if (!UpdateSettingsFragment.wasUserAskedForConsent(this)
|
||||||
&& !App.getApp().isFirstRun()
|
&& !App.getInstance().isFirstRun()
|
||||||
&& ReleaseVersionUtil.INSTANCE.isReleaseApk()) {
|
&& ReleaseVersionUtil.INSTANCE.isReleaseApk()) {
|
||||||
UpdateSettingsFragment.askForConsentToUpdateChecks(this);
|
UpdateSettingsFragment.askForConsentToUpdateChecks(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ReleaseVersionUtil.INSTANCE.isReleaseApk() will be true only for main official build
|
||||||
|
// We want every release build (nightly, nightly-refactor) to show the popup
|
||||||
|
if (!DEBUG) {
|
||||||
|
showKeepAndroidDialog();
|
||||||
|
}
|
||||||
|
|
||||||
MigrationManager.showUserInfoIfPresent(this);
|
MigrationManager.showUserInfoIfPresent(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -203,7 +212,7 @@ public class MainActivity extends AppCompatActivity {
|
|||||||
protected void onPostCreate(final Bundle savedInstanceState) {
|
protected void onPostCreate(final Bundle savedInstanceState) {
|
||||||
super.onPostCreate(savedInstanceState);
|
super.onPostCreate(savedInstanceState);
|
||||||
|
|
||||||
final App app = App.getApp();
|
final App app = App.getInstance();
|
||||||
|
|
||||||
if (sharedPreferences.getBoolean(app.getString(R.string.update_app_key), false)
|
if (sharedPreferences.getBoolean(app.getString(R.string.update_app_key), false)
|
||||||
&& sharedPreferences
|
&& sharedPreferences
|
||||||
@@ -309,25 +318,21 @@ public class MainActivity extends AppCompatActivity {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private boolean drawerItemSelected(final MenuItem item) {
|
private boolean drawerItemSelected(final MenuItem item) {
|
||||||
switch (item.getGroupId()) {
|
final int groupId = item.getGroupId();
|
||||||
case R.id.menu_services_group:
|
if (groupId == R.id.menu_services_group) {
|
||||||
changeService(item);
|
changeService(item);
|
||||||
break;
|
} else if (groupId == R.id.menu_tabs_group) {
|
||||||
case R.id.menu_tabs_group:
|
tabSelected(item);
|
||||||
tabSelected(item);
|
} else if (groupId == R.id.menu_kiosks_group) {
|
||||||
break;
|
try {
|
||||||
case R.id.menu_kiosks_group:
|
kioskSelected(item);
|
||||||
try {
|
} catch (final Exception e) {
|
||||||
kioskSelected(item);
|
ErrorUtil.showUiErrorSnackbar(this, "Selecting drawer kiosk", e);
|
||||||
} catch (final Exception e) {
|
}
|
||||||
ErrorUtil.showUiErrorSnackbar(this, "Selecting drawer kiosk", e);
|
} else if (groupId == R.id.menu_options_about_group) {
|
||||||
}
|
optionsAboutSelected(item);
|
||||||
break;
|
} else {
|
||||||
case R.id.menu_options_about_group:
|
return false;
|
||||||
optionsAboutSelected(item);
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
mainBinding.getRoot().closeDrawers();
|
mainBinding.getRoot().closeDrawers();
|
||||||
@@ -977,4 +982,57 @@ public class MainActivity extends AppCompatActivity {
|
|||||||
|| sheetState == BottomSheetBehavior.STATE_COLLAPSED;
|
|| sheetState == BottomSheetBehavior.STATE_COLLAPSED;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void showKeepAndroidDialog() {
|
||||||
|
final var prefs = PreferenceManager.getDefaultSharedPreferences(this);
|
||||||
|
|
||||||
|
final var now = Instant.now();
|
||||||
|
final var kaoLastCheck = Instant.ofEpochMilli(prefs.getLong(
|
||||||
|
getString(R.string.kao_last_checked_key),
|
||||||
|
0
|
||||||
|
));
|
||||||
|
|
||||||
|
final var supportedLannguages = List.of("fr", "de", "ca", "es", "id", "it", "pl",
|
||||||
|
"pt", "cs", "sk", "fa", "ar", "tr", "el", "th", "ru", "uk", "ko", "zh", "ja");
|
||||||
|
final var locale = Localization.getAppLocale();
|
||||||
|
final String kaoBaseUrl = "https://keepandroidopen.org/";
|
||||||
|
final String kaoURI;
|
||||||
|
if (supportedLannguages.contains(locale.getLanguage())) {
|
||||||
|
if ("zh".equals(locale.getLanguage())) {
|
||||||
|
kaoURI = kaoBaseUrl + ("TW".equals(locale.getCountry()) ? "zh-TW" : "zh-CN");
|
||||||
|
} else {
|
||||||
|
kaoURI = kaoBaseUrl + locale.getLanguage();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
kaoURI = kaoBaseUrl;
|
||||||
|
}
|
||||||
|
final var solutionURI =
|
||||||
|
"https://github.com/woheller69/FreeDroidWarn?tab=readme-ov-file#solutions";
|
||||||
|
|
||||||
|
if (kaoLastCheck.plus(30, ChronoUnit.DAYS).isBefore(now)) {
|
||||||
|
final var dialog = new AlertDialog.Builder(this)
|
||||||
|
.setTitle("Keep Android Open")
|
||||||
|
.setCancelable(false)
|
||||||
|
.setMessage(this.getString(R.string.kao_dialog_warning))
|
||||||
|
.setPositiveButton(this.getString(android.R.string.ok), (d, w) -> {
|
||||||
|
prefs.edit()
|
||||||
|
.putLong(
|
||||||
|
getString(R.string.kao_last_checked_key),
|
||||||
|
now.toEpochMilli()
|
||||||
|
)
|
||||||
|
.apply();
|
||||||
|
})
|
||||||
|
.setNeutralButton(this.getString(R.string.kao_solution), null)
|
||||||
|
.setNegativeButton(this.getString(R.string.kao_dialog_more_info), null)
|
||||||
|
.show();
|
||||||
|
|
||||||
|
// If we use setNeutralButton and etc. dialog will close after pressing the buttons,
|
||||||
|
// but we want it to close only when positive button is pressed
|
||||||
|
dialog.getButton(AlertDialog.BUTTON_NEGATIVE).setOnClickListener(v ->
|
||||||
|
ShareUtils.openUrlInBrowser(this, kaoURI)
|
||||||
|
);
|
||||||
|
dialog.getButton(AlertDialog.BUTTON_NEUTRAL).setOnClickListener(v ->
|
||||||
|
ShareUtils.openUrlInBrowser(this, solutionURI)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,12 +76,15 @@ class NewVersionWorker(
|
|||||||
)
|
)
|
||||||
.setContentText(
|
.setContentText(
|
||||||
applicationContext.getString(
|
applicationContext.getString(
|
||||||
R.string.app_update_available_notification_text, versionName
|
R.string.app_update_available_notification_text,
|
||||||
|
versionName
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
val notificationManager = NotificationManagerCompat.from(applicationContext)
|
val notificationManager = NotificationManagerCompat.from(applicationContext)
|
||||||
notificationManager.notify(2000, notificationBuilder.build())
|
if (notificationManager.areNotificationsEnabled()) {
|
||||||
|
notificationManager.notify(2000, notificationBuilder.build())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Throws(IOException::class, ReCaptchaException::class)
|
@Throws(IOException::class, ReCaptchaException::class)
|
||||||
|
|||||||
@@ -41,50 +41,50 @@ public final class QueueItemMenuUtil {
|
|||||||
}
|
}
|
||||||
|
|
||||||
popupMenu.setOnMenuItemClickListener(menuItem -> {
|
popupMenu.setOnMenuItemClickListener(menuItem -> {
|
||||||
switch (menuItem.getItemId()) {
|
final int itemId = menuItem.getItemId();
|
||||||
case R.id.menu_item_remove:
|
if (itemId == R.id.menu_item_remove) {
|
||||||
final int index = playQueue.indexOf(item);
|
final int index = playQueue.indexOf(item);
|
||||||
playQueue.remove(index);
|
playQueue.remove(index);
|
||||||
return true;
|
return true;
|
||||||
case R.id.menu_item_details:
|
} else if (itemId == R.id.menu_item_details) {
|
||||||
// playQueue is null since we don't want any queue change
|
// playQueue is null since we don't want any queue change
|
||||||
NavigationHelper.openVideoDetail(context, item.getServiceId(),
|
NavigationHelper.openVideoDetail(context, item.getServiceId(),
|
||||||
item.getUrl(), item.getTitle(), null,
|
item.getUrl(), item.getTitle(), null,
|
||||||
false);
|
false);
|
||||||
return true;
|
return true;
|
||||||
case R.id.menu_item_append_playlist:
|
} else if (itemId == R.id.menu_item_append_playlist) {
|
||||||
PlaylistDialog.createCorrespondingDialog(
|
PlaylistDialog.createCorrespondingDialog(
|
||||||
context,
|
context,
|
||||||
List.of(new StreamEntity(item)),
|
List.of(new StreamEntity(item)),
|
||||||
dialog -> dialog.show(
|
dialog -> dialog.show(
|
||||||
fragmentManager,
|
fragmentManager,
|
||||||
"QueueItemMenuUtil@append_playlist"
|
"QueueItemMenuUtil@append_playlist"
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
case R.id.menu_item_channel_details:
|
} else if (itemId == R.id.menu_item_channel_details) {
|
||||||
SparseItemUtil.fetchUploaderUrlIfSparse(context, item.getServiceId(),
|
SparseItemUtil.fetchUploaderUrlIfSparse(context, item.getServiceId(),
|
||||||
item.getUrl(), item.getUploaderUrl(),
|
item.getUrl(), item.getUploaderUrl(),
|
||||||
// An intent must be used here.
|
// An intent must be used here.
|
||||||
// Opening with FragmentManager transactions is not working,
|
// Opening with FragmentManager transactions is not working,
|
||||||
// as PlayQueueActivity doesn't use fragments.
|
// as PlayQueueActivity doesn't use fragments.
|
||||||
uploaderUrl -> NavigationHelper.openChannelFragmentUsingIntent(
|
uploaderUrl -> NavigationHelper.openChannelFragmentUsingIntent(
|
||||||
context, item.getServiceId(), uploaderUrl, item.getUploader()
|
context, item.getServiceId(), uploaderUrl, item.getUploader()
|
||||||
));
|
));
|
||||||
return true;
|
return true;
|
||||||
case R.id.menu_item_share:
|
} else if (itemId == R.id.menu_item_share) {
|
||||||
shareText(context, item.getTitle(), item.getUrl(),
|
shareText(context, item.getTitle(), item.getUrl(),
|
||||||
item.getThumbnails());
|
item.getThumbnails());
|
||||||
return true;
|
return true;
|
||||||
case R.id.menu_item_download:
|
} else if (itemId == R.id.menu_item_download) {
|
||||||
fetchStreamInfoAndSaveToDatabase(context, item.getServiceId(), item.getUrl(),
|
fetchStreamInfoAndSaveToDatabase(context, item.getServiceId(), item.getUrl(),
|
||||||
info -> {
|
info -> {
|
||||||
final DownloadDialog downloadDialog = new DownloadDialog(context,
|
final DownloadDialog downloadDialog = new DownloadDialog(context,
|
||||||
info);
|
info);
|
||||||
downloadDialog.show(fragmentManager, "downloadDialog");
|
downloadDialog.show(fragmentManager, "downloadDialog");
|
||||||
});
|
});
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -343,8 +343,7 @@ public class RouterActivity extends AppCompatActivity {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
final List<StreamingService.ServiceInfo.MediaCapability> capabilities =
|
final var capabilities = currentService.getServiceInfo().getMediaCapabilities();
|
||||||
currentService.getServiceInfo().getMediaCapabilities();
|
|
||||||
|
|
||||||
// Check if the service supports the choice
|
// Check if the service supports the choice
|
||||||
if ((isVideoPlayerSelected && capabilities.contains(VIDEO))
|
if ((isVideoPlayerSelected && capabilities.contains(VIDEO))
|
||||||
@@ -528,8 +527,7 @@ public class RouterActivity extends AppCompatActivity {
|
|||||||
final List<AdapterChoiceItem> returnedItems = new ArrayList<>();
|
final List<AdapterChoiceItem> returnedItems = new ArrayList<>();
|
||||||
returnedItems.add(showInfo); // Always present
|
returnedItems.add(showInfo); // Always present
|
||||||
|
|
||||||
final List<StreamingService.ServiceInfo.MediaCapability> capabilities =
|
final var capabilities = service.getServiceInfo().getMediaCapabilities();
|
||||||
service.getServiceInfo().getMediaCapabilities();
|
|
||||||
|
|
||||||
if (linkType == LinkType.STREAM || linkType == LinkType.PLAYLIST) {
|
if (linkType == LinkType.STREAM || linkType == LinkType.PLAYLIST) {
|
||||||
if (capabilities.contains(VIDEO)) {
|
if (capabilities.contains(VIDEO)) {
|
||||||
|
|||||||
@@ -92,7 +92,7 @@ class AboutActivity : AppCompatActivity() {
|
|||||||
return when (position) {
|
return when (position) {
|
||||||
posAbout -> AboutFragment()
|
posAbout -> AboutFragment()
|
||||||
posLicense -> LicenseFragment.newInstance(SOFTWARE_COMPONENTS)
|
posLicense -> LicenseFragment.newInstance(SOFTWARE_COMPONENTS)
|
||||||
else -> throw IllegalArgumentException("Unknown position for ViewPager2")
|
else -> error("Unknown position for ViewPager2")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -105,7 +105,7 @@ class AboutActivity : AppCompatActivity() {
|
|||||||
return when (position) {
|
return when (position) {
|
||||||
posAbout -> R.string.tab_about
|
posAbout -> R.string.tab_about
|
||||||
posLicense -> R.string.tab_licenses
|
posLicense -> R.string.tab_licenses
|
||||||
else -> throw IllegalArgumentException("Unknown position for ViewPager2")
|
else -> error("Unknown position for ViewPager2")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -116,86 +116,152 @@ 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.",
|
"Coil",
|
||||||
"https://square.github.io/picasso/", StandardLicenses.APACHE2
|
"2023",
|
||||||
|
"Coil Contributors",
|
||||||
|
"https://coil-kt.github.io/coil/",
|
||||||
|
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
|
||||||
),
|
),
|
||||||
|
SoftwareComponent(
|
||||||
|
"FreeDroidWarn",
|
||||||
|
"2026",
|
||||||
|
"woheller69",
|
||||||
|
"https://github.com/woheller69/FreeDroidWarn",
|
||||||
|
StandardLicenses.APACHE2
|
||||||
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,7 +8,6 @@ package org.schabi.newpipe.database
|
|||||||
|
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import androidx.room.migration.Migration
|
import androidx.room.migration.Migration
|
||||||
import androidx.sqlite.db.SupportSQLiteDatabase
|
|
||||||
import org.schabi.newpipe.MainActivity
|
import org.schabi.newpipe.MainActivity
|
||||||
|
|
||||||
object Migrations {
|
object Migrations {
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -62,11 +62,7 @@ data class PlaylistRemoteEntity(
|
|||||||
orderingName = playlistInfo.name,
|
orderingName = playlistInfo.name,
|
||||||
url = playlistInfo.url,
|
url = playlistInfo.url,
|
||||||
thumbnailUrl = ImageStrategy.imageListToDbUrl(
|
thumbnailUrl = ImageStrategy.imageListToDbUrl(
|
||||||
if (playlistInfo.thumbnails.isEmpty()) {
|
playlistInfo.thumbnails.ifEmpty { playlistInfo.uploaderAvatars }
|
||||||
playlistInfo.uploaderAvatars
|
|
||||||
} else {
|
|
||||||
playlistInfo.thumbnails
|
|
||||||
}
|
|
||||||
),
|
),
|
||||||
uploader = playlistInfo.uploaderName,
|
uploader = playlistInfo.uploaderName,
|
||||||
streamCount = playlistInfo.streamCount
|
streamCount = playlistInfo.streamCount
|
||||||
|
|||||||
@@ -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> {
|
||||||
@@ -87,11 +87,10 @@ abstract class StreamDAO : BasicDAO<StreamEntity> {
|
|||||||
|
|
||||||
private fun compareAndUpdateStream(newerStream: StreamEntity) {
|
private fun compareAndUpdateStream(newerStream: StreamEntity) {
|
||||||
val existentMinimalStream = getMinimalStreamForCompare(newerStream.serviceId, newerStream.url)
|
val existentMinimalStream = getMinimalStreamForCompare(newerStream.serviceId, newerStream.url)
|
||||||
?: throw IllegalStateException("Stream cannot be null just after insertion.")
|
?: error("Stream cannot be null just after insertion.")
|
||||||
newerStream.uid = existentMinimalStream.uid
|
newerStream.uid = existentMinimalStream.uid
|
||||||
|
|
||||||
if (!StreamTypeUtil.isLiveStream(newerStream.streamType)) {
|
if (!StreamTypeUtil.isLiveStream(newerStream.streamType)) {
|
||||||
|
|
||||||
// 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)
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -100,7 +100,7 @@ abstract class SubscriptionDAO : BasicDAO<SubscriptionEntity> {
|
|||||||
entity.uid = uidFromInsert
|
entity.uid = uidFromInsert
|
||||||
} else {
|
} else {
|
||||||
val subscriptionIdFromDb = getSubscriptionIdInternal(entity.serviceId, entity.url!!)
|
val subscriptionIdFromDb = getSubscriptionIdInternal(entity.serviceId, entity.url!!)
|
||||||
?: throw IllegalStateException("Subscription cannot be null just after insertion.")
|
?: error("Subscription cannot be null just after insertion.")
|
||||||
entity.uid = subscriptionIdFromDb
|
entity.uid = subscriptionIdFromDb
|
||||||
|
|
||||||
update(entity)
|
update(entity)
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import android.os.IBinder;
|
|||||||
import android.provider.Settings;
|
import android.provider.Settings;
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
import android.view.LayoutInflater;
|
import android.view.LayoutInflater;
|
||||||
|
import android.view.MenuItem;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
import android.view.ViewGroup;
|
import android.view.ViewGroup;
|
||||||
import android.widget.AdapterView;
|
import android.widget.AdapterView;
|
||||||
@@ -31,7 +32,6 @@ import androidx.annotation.NonNull;
|
|||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
import androidx.annotation.StringRes;
|
import androidx.annotation.StringRes;
|
||||||
import androidx.appcompat.app.AlertDialog;
|
import androidx.appcompat.app.AlertDialog;
|
||||||
import androidx.appcompat.view.menu.ActionMenuItemView;
|
|
||||||
import androidx.appcompat.widget.Toolbar;
|
import androidx.appcompat.widget.Toolbar;
|
||||||
import androidx.collection.SparseArrayCompat;
|
import androidx.collection.SparseArrayCompat;
|
||||||
import androidx.documentfile.provider.DocumentFile;
|
import androidx.documentfile.provider.DocumentFile;
|
||||||
@@ -113,7 +113,7 @@ public class DownloadDialog extends DialogFragment
|
|||||||
private StoredDirectoryHelper mainStorageAudio = null;
|
private StoredDirectoryHelper mainStorageAudio = null;
|
||||||
private StoredDirectoryHelper mainStorageVideo = null;
|
private StoredDirectoryHelper mainStorageVideo = null;
|
||||||
private DownloadManager downloadManager = null;
|
private DownloadManager downloadManager = null;
|
||||||
private ActionMenuItemView okButton = null;
|
private MenuItem okButton = null;
|
||||||
private Context context = null;
|
private Context context = null;
|
||||||
private boolean askForSavePath;
|
private boolean askForSavePath;
|
||||||
|
|
||||||
@@ -344,7 +344,7 @@ public class DownloadDialog extends DialogFragment
|
|||||||
toolbar.setNavigationOnClickListener(v -> dismiss());
|
toolbar.setNavigationOnClickListener(v -> dismiss());
|
||||||
toolbar.setNavigationContentDescription(R.string.cancel);
|
toolbar.setNavigationContentDescription(R.string.cancel);
|
||||||
|
|
||||||
okButton = toolbar.findViewById(R.id.okay);
|
okButton = toolbar.getMenu().findItem(R.id.okay);
|
||||||
okButton.setEnabled(false); // disable until the download service connection is done
|
okButton.setEnabled(false); // disable until the download service connection is done
|
||||||
|
|
||||||
toolbar.setOnMenuItemClickListener(item -> {
|
toolbar.setOnMenuItemClickListener(item -> {
|
||||||
@@ -558,17 +558,13 @@ public class DownloadDialog extends DialogFragment
|
|||||||
}
|
}
|
||||||
boolean flag = true;
|
boolean flag = true;
|
||||||
|
|
||||||
switch (checkedId) {
|
if (checkedId == R.id.audio_button) {
|
||||||
case R.id.audio_button:
|
setupAudioSpinner();
|
||||||
setupAudioSpinner();
|
} else if (checkedId == R.id.video_button) {
|
||||||
break;
|
setupVideoSpinner();
|
||||||
case R.id.video_button:
|
} else if (checkedId == R.id.subtitle_button) {
|
||||||
setupVideoSpinner();
|
setupSubtitleSpinner();
|
||||||
break;
|
flag = false;
|
||||||
case R.id.subtitle_button:
|
|
||||||
setupSubtitleSpinner();
|
|
||||||
flag = false;
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
dialogBinding.threads.setEnabled(flag);
|
dialogBinding.threads.setEnabled(flag);
|
||||||
@@ -585,29 +581,26 @@ public class DownloadDialog extends DialogFragment
|
|||||||
+ "position = [" + position + "], id = [" + id + "]");
|
+ "position = [" + position + "], id = [" + id + "]");
|
||||||
}
|
}
|
||||||
|
|
||||||
switch (parent.getId()) {
|
final int parentId = parent.getId();
|
||||||
case R.id.quality_spinner:
|
if (parentId == R.id.quality_spinner) {
|
||||||
switch (dialogBinding.videoAudioGroup.getCheckedRadioButtonId()) {
|
final int checkedRadioButtonId = dialogBinding.videoAudioGroup
|
||||||
case R.id.video_button:
|
.getCheckedRadioButtonId();
|
||||||
selectedVideoIndex = position;
|
if (checkedRadioButtonId == R.id.video_button) {
|
||||||
onVideoStreamSelected();
|
selectedVideoIndex = position;
|
||||||
break;
|
onVideoStreamSelected();
|
||||||
case R.id.subtitle_button:
|
} else if (checkedRadioButtonId == R.id.subtitle_button) {
|
||||||
selectedSubtitleIndex = position;
|
selectedSubtitleIndex = position;
|
||||||
break;
|
}
|
||||||
}
|
onItemSelectedSetFileName();
|
||||||
onItemSelectedSetFileName();
|
} else if (parentId == R.id.audio_track_spinner) {
|
||||||
break;
|
final boolean trackChanged = selectedAudioTrackIndex != position;
|
||||||
case R.id.audio_track_spinner:
|
selectedAudioTrackIndex = position;
|
||||||
final boolean trackChanged = selectedAudioTrackIndex != position;
|
if (trackChanged) {
|
||||||
selectedAudioTrackIndex = position;
|
updateSecondaryStreams();
|
||||||
if (trackChanged) {
|
fetchStreamsSize();
|
||||||
updateSecondaryStreams();
|
}
|
||||||
fetchStreamsSize();
|
} else if (parentId == R.id.audio_stream_spinner) {
|
||||||
}
|
selectedAudioIndex = position;
|
||||||
break;
|
|
||||||
case R.id.audio_stream_spinner:
|
|
||||||
selectedAudioIndex = position;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -622,23 +615,20 @@ public class DownloadDialog extends DialogFragment
|
|||||||
|| prevFileName.startsWith(getString(R.string.caption_file_name, fileName, ""))) {
|
|| prevFileName.startsWith(getString(R.string.caption_file_name, fileName, ""))) {
|
||||||
// only update the file name field if it was not edited by the user
|
// only update the file name field if it was not edited by the user
|
||||||
|
|
||||||
switch (dialogBinding.videoAudioGroup.getCheckedRadioButtonId()) {
|
final int radioButtonId = dialogBinding.videoAudioGroup
|
||||||
case R.id.audio_button:
|
.getCheckedRadioButtonId();
|
||||||
case R.id.video_button:
|
if (radioButtonId == R.id.audio_button || radioButtonId == R.id.video_button) {
|
||||||
if (!prevFileName.equals(fileName)) {
|
if (!prevFileName.equals(fileName)) {
|
||||||
// since the user might have switched between audio and video, the correct
|
// since the user might have switched between audio and video, the correct
|
||||||
// text might already be in place, so avoid resetting the cursor position
|
// text might already be in place, so avoid resetting the cursor position
|
||||||
dialogBinding.fileName.setText(fileName);
|
dialogBinding.fileName.setText(fileName);
|
||||||
}
|
}
|
||||||
break;
|
} else if (radioButtonId == R.id.subtitle_button) {
|
||||||
|
final String setSubtitleLanguageCode = subtitleStreamsAdapter
|
||||||
case R.id.subtitle_button:
|
.getItem(selectedSubtitleIndex).getLanguageTag();
|
||||||
final String setSubtitleLanguageCode = subtitleStreamsAdapter
|
// this will reset the cursor position, which is bad UX, but it can't be avoided
|
||||||
.getItem(selectedSubtitleIndex).getLanguageTag();
|
dialogBinding.fileName.setText(getString(
|
||||||
// this will reset the cursor position, which is bad UX, but it can't be avoided
|
R.string.caption_file_name, fileName, setSubtitleLanguageCode));
|
||||||
dialogBinding.fileName.setText(getString(
|
|
||||||
R.string.caption_file_name, fileName, setSubtitleLanguageCode));
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -770,47 +760,44 @@ public class DownloadDialog extends DialogFragment
|
|||||||
|
|
||||||
filenameTmp = getNameEditText().concat(".");
|
filenameTmp = getNameEditText().concat(".");
|
||||||
|
|
||||||
switch (dialogBinding.videoAudioGroup.getCheckedRadioButtonId()) {
|
final int checkedRadioButtonId = dialogBinding.videoAudioGroup.getCheckedRadioButtonId();
|
||||||
case R.id.audio_button:
|
if (checkedRadioButtonId == R.id.audio_button) {
|
||||||
selectedMediaType = getString(R.string.last_download_type_audio_key);
|
selectedMediaType = getString(R.string.last_download_type_audio_key);
|
||||||
mainStorage = mainStorageAudio;
|
mainStorage = mainStorageAudio;
|
||||||
format = audioStreamsAdapter.getItem(selectedAudioIndex).getFormat();
|
format = audioStreamsAdapter.getItem(selectedAudioIndex).getFormat();
|
||||||
size = getWrappedAudioStreams().getSizeInBytes(selectedAudioIndex);
|
size = getWrappedAudioStreams().getSizeInBytes(selectedAudioIndex);
|
||||||
if (format == MediaFormat.WEBMA_OPUS) {
|
if (format == MediaFormat.WEBMA_OPUS) {
|
||||||
mimeTmp = "audio/ogg";
|
mimeTmp = "audio/ogg";
|
||||||
filenameTmp += "opus";
|
filenameTmp += "opus";
|
||||||
} else if (format != null) {
|
} else if (format != null) {
|
||||||
mimeTmp = format.mimeType;
|
mimeTmp = format.mimeType;
|
||||||
filenameTmp += format.getSuffix();
|
filenameTmp += format.getSuffix();
|
||||||
}
|
}
|
||||||
break;
|
} else if (checkedRadioButtonId == R.id.video_button) {
|
||||||
case R.id.video_button:
|
selectedMediaType = getString(R.string.last_download_type_video_key);
|
||||||
selectedMediaType = getString(R.string.last_download_type_video_key);
|
mainStorage = mainStorageVideo;
|
||||||
mainStorage = mainStorageVideo;
|
format = videoStreamsAdapter.getItem(selectedVideoIndex).getFormat();
|
||||||
format = videoStreamsAdapter.getItem(selectedVideoIndex).getFormat();
|
size = wrappedVideoStreams.getSizeInBytes(selectedVideoIndex);
|
||||||
size = wrappedVideoStreams.getSizeInBytes(selectedVideoIndex);
|
if (format != null) {
|
||||||
if (format != null) {
|
mimeTmp = format.mimeType;
|
||||||
mimeTmp = format.mimeType;
|
filenameTmp += format.getSuffix();
|
||||||
filenameTmp += format.getSuffix();
|
}
|
||||||
}
|
} else if (checkedRadioButtonId == R.id.subtitle_button) {
|
||||||
break;
|
selectedMediaType = getString(R.string.last_download_type_subtitle_key);
|
||||||
case R.id.subtitle_button:
|
mainStorage = mainStorageVideo; // subtitle & video files go together
|
||||||
selectedMediaType = getString(R.string.last_download_type_subtitle_key);
|
format = subtitleStreamsAdapter.getItem(selectedSubtitleIndex).getFormat();
|
||||||
mainStorage = mainStorageVideo; // subtitle & video files go together
|
size = wrappedSubtitleStreams.getSizeInBytes(selectedSubtitleIndex);
|
||||||
format = subtitleStreamsAdapter.getItem(selectedSubtitleIndex).getFormat();
|
if (format != null) {
|
||||||
size = wrappedSubtitleStreams.getSizeInBytes(selectedSubtitleIndex);
|
mimeTmp = format.mimeType;
|
||||||
if (format != null) {
|
}
|
||||||
mimeTmp = format.mimeType;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (format == MediaFormat.TTML) {
|
if (format == MediaFormat.TTML) {
|
||||||
filenameTmp += MediaFormat.SRT.getSuffix();
|
filenameTmp += MediaFormat.SRT.getSuffix();
|
||||||
} else if (format != null) {
|
} else if (format != null) {
|
||||||
filenameTmp += format.getSuffix();
|
filenameTmp += format.getSuffix();
|
||||||
}
|
}
|
||||||
break;
|
} else {
|
||||||
default:
|
throw new RuntimeException("No stream selected");
|
||||||
throw new RuntimeException("No stream selected");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!askForSavePath && (mainStorage == null
|
if (!askForSavePath && (mainStorage == null
|
||||||
@@ -1057,59 +1044,56 @@ public class DownloadDialog extends DialogFragment
|
|||||||
long nearLength = 0;
|
long nearLength = 0;
|
||||||
|
|
||||||
// more download logic: select muxer, subtitle converter, etc.
|
// more download logic: select muxer, subtitle converter, etc.
|
||||||
switch (dialogBinding.videoAudioGroup.getCheckedRadioButtonId()) {
|
final int checkedRadioButtonId = dialogBinding.videoAudioGroup.getCheckedRadioButtonId();
|
||||||
case R.id.audio_button:
|
if (checkedRadioButtonId == R.id.audio_button) {
|
||||||
kind = 'a';
|
kind = 'a';
|
||||||
selectedStream = audioStreamsAdapter.getItem(selectedAudioIndex);
|
selectedStream = audioStreamsAdapter.getItem(selectedAudioIndex);
|
||||||
|
|
||||||
if (selectedStream.getFormat() == MediaFormat.M4A) {
|
if (selectedStream.getFormat() == MediaFormat.M4A) {
|
||||||
psName = Postprocessing.ALGORITHM_M4A_NO_DASH;
|
psName = Postprocessing.ALGORITHM_M4A_NO_DASH;
|
||||||
} else if (selectedStream.getFormat() == MediaFormat.WEBMA_OPUS) {
|
} else if (selectedStream.getFormat() == MediaFormat.WEBMA_OPUS) {
|
||||||
psName = Postprocessing.ALGORITHM_OGG_FROM_WEBM_DEMUXER;
|
psName = Postprocessing.ALGORITHM_OGG_FROM_WEBM_DEMUXER;
|
||||||
|
}
|
||||||
|
} else if (checkedRadioButtonId == R.id.video_button) {
|
||||||
|
kind = 'v';
|
||||||
|
selectedStream = videoStreamsAdapter.getItem(selectedVideoIndex);
|
||||||
|
|
||||||
|
final SecondaryStreamHelper<AudioStream> secondary = videoStreamsAdapter
|
||||||
|
.getAllSecondary()
|
||||||
|
.get(wrappedVideoStreams.getStreamsList().indexOf(selectedStream));
|
||||||
|
|
||||||
|
if (secondary != null) {
|
||||||
|
secondaryStream = secondary.getStream();
|
||||||
|
|
||||||
|
if (selectedStream.getFormat() == MediaFormat.MPEG_4) {
|
||||||
|
psName = Postprocessing.ALGORITHM_MP4_FROM_DASH_MUXER;
|
||||||
|
} else {
|
||||||
|
psName = Postprocessing.ALGORITHM_WEBM_MUXER;
|
||||||
}
|
}
|
||||||
break;
|
|
||||||
case R.id.video_button:
|
|
||||||
kind = 'v';
|
|
||||||
selectedStream = videoStreamsAdapter.getItem(selectedVideoIndex);
|
|
||||||
|
|
||||||
final SecondaryStreamHelper<AudioStream> secondary = videoStreamsAdapter
|
final long videoSize = wrappedVideoStreams.getSizeInBytes(
|
||||||
.getAllSecondary()
|
(VideoStream) selectedStream);
|
||||||
.get(wrappedVideoStreams.getStreamsList().indexOf(selectedStream));
|
|
||||||
|
|
||||||
if (secondary != null) {
|
// set nearLength, only, if both sizes are fetched or known. This probably
|
||||||
secondaryStream = secondary.getStream();
|
// does not work on slow networks but is later updated in the downloader
|
||||||
|
if (secondary.getSizeInBytes() > 0 && videoSize > 0) {
|
||||||
if (selectedStream.getFormat() == MediaFormat.MPEG_4) {
|
nearLength = secondary.getSizeInBytes() + videoSize;
|
||||||
psName = Postprocessing.ALGORITHM_MP4_FROM_DASH_MUXER;
|
|
||||||
} else {
|
|
||||||
psName = Postprocessing.ALGORITHM_WEBM_MUXER;
|
|
||||||
}
|
|
||||||
|
|
||||||
final long videoSize = wrappedVideoStreams.getSizeInBytes(
|
|
||||||
(VideoStream) selectedStream);
|
|
||||||
|
|
||||||
// set nearLength, only, if both sizes are fetched or known. This probably
|
|
||||||
// does not work on slow networks but is later updated in the downloader
|
|
||||||
if (secondary.getSizeInBytes() > 0 && videoSize > 0) {
|
|
||||||
nearLength = secondary.getSizeInBytes() + videoSize;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
break;
|
}
|
||||||
case R.id.subtitle_button:
|
} else if (checkedRadioButtonId == R.id.subtitle_button) {
|
||||||
threads = 1; // use unique thread for subtitles due small file size
|
threads = 1; // use unique thread for subtitles due small file size
|
||||||
kind = 's';
|
kind = 's';
|
||||||
selectedStream = subtitleStreamsAdapter.getItem(selectedSubtitleIndex);
|
selectedStream = subtitleStreamsAdapter.getItem(selectedSubtitleIndex);
|
||||||
|
|
||||||
if (selectedStream.getFormat() == MediaFormat.TTML) {
|
if (selectedStream.getFormat() == MediaFormat.TTML) {
|
||||||
psName = Postprocessing.ALGORITHM_TTML_CONVERTER;
|
psName = Postprocessing.ALGORITHM_TTML_CONVERTER;
|
||||||
psArgs = new String[] {
|
psArgs = new String[]{
|
||||||
selectedStream.getFormat().getSuffix(),
|
selectedStream.getFormat().getSuffix(),
|
||||||
"false" // ignore empty frames
|
"false" // ignore empty frames
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
break;
|
} else {
|
||||||
default:
|
return;
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (secondaryStream == null) {
|
if (secondaryStream == null) {
|
||||||
|
|||||||
@@ -1,324 +0,0 @@
|
|||||||
package org.schabi.newpipe.error;
|
|
||||||
|
|
||||||
import android.content.Context;
|
|
||||||
import android.content.Intent;
|
|
||||||
import android.net.Uri;
|
|
||||||
import android.os.Build;
|
|
||||||
import android.os.Bundle;
|
|
||||||
import android.util.Log;
|
|
||||||
import android.view.Menu;
|
|
||||||
import android.view.MenuInflater;
|
|
||||||
import android.view.MenuItem;
|
|
||||||
|
|
||||||
import androidx.appcompat.app.ActionBar;
|
|
||||||
import androidx.appcompat.app.AlertDialog;
|
|
||||||
import androidx.appcompat.app.AppCompatActivity;
|
|
||||||
import androidx.core.content.IntentCompat;
|
|
||||||
|
|
||||||
import com.grack.nanojson.JsonWriter;
|
|
||||||
|
|
||||||
import org.schabi.newpipe.BuildConfig;
|
|
||||||
import org.schabi.newpipe.R;
|
|
||||||
import org.schabi.newpipe.databinding.ActivityErrorBinding;
|
|
||||||
import org.schabi.newpipe.util.Localization;
|
|
||||||
import org.schabi.newpipe.util.ThemeHelper;
|
|
||||||
import org.schabi.newpipe.util.external_communication.ShareUtils;
|
|
||||||
|
|
||||||
import java.time.ZonedDateTime;
|
|
||||||
import java.time.format.DateTimeFormatter;
|
|
||||||
import java.util.Arrays;
|
|
||||||
import java.util.stream.Collectors;
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Created by Christian Schabesberger on 24.10.15.
|
|
||||||
*
|
|
||||||
* Copyright (C) Christian Schabesberger 2016 <chris.schabesberger@mailbox.org>
|
|
||||||
* ErrorActivity.java is part of NewPipe.
|
|
||||||
*
|
|
||||||
* NewPipe is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
* <
|
|
||||||
* NewPipe is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
* <
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with NewPipe. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This activity is used to show error details and allow reporting them in various ways. Use {@link
|
|
||||||
* ErrorUtil#openActivity(Context, ErrorInfo)} to correctly open this activity.
|
|
||||||
*/
|
|
||||||
public class ErrorActivity extends AppCompatActivity {
|
|
||||||
// LOG TAGS
|
|
||||||
public static final String TAG = ErrorActivity.class.toString();
|
|
||||||
// BUNDLE TAGS
|
|
||||||
public static final String ERROR_INFO = "error_info";
|
|
||||||
|
|
||||||
public static final String ERROR_EMAIL_ADDRESS = "crashreport@newpipe.schabi.org";
|
|
||||||
public static final String ERROR_EMAIL_SUBJECT = "Exception in ";
|
|
||||||
|
|
||||||
public static final String ERROR_GITHUB_ISSUE_URL =
|
|
||||||
"https://github.com/TeamNewPipe/NewPipe/issues";
|
|
||||||
|
|
||||||
private ErrorInfo errorInfo;
|
|
||||||
private String currentTimeStamp;
|
|
||||||
|
|
||||||
private ActivityErrorBinding activityErrorBinding;
|
|
||||||
|
|
||||||
|
|
||||||
////////////////////////////////////////////////////////////////////////
|
|
||||||
// Activity lifecycle
|
|
||||||
////////////////////////////////////////////////////////////////////////
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void onCreate(final Bundle savedInstanceState) {
|
|
||||||
super.onCreate(savedInstanceState);
|
|
||||||
|
|
||||||
ThemeHelper.setDayNightMode(this);
|
|
||||||
ThemeHelper.setTheme(this);
|
|
||||||
|
|
||||||
activityErrorBinding = ActivityErrorBinding.inflate(getLayoutInflater());
|
|
||||||
setContentView(activityErrorBinding.getRoot());
|
|
||||||
|
|
||||||
final Intent intent = getIntent();
|
|
||||||
|
|
||||||
setSupportActionBar(activityErrorBinding.toolbarLayout.toolbar);
|
|
||||||
|
|
||||||
final ActionBar actionBar = getSupportActionBar();
|
|
||||||
if (actionBar != null) {
|
|
||||||
actionBar.setDisplayHomeAsUpEnabled(true);
|
|
||||||
actionBar.setTitle(R.string.error_report_title);
|
|
||||||
actionBar.setDisplayShowTitleEnabled(true);
|
|
||||||
}
|
|
||||||
|
|
||||||
errorInfo = IntentCompat.getParcelableExtra(intent, ERROR_INFO, ErrorInfo.class);
|
|
||||||
|
|
||||||
// important add guru meditation
|
|
||||||
addGuruMeditation();
|
|
||||||
// print current time, as zoned ISO8601 timestamp
|
|
||||||
final ZonedDateTime now = ZonedDateTime.now();
|
|
||||||
currentTimeStamp = now.format(DateTimeFormatter.ISO_OFFSET_DATE_TIME);
|
|
||||||
|
|
||||||
activityErrorBinding.errorReportEmailButton.setOnClickListener(v ->
|
|
||||||
openPrivacyPolicyDialog(this, "EMAIL"));
|
|
||||||
|
|
||||||
activityErrorBinding.errorReportCopyButton.setOnClickListener(v ->
|
|
||||||
ShareUtils.copyToClipboard(this, buildMarkdown()));
|
|
||||||
|
|
||||||
activityErrorBinding.errorReportGitHubButton.setOnClickListener(v ->
|
|
||||||
openPrivacyPolicyDialog(this, "GITHUB"));
|
|
||||||
|
|
||||||
// normal bugreport
|
|
||||||
buildInfo(errorInfo);
|
|
||||||
activityErrorBinding.errorMessageView.setText(errorInfo.getMessage(this));
|
|
||||||
activityErrorBinding.errorView.setText(formErrorText(errorInfo.getStackTraces()));
|
|
||||||
|
|
||||||
// print stack trace once again for debugging:
|
|
||||||
for (final String e : errorInfo.getStackTraces()) {
|
|
||||||
Log.e(TAG, e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean onCreateOptionsMenu(final Menu menu) {
|
|
||||||
final MenuInflater inflater = getMenuInflater();
|
|
||||||
inflater.inflate(R.menu.error_menu, menu);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean onOptionsItemSelected(final MenuItem item) {
|
|
||||||
switch (item.getItemId()) {
|
|
||||||
case android.R.id.home:
|
|
||||||
onBackPressed();
|
|
||||||
return true;
|
|
||||||
case R.id.menu_item_share_error:
|
|
||||||
ShareUtils.shareText(getApplicationContext(),
|
|
||||||
getString(R.string.error_report_title), buildJson());
|
|
||||||
return true;
|
|
||||||
default:
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void openPrivacyPolicyDialog(final Context context, final String action) {
|
|
||||||
new AlertDialog.Builder(context)
|
|
||||||
.setIcon(android.R.drawable.ic_dialog_alert)
|
|
||||||
.setTitle(R.string.privacy_policy_title)
|
|
||||||
.setMessage(R.string.start_accept_privacy_policy)
|
|
||||||
.setCancelable(false)
|
|
||||||
.setNeutralButton(R.string.read_privacy_policy, (dialog, which) ->
|
|
||||||
ShareUtils.openUrlInApp(context,
|
|
||||||
context.getString(R.string.privacy_policy_url)))
|
|
||||||
.setPositiveButton(R.string.accept, (dialog, which) -> {
|
|
||||||
if (action.equals("EMAIL")) { // send on email
|
|
||||||
final Intent i = new Intent(Intent.ACTION_SENDTO)
|
|
||||||
.setData(Uri.parse("mailto:")) // only email apps should handle this
|
|
||||||
.putExtra(Intent.EXTRA_EMAIL, new String[]{ERROR_EMAIL_ADDRESS})
|
|
||||||
.putExtra(Intent.EXTRA_SUBJECT, ERROR_EMAIL_SUBJECT
|
|
||||||
+ getString(R.string.app_name) + " "
|
|
||||||
+ BuildConfig.VERSION_NAME)
|
|
||||||
.putExtra(Intent.EXTRA_TEXT, buildJson());
|
|
||||||
ShareUtils.openIntentInApp(context, i);
|
|
||||||
} else if (action.equals("GITHUB")) { // open the NewPipe issue page on GitHub
|
|
||||||
ShareUtils.openUrlInApp(this, ERROR_GITHUB_ISSUE_URL);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.setNegativeButton(R.string.decline, null)
|
|
||||||
.show();
|
|
||||||
}
|
|
||||||
|
|
||||||
private String formErrorText(final String[] el) {
|
|
||||||
final String separator = "-------------------------------------";
|
|
||||||
return Arrays.stream(el)
|
|
||||||
.collect(Collectors.joining(separator + "\n", separator + "\n", separator));
|
|
||||||
}
|
|
||||||
|
|
||||||
private void buildInfo(final ErrorInfo info) {
|
|
||||||
String text = "";
|
|
||||||
|
|
||||||
activityErrorBinding.errorInfoLabelsView.setText(getString(R.string.info_labels)
|
|
||||||
.replace("\\n", "\n"));
|
|
||||||
|
|
||||||
text += getUserActionString(info.getUserAction()) + "\n"
|
|
||||||
+ info.getRequest() + "\n"
|
|
||||||
+ getContentLanguageString() + "\n"
|
|
||||||
+ getContentCountryString() + "\n"
|
|
||||||
+ getAppLanguage() + "\n"
|
|
||||||
+ info.getServiceName() + "\n"
|
|
||||||
+ currentTimeStamp + "\n"
|
|
||||||
+ getPackageName() + "\n"
|
|
||||||
+ BuildConfig.VERSION_NAME + "\n"
|
|
||||||
+ getOsString();
|
|
||||||
|
|
||||||
activityErrorBinding.errorInfosView.setText(text);
|
|
||||||
}
|
|
||||||
|
|
||||||
private String buildJson() {
|
|
||||||
try {
|
|
||||||
return JsonWriter.string()
|
|
||||||
.object()
|
|
||||||
.value("user_action", getUserActionString(errorInfo.getUserAction()))
|
|
||||||
.value("request", errorInfo.getRequest())
|
|
||||||
.value("content_language", getContentLanguageString())
|
|
||||||
.value("content_country", getContentCountryString())
|
|
||||||
.value("app_language", getAppLanguage())
|
|
||||||
.value("service", errorInfo.getServiceName())
|
|
||||||
.value("package", getPackageName())
|
|
||||||
.value("version", BuildConfig.VERSION_NAME)
|
|
||||||
.value("os", getOsString())
|
|
||||||
.value("time", currentTimeStamp)
|
|
||||||
.array("exceptions", Arrays.asList(errorInfo.getStackTraces()))
|
|
||||||
.value("user_comment", activityErrorBinding.errorCommentBox.getText()
|
|
||||||
.toString())
|
|
||||||
.end()
|
|
||||||
.done();
|
|
||||||
} catch (final Throwable e) {
|
|
||||||
Log.e(TAG, "Error while erroring: Could not build json");
|
|
||||||
e.printStackTrace();
|
|
||||||
}
|
|
||||||
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
|
|
||||||
private String buildMarkdown() {
|
|
||||||
try {
|
|
||||||
final StringBuilder htmlErrorReport = new StringBuilder();
|
|
||||||
|
|
||||||
final String userComment = activityErrorBinding.errorCommentBox.getText().toString();
|
|
||||||
if (!userComment.isEmpty()) {
|
|
||||||
htmlErrorReport.append(userComment).append("\n");
|
|
||||||
}
|
|
||||||
|
|
||||||
// basic error info
|
|
||||||
htmlErrorReport
|
|
||||||
.append("## Exception")
|
|
||||||
.append("\n* __User Action:__ ")
|
|
||||||
.append(getUserActionString(errorInfo.getUserAction()))
|
|
||||||
.append("\n* __Request:__ ").append(errorInfo.getRequest())
|
|
||||||
.append("\n* __Content Country:__ ").append(getContentCountryString())
|
|
||||||
.append("\n* __Content Language:__ ").append(getContentLanguageString())
|
|
||||||
.append("\n* __App Language:__ ").append(getAppLanguage())
|
|
||||||
.append("\n* __Service:__ ").append(errorInfo.getServiceName())
|
|
||||||
.append("\n* __Timestamp:__ ").append(currentTimeStamp)
|
|
||||||
.append("\n* __Package:__ ").append(getPackageName())
|
|
||||||
.append("\n* __Service:__ ").append(errorInfo.getServiceName())
|
|
||||||
.append("\n* __Version:__ ").append(BuildConfig.VERSION_NAME)
|
|
||||||
.append("\n* __OS:__ ").append(getOsString()).append("\n");
|
|
||||||
|
|
||||||
|
|
||||||
// Collapse all logs to a single paragraph when there are more than one
|
|
||||||
// to keep the GitHub issue clean.
|
|
||||||
if (errorInfo.getStackTraces().length > 1) {
|
|
||||||
htmlErrorReport
|
|
||||||
.append("<details><summary><b>Exceptions (")
|
|
||||||
.append(errorInfo.getStackTraces().length)
|
|
||||||
.append(")</b></summary><p>\n");
|
|
||||||
}
|
|
||||||
|
|
||||||
// add the logs
|
|
||||||
for (int i = 0; i < errorInfo.getStackTraces().length; i++) {
|
|
||||||
htmlErrorReport.append("<details><summary><b>Crash log ");
|
|
||||||
if (errorInfo.getStackTraces().length > 1) {
|
|
||||||
htmlErrorReport.append(i + 1);
|
|
||||||
}
|
|
||||||
htmlErrorReport.append("</b>")
|
|
||||||
.append("</summary><p>\n")
|
|
||||||
.append("\n```\n").append(errorInfo.getStackTraces()[i]).append("\n```\n")
|
|
||||||
.append("</details>\n");
|
|
||||||
}
|
|
||||||
|
|
||||||
// make sure to close everything
|
|
||||||
if (errorInfo.getStackTraces().length > 1) {
|
|
||||||
htmlErrorReport.append("</p></details>\n");
|
|
||||||
}
|
|
||||||
htmlErrorReport.append("<hr>\n");
|
|
||||||
return htmlErrorReport.toString();
|
|
||||||
} catch (final Throwable e) {
|
|
||||||
Log.e(TAG, "Error while erroring: Could not build markdown");
|
|
||||||
e.printStackTrace();
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private String getUserActionString(final UserAction userAction) {
|
|
||||||
if (userAction == null) {
|
|
||||||
return "Your description is in another castle.";
|
|
||||||
} else {
|
|
||||||
return userAction.getMessage();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private String getContentCountryString() {
|
|
||||||
return Localization.getPreferredContentCountry(this).getCountryCode();
|
|
||||||
}
|
|
||||||
|
|
||||||
private String getContentLanguageString() {
|
|
||||||
return Localization.getPreferredLocalization(this).getLocalizationCode();
|
|
||||||
}
|
|
||||||
|
|
||||||
private String getAppLanguage() {
|
|
||||||
return Localization.getAppLocale().toString();
|
|
||||||
}
|
|
||||||
|
|
||||||
private String getOsString() {
|
|
||||||
final String osBase = Build.VERSION.SDK_INT >= Build.VERSION_CODES.M
|
|
||||||
? Build.VERSION.BASE_OS : "Android";
|
|
||||||
return System.getProperty("os.name")
|
|
||||||
+ " " + (osBase.isEmpty() ? "Android" : osBase)
|
|
||||||
+ " " + Build.VERSION.RELEASE
|
|
||||||
+ " - " + Build.VERSION.SDK_INT;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void addGuruMeditation() {
|
|
||||||
//just an easter egg
|
|
||||||
String text = activityErrorBinding.errorSorryView.getText().toString();
|
|
||||||
text += "\n" + getString(R.string.guru_meditation);
|
|
||||||
activityErrorBinding.errorSorryView.setText(text);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
281
app/src/main/java/org/schabi/newpipe/error/ErrorActivity.kt
Normal file
281
app/src/main/java/org/schabi/newpipe/error/ErrorActivity.kt
Normal file
@@ -0,0 +1,281 @@
|
|||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: 2015-2026 NewPipe contributors <https://newpipe.net>
|
||||||
|
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.schabi.newpipe.error
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.os.Build
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.util.Log
|
||||||
|
import android.view.Menu
|
||||||
|
import android.view.MenuItem
|
||||||
|
import androidx.appcompat.app.AlertDialog
|
||||||
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
|
import androidx.core.content.IntentCompat
|
||||||
|
import androidx.core.net.toUri
|
||||||
|
import com.grack.nanojson.JsonWriter
|
||||||
|
import java.time.ZonedDateTime
|
||||||
|
import java.time.format.DateTimeFormatter
|
||||||
|
import org.schabi.newpipe.BuildConfig
|
||||||
|
import org.schabi.newpipe.R
|
||||||
|
import org.schabi.newpipe.databinding.ActivityErrorBinding
|
||||||
|
import org.schabi.newpipe.util.Localization
|
||||||
|
import org.schabi.newpipe.util.ThemeHelper
|
||||||
|
import org.schabi.newpipe.util.external_communication.ShareUtils
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This activity is used to show error details and allow reporting them in various ways.
|
||||||
|
* Use [ErrorUtil.openActivity] to correctly open this activity.
|
||||||
|
*/
|
||||||
|
class ErrorActivity : AppCompatActivity() {
|
||||||
|
private lateinit var errorInfo: ErrorInfo
|
||||||
|
private lateinit var currentTimeStamp: String
|
||||||
|
|
||||||
|
private lateinit var binding: ActivityErrorBinding
|
||||||
|
|
||||||
|
private val contentCountryString: String
|
||||||
|
get() = Localization.getPreferredContentCountry(this).countryCode
|
||||||
|
|
||||||
|
private val contentLanguageString: String
|
||||||
|
get() = Localization.getPreferredLocalization(this).localizationCode
|
||||||
|
|
||||||
|
private val appLanguage: String
|
||||||
|
get() = Localization.getAppLocale().toString()
|
||||||
|
|
||||||
|
private val osString: String
|
||||||
|
get() {
|
||||||
|
val name = System.getProperty("os.name")!!
|
||||||
|
val osBase = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||||
|
Build.VERSION.BASE_OS.ifEmpty { "Android" }
|
||||||
|
} else {
|
||||||
|
"Android"
|
||||||
|
}
|
||||||
|
return "$name $osBase ${Build.VERSION.RELEASE} - ${Build.VERSION.SDK_INT}"
|
||||||
|
}
|
||||||
|
|
||||||
|
private val errorEmailSubject: String
|
||||||
|
get() = "$ERROR_EMAIL_SUBJECT ${getString(R.string.app_name)} ${BuildConfig.VERSION_NAME}"
|
||||||
|
|
||||||
|
// /////////////////////////////////////////////////////////////////////
|
||||||
|
// Activity lifecycle
|
||||||
|
// /////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
|
||||||
|
ThemeHelper.setDayNightMode(this)
|
||||||
|
ThemeHelper.setTheme(this)
|
||||||
|
|
||||||
|
binding = ActivityErrorBinding.inflate(layoutInflater)
|
||||||
|
setContentView(binding.getRoot())
|
||||||
|
|
||||||
|
setSupportActionBar(binding.toolbarLayout.toolbar)
|
||||||
|
supportActionBar?.apply {
|
||||||
|
setDisplayHomeAsUpEnabled(true)
|
||||||
|
setTitle(R.string.error_report_title)
|
||||||
|
setDisplayShowTitleEnabled(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
errorInfo = IntentCompat.getParcelableExtra(intent, ERROR_INFO, ErrorInfo::class.java)!!
|
||||||
|
|
||||||
|
// important add guru meditation
|
||||||
|
addGuruMeditation()
|
||||||
|
// print current time, as zoned ISO8601 timestamp
|
||||||
|
currentTimeStamp = ZonedDateTime.now().format(DateTimeFormatter.ISO_OFFSET_DATE_TIME)
|
||||||
|
|
||||||
|
binding.errorReportEmailButton.setOnClickListener { _ ->
|
||||||
|
openPrivacyPolicyDialog(this, "EMAIL")
|
||||||
|
}
|
||||||
|
|
||||||
|
binding.errorReportCopyButton.setOnClickListener { _ ->
|
||||||
|
ShareUtils.copyToClipboard(this, buildMarkdown())
|
||||||
|
}
|
||||||
|
|
||||||
|
binding.errorReportGitHubButton.setOnClickListener { _ ->
|
||||||
|
openPrivacyPolicyDialog(this, "GITHUB")
|
||||||
|
}
|
||||||
|
|
||||||
|
// normal bugreport
|
||||||
|
buildInfo(errorInfo)
|
||||||
|
binding.errorMessageView.text = errorInfo.getMessage(this)
|
||||||
|
binding.errorView.text = formErrorText(errorInfo.stackTraces)
|
||||||
|
|
||||||
|
// print stack trace once again for debugging:
|
||||||
|
errorInfo.stackTraces.forEach { Log.e(TAG, it) }
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreateOptionsMenu(menu: Menu): Boolean {
|
||||||
|
menuInflater.inflate(R.menu.error_menu, menu)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||||
|
return when (item.itemId) {
|
||||||
|
android.R.id.home -> {
|
||||||
|
onBackPressed()
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
R.id.menu_item_share_error -> {
|
||||||
|
ShareUtils.shareText(
|
||||||
|
applicationContext,
|
||||||
|
getString(R.string.error_report_title),
|
||||||
|
buildJson()
|
||||||
|
)
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
else -> false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun openPrivacyPolicyDialog(context: Context, action: String) {
|
||||||
|
AlertDialog.Builder(context)
|
||||||
|
.setIcon(android.R.drawable.ic_dialog_alert)
|
||||||
|
.setTitle(R.string.privacy_policy_title)
|
||||||
|
.setMessage(R.string.start_accept_privacy_policy)
|
||||||
|
.setCancelable(false)
|
||||||
|
.setNeutralButton(R.string.read_privacy_policy) { _, _ ->
|
||||||
|
ShareUtils.openUrlInApp(context, context.getString(R.string.privacy_policy_url))
|
||||||
|
}
|
||||||
|
.setPositiveButton(R.string.accept) { _, _ ->
|
||||||
|
if (action == "EMAIL") { // send on email
|
||||||
|
val intent = Intent(Intent.ACTION_SENDTO)
|
||||||
|
.setData("mailto:".toUri()) // only email apps should handle this
|
||||||
|
.putExtra(Intent.EXTRA_EMAIL, arrayOf(ERROR_EMAIL_ADDRESS))
|
||||||
|
.putExtra(Intent.EXTRA_SUBJECT, errorEmailSubject)
|
||||||
|
.putExtra(Intent.EXTRA_TEXT, buildJson())
|
||||||
|
ShareUtils.openIntentInApp(context, intent)
|
||||||
|
} else if (action == "GITHUB") { // open the NewPipe issue page on GitHub
|
||||||
|
ShareUtils.openUrlInApp(this, ERROR_GITHUB_ISSUE_URL)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.setNegativeButton(R.string.decline, null)
|
||||||
|
.show()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun formErrorText(stacktrace: Array<String>): String {
|
||||||
|
val separator = "-------------------------------------"
|
||||||
|
return stacktrace.joinToString(separator + "\n", separator + "\n", separator)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun buildInfo(info: ErrorInfo) {
|
||||||
|
binding.errorInfoLabelsView.text = getString(R.string.info_labels)
|
||||||
|
|
||||||
|
val text = info.userAction.message + "\n" +
|
||||||
|
info.request + "\n" +
|
||||||
|
contentLanguageString + "\n" +
|
||||||
|
contentCountryString + "\n" +
|
||||||
|
appLanguage + "\n" +
|
||||||
|
info.getServiceName() + "\n" +
|
||||||
|
currentTimeStamp + "\n" +
|
||||||
|
packageName + "\n" +
|
||||||
|
BuildConfig.VERSION_NAME + "\n" +
|
||||||
|
osString
|
||||||
|
|
||||||
|
binding.errorInfosView.text = text
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun buildJson(): String {
|
||||||
|
try {
|
||||||
|
return JsonWriter.string()
|
||||||
|
.`object`()
|
||||||
|
.value("user_action", errorInfo.userAction.message)
|
||||||
|
.value("request", errorInfo.request)
|
||||||
|
.value("content_language", contentLanguageString)
|
||||||
|
.value("content_country", contentCountryString)
|
||||||
|
.value("app_language", appLanguage)
|
||||||
|
.value("service", errorInfo.getServiceName())
|
||||||
|
.value("package", packageName)
|
||||||
|
.value("version", BuildConfig.VERSION_NAME)
|
||||||
|
.value("os", osString)
|
||||||
|
.value("time", currentTimeStamp)
|
||||||
|
.array("exceptions", errorInfo.stackTraces.toList())
|
||||||
|
.value("user_comment", binding.errorCommentBox.getText().toString())
|
||||||
|
.end()
|
||||||
|
.done()
|
||||||
|
} catch (exception: Exception) {
|
||||||
|
Log.e(TAG, "Error while erroring: Could not build json", exception)
|
||||||
|
}
|
||||||
|
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun buildMarkdown(): String {
|
||||||
|
try {
|
||||||
|
return buildString(1024) {
|
||||||
|
val userComment = binding.errorCommentBox.text.toString()
|
||||||
|
if (userComment.isNotEmpty()) {
|
||||||
|
appendLine(userComment)
|
||||||
|
}
|
||||||
|
|
||||||
|
// basic error info
|
||||||
|
appendLine("## Exception")
|
||||||
|
appendLine("* __User Action:__ ${errorInfo.userAction.message}")
|
||||||
|
appendLine("* __Request:__ ${errorInfo.request}")
|
||||||
|
appendLine("* __Content Country:__ $contentCountryString")
|
||||||
|
appendLine("* __Content Language:__ $contentLanguageString")
|
||||||
|
appendLine("* __App Language:__ $appLanguage")
|
||||||
|
appendLine("* __Service:__ ${errorInfo.getServiceName()}")
|
||||||
|
appendLine("* __Timestamp:__ $currentTimeStamp")
|
||||||
|
appendLine("* __Package:__ $packageName")
|
||||||
|
appendLine("* __Service:__ ${errorInfo.getServiceName()}")
|
||||||
|
appendLine("* __Version:__ ${BuildConfig.VERSION_NAME}")
|
||||||
|
appendLine("* __OS:__ $osString")
|
||||||
|
|
||||||
|
// Collapse all logs to a single paragraph when there are more than one
|
||||||
|
// to keep the GitHub issue clean.
|
||||||
|
if (errorInfo.stackTraces.size > 1) {
|
||||||
|
append("<details><summary><b>Exceptions (")
|
||||||
|
append(errorInfo.stackTraces.size)
|
||||||
|
append(")</b></summary><p>\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
// add the logs
|
||||||
|
errorInfo.stackTraces.forEachIndexed { index, stacktrace ->
|
||||||
|
append("<details><summary><b>Crash log ")
|
||||||
|
if (errorInfo.stackTraces.size > 1) {
|
||||||
|
append(index + 1)
|
||||||
|
}
|
||||||
|
append("</b>")
|
||||||
|
append("</summary><p>\n")
|
||||||
|
append("\n```\n${stacktrace}\n```\n")
|
||||||
|
append("</details>\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
// make sure to close everything
|
||||||
|
if (errorInfo.stackTraces.size > 1) {
|
||||||
|
append("</p></details>\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
append("<hr>\n")
|
||||||
|
}
|
||||||
|
} catch (exception: Exception) {
|
||||||
|
Log.e(TAG, "Error while erroring: Could not build markdown", exception)
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun addGuruMeditation() {
|
||||||
|
// just an easter egg
|
||||||
|
var text = binding.errorSorryView.text.toString()
|
||||||
|
text += "\n" + getString(R.string.guru_meditation)
|
||||||
|
binding.errorSorryView.text = text
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
// LOG TAGS
|
||||||
|
private val TAG = ErrorActivity::class.java.toString()
|
||||||
|
|
||||||
|
// BUNDLE TAGS
|
||||||
|
const val ERROR_INFO = "error_info"
|
||||||
|
|
||||||
|
private const val ERROR_EMAIL_ADDRESS = "crashreport@newpipe.schabi.org"
|
||||||
|
private const val ERROR_EMAIL_SUBJECT = "Exception in "
|
||||||
|
|
||||||
|
private const val ERROR_GITHUB_ISSUE_URL = "https://github.com/TeamNewPipe/NewPipe/issues"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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,
|
|
||||||
// video deleted, etc.), there is no use in letting users report it
|
// if the service explicitly said that content is not available (e.g. age
|
||||||
is ContentNotAvailableException -> false
|
// restrictions, video deleted, etc.), there is no use in letting users report it
|
||||||
|
is ContentNotAvailableException -> !isContentSurelyNotAvailable(throwable)
|
||||||
|
|
||||||
// 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
|
||||||
}
|
}
|
||||||
@@ -292,14 +318,39 @@ class ErrorInfo private constructor(
|
|||||||
|
|
||||||
fun isRetryable(throwable: Throwable?): Boolean {
|
fun isRetryable(throwable: Throwable?): Boolean {
|
||||||
return when (throwable) {
|
return when (throwable) {
|
||||||
// we know the content is not available, retrying won't help
|
// if we know the content is surely not available, retrying won't help
|
||||||
is ContentNotAvailableException -> false
|
is ContentNotAvailableException -> !isContentSurelyNotAvailable(throwable)
|
||||||
|
|
||||||
// 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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Unfortunately sometimes [ContentNotAvailableException] may not indicate that the content
|
||||||
|
* is blocked/deleted/paid, but may just indicate that we could not extract it. This is an
|
||||||
|
* inconsistency in the exceptions thrown by the extractor, but until it is fixed, this
|
||||||
|
* function will distinguish between the two types.
|
||||||
|
* @return `true` if the content is not available because of a limitation imposed by the
|
||||||
|
* service or the owner, `false` if the extractor could not extract info about it
|
||||||
|
*/
|
||||||
|
fun isContentSurelyNotAvailable(e: ContentNotAvailableException): Boolean {
|
||||||
|
return when (e) {
|
||||||
|
is AccountTerminatedException,
|
||||||
|
is AgeRestrictedContentException,
|
||||||
|
is GeographicRestrictionException,
|
||||||
|
is PaidContentException,
|
||||||
|
is PrivateContentException,
|
||||||
|
is SoundCloudGoPlusContentException,
|
||||||
|
is UnsupportedContentInCountryException,
|
||||||
|
is YoutubeMusicPremiumContentException -> true
|
||||||
|
|
||||||
|
else -> false
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 {
|
||||||
@@ -134,8 +134,11 @@ class ErrorUtil {
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
NotificationManagerCompat.from(context)
|
val notificationManager = NotificationManagerCompat.from(context)
|
||||||
.notify(ERROR_REPORT_NOTIFICATION_ID, notificationBuilder.build())
|
if (notificationManager.areNotificationsEnabled()) {
|
||||||
|
notificationManager
|
||||||
|
.notify(ERROR_REPORT_NOTIFICATION_ID, notificationBuilder.build())
|
||||||
|
}
|
||||||
|
|
||||||
ContextCompat.getMainExecutor(context).execute {
|
ContextCompat.getMainExecutor(context).execute {
|
||||||
// since the notification is silent, also show a toast, otherwise the user is confused
|
// since the notification is silent, also show a toast, otherwise the user is confused
|
||||||
|
|||||||
@@ -126,6 +126,7 @@ public class ReCaptchaActivity extends AppCompatActivity {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@SuppressLint("MissingSuperCall") // saveCookiesAndFinish method handles back navigation
|
||||||
public void onBackPressed() {
|
public void onBackPressed() {
|
||||||
saveCookiesAndFinish();
|
saveCookiesAndFinish();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -118,7 +118,7 @@ import org.schabi.newpipe.util.StreamTypeUtil;
|
|||||||
import org.schabi.newpipe.util.ThemeHelper;
|
import org.schabi.newpipe.util.ThemeHelper;
|
||||||
import org.schabi.newpipe.util.external_communication.KoreUtils;
|
import org.schabi.newpipe.util.external_communication.KoreUtils;
|
||||||
import org.schabi.newpipe.util.external_communication.ShareUtils;
|
import org.schabi.newpipe.util.external_communication.ShareUtils;
|
||||||
import org.schabi.newpipe.util.image.PicassoHelper;
|
import org.schabi.newpipe.util.image.CoilHelper;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Iterator;
|
import java.util.Iterator;
|
||||||
@@ -129,6 +129,7 @@ import java.util.Optional;
|
|||||||
import java.util.concurrent.TimeUnit;
|
import java.util.concurrent.TimeUnit;
|
||||||
import java.util.function.Consumer;
|
import java.util.function.Consumer;
|
||||||
|
|
||||||
|
import coil3.util.CoilUtils;
|
||||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
|
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
|
||||||
import io.reactivex.rxjava3.disposables.CompositeDisposable;
|
import io.reactivex.rxjava3.disposables.CompositeDisposable;
|
||||||
import io.reactivex.rxjava3.disposables.Disposable;
|
import io.reactivex.rxjava3.disposables.Disposable;
|
||||||
@@ -160,8 +161,6 @@ public final class VideoDetailFragment
|
|||||||
private static final String DESCRIPTION_TAB_TAG = "DESCRIPTION TAB";
|
private static final String DESCRIPTION_TAB_TAG = "DESCRIPTION TAB";
|
||||||
private static final String EMPTY_TAB_TAG = "EMPTY TAB";
|
private static final String EMPTY_TAB_TAG = "EMPTY TAB";
|
||||||
|
|
||||||
private static final String PICASSO_VIDEO_DETAILS_TAG = "PICASSO_VIDEO_DETAILS_TAG";
|
|
||||||
|
|
||||||
// tabs
|
// tabs
|
||||||
private boolean showComments;
|
private boolean showComments;
|
||||||
private boolean showRelatedItems;
|
private boolean showRelatedItems;
|
||||||
@@ -206,8 +205,6 @@ public final class VideoDetailFragment
|
|||||||
int lastStableBottomSheetState = BottomSheetBehavior.STATE_EXPANDED;
|
int lastStableBottomSheetState = BottomSheetBehavior.STATE_EXPANDED;
|
||||||
@State
|
@State
|
||||||
protected boolean autoPlayEnabled = true;
|
protected boolean autoPlayEnabled = true;
|
||||||
@State
|
|
||||||
protected int originalOrientation = ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED;
|
|
||||||
|
|
||||||
@Nullable
|
@Nullable
|
||||||
private StreamInfo currentInfo = null;
|
private StreamInfo currentInfo = null;
|
||||||
@@ -648,6 +645,12 @@ public final class VideoDetailFragment
|
|||||||
protected void initListeners() {
|
protected void initListeners() {
|
||||||
super.initListeners();
|
super.initListeners();
|
||||||
|
|
||||||
|
// Workaround for #5600
|
||||||
|
// Forcefully catch click events uncaught by children because otherwise
|
||||||
|
// they will be caught by underlying view and "click through" will happen
|
||||||
|
binding.getRoot().setOnClickListener(v -> { });
|
||||||
|
binding.getRoot().setOnLongClickListener(v -> true);
|
||||||
|
|
||||||
setOnClickListeners();
|
setOnClickListeners();
|
||||||
setOnLongClickListeners();
|
setOnLongClickListeners();
|
||||||
|
|
||||||
@@ -1424,8 +1427,10 @@ public final class VideoDetailFragment
|
|||||||
bottomSheetBehavior.setState(BottomSheetBehavior.STATE_COLLAPSED);
|
bottomSheetBehavior.setState(BottomSheetBehavior.STATE_COLLAPSED);
|
||||||
}
|
}
|
||||||
// Rebound to the service if it was closed via notification or mini player
|
// Rebound to the service if it was closed via notification or mini player
|
||||||
playerHolder.setListener(VideoDetailFragment.this);
|
if (!playerHolder.isBound()) {
|
||||||
playerHolder.tryBindIfNeeded(context);
|
playerHolder.startService(
|
||||||
|
false, VideoDetailFragment.this);
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1493,7 +1498,10 @@ public final class VideoDetailFragment
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
PicassoHelper.cancelTag(PICASSO_VIDEO_DETAILS_TAG);
|
CoilUtils.dispose(binding.detailThumbnailImageView);
|
||||||
|
CoilUtils.dispose(binding.detailSubChannelThumbnailView);
|
||||||
|
CoilUtils.dispose(binding.overlayThumbnail);
|
||||||
|
CoilUtils.dispose(binding.detailUploaderThumbnailView);
|
||||||
binding.detailThumbnailImageView.setImageBitmap(null);
|
binding.detailThumbnailImageView.setImageBitmap(null);
|
||||||
binding.detailSubChannelThumbnailView.setImageBitmap(null);
|
binding.detailSubChannelThumbnailView.setImageBitmap(null);
|
||||||
}
|
}
|
||||||
@@ -1584,8 +1592,8 @@ public final class VideoDetailFragment
|
|||||||
binding.detailSecondaryControlPanel.setVisibility(View.GONE);
|
binding.detailSecondaryControlPanel.setVisibility(View.GONE);
|
||||||
|
|
||||||
checkUpdateProgressInfo(info);
|
checkUpdateProgressInfo(info);
|
||||||
PicassoHelper.loadDetailsThumbnail(info.getThumbnails()).tag(PICASSO_VIDEO_DETAILS_TAG)
|
CoilHelper.INSTANCE.loadDetailsThumbnail(binding.detailThumbnailImageView,
|
||||||
.into(binding.detailThumbnailImageView);
|
info.getThumbnails());
|
||||||
showMetaInfoInTextView(info.getMetaInfo(), binding.detailMetaInfoTextView,
|
showMetaInfoInTextView(info.getMetaInfo(), binding.detailMetaInfoTextView,
|
||||||
binding.detailMetaInfoSeparator, disposables);
|
binding.detailMetaInfoSeparator, disposables);
|
||||||
|
|
||||||
@@ -1635,8 +1643,8 @@ public final class VideoDetailFragment
|
|||||||
binding.detailUploaderTextView.setVisibility(View.GONE);
|
binding.detailUploaderTextView.setVisibility(View.GONE);
|
||||||
}
|
}
|
||||||
|
|
||||||
PicassoHelper.loadAvatar(info.getUploaderAvatars()).tag(PICASSO_VIDEO_DETAILS_TAG)
|
CoilHelper.INSTANCE.loadAvatar(binding.detailSubChannelThumbnailView,
|
||||||
.into(binding.detailSubChannelThumbnailView);
|
info.getUploaderAvatars());
|
||||||
binding.detailSubChannelThumbnailView.setVisibility(View.VISIBLE);
|
binding.detailSubChannelThumbnailView.setVisibility(View.VISIBLE);
|
||||||
binding.detailUploaderThumbnailView.setVisibility(View.GONE);
|
binding.detailUploaderThumbnailView.setVisibility(View.GONE);
|
||||||
}
|
}
|
||||||
@@ -1667,11 +1675,11 @@ public final class VideoDetailFragment
|
|||||||
binding.detailUploaderTextView.setVisibility(View.GONE);
|
binding.detailUploaderTextView.setVisibility(View.GONE);
|
||||||
}
|
}
|
||||||
|
|
||||||
PicassoHelper.loadAvatar(info.getSubChannelAvatars()).tag(PICASSO_VIDEO_DETAILS_TAG)
|
CoilHelper.INSTANCE.loadAvatar(binding.detailSubChannelThumbnailView,
|
||||||
.into(binding.detailSubChannelThumbnailView);
|
info.getSubChannelAvatars());
|
||||||
binding.detailSubChannelThumbnailView.setVisibility(View.VISIBLE);
|
binding.detailSubChannelThumbnailView.setVisibility(View.VISIBLE);
|
||||||
PicassoHelper.loadAvatar(info.getUploaderAvatars()).tag(PICASSO_VIDEO_DETAILS_TAG)
|
CoilHelper.INSTANCE.loadAvatar(binding.detailUploaderThumbnailView,
|
||||||
.into(binding.detailUploaderThumbnailView);
|
info.getUploaderAvatars());
|
||||||
binding.detailUploaderThumbnailView.setVisibility(View.VISIBLE);
|
binding.detailUploaderThumbnailView.setVisibility(View.VISIBLE);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1899,7 +1907,11 @@ public final class VideoDetailFragment
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (binding.relatedItemsLayout != null) {
|
if (binding.relatedItemsLayout != null) {
|
||||||
binding.relatedItemsLayout.setVisibility(fullscreen ? View.GONE : View.VISIBLE);
|
if (showRelatedItems) {
|
||||||
|
binding.relatedItemsLayout.setVisibility(fullscreen ? View.GONE : View.VISIBLE);
|
||||||
|
} else {
|
||||||
|
binding.relatedItemsLayout.setVisibility(View.GONE);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
scrollToTop();
|
scrollToTop();
|
||||||
|
|
||||||
@@ -1908,29 +1920,23 @@ public final class VideoDetailFragment
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onScreenRotationButtonClicked() {
|
public void onScreenRotationButtonClicked() {
|
||||||
final Optional<MainPlayerUi> playerUi = player != null
|
// On Android TV screen rotation is not supported
|
||||||
? player.UIs().get(MainPlayerUi.class)
|
// In tablet user experience will be better if screen will not be rotated
|
||||||
: Optional.empty();
|
// from landscape to portrait every time.
|
||||||
if (playerUi.isEmpty()) {
|
// Just turn on fullscreen mode in landscape orientation
|
||||||
|
// or portrait & unlocked global orientation
|
||||||
|
final boolean isLandscape = DeviceUtils.isLandscape(requireContext());
|
||||||
|
if (DeviceUtils.isTv(activity) || DeviceUtils.isTablet(activity)
|
||||||
|
&& (!globalScreenOrientationLocked(activity) || isLandscape)) {
|
||||||
|
player.UIs().get(MainPlayerUi.class).ifPresent(MainPlayerUi::toggleFullscreen);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// On tablets and TVs, just toggle fullscreen UI without orientation change.
|
final int newOrientation = isLandscape
|
||||||
if (DeviceUtils.isTablet(activity) || DeviceUtils.isTv(activity)) {
|
? ActivityInfo.SCREEN_ORIENTATION_PORTRAIT
|
||||||
playerUi.get().toggleFullscreen();
|
: ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE;
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (playerUi.get().isFullscreen()) {
|
activity.setRequestedOrientation(newOrientation);
|
||||||
// EXITING FULLSCREEN
|
|
||||||
playerUi.get().toggleFullscreen();
|
|
||||||
activity.setRequestedOrientation(originalOrientation);
|
|
||||||
} else {
|
|
||||||
// ENTERING FULLSCREEN
|
|
||||||
originalOrientation = activity.getRequestedOrientation();
|
|
||||||
playerUi.get().toggleFullscreen();
|
|
||||||
activity.setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@@ -2435,8 +2441,7 @@ public final class VideoDetailFragment
|
|||||||
binding.overlayTitleTextView.setText(isEmpty(overlayTitle) ? "" : overlayTitle);
|
binding.overlayTitleTextView.setText(isEmpty(overlayTitle) ? "" : overlayTitle);
|
||||||
binding.overlayChannelTextView.setText(isEmpty(uploader) ? "" : uploader);
|
binding.overlayChannelTextView.setText(isEmpty(uploader) ? "" : uploader);
|
||||||
binding.overlayThumbnail.setImageDrawable(null);
|
binding.overlayThumbnail.setImageDrawable(null);
|
||||||
PicassoHelper.loadDetailsThumbnail(thumbnails).tag(PICASSO_VIDEO_DETAILS_TAG)
|
CoilHelper.INSTANCE.loadDetailsThumbnail(binding.overlayThumbnail, thumbnails);
|
||||||
.into(binding.overlayThumbnail);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void setOverlayPlayPauseImage(final boolean playerIsPlaying) {
|
private void setOverlayPlayPauseImage(final boolean playerIsPlaying) {
|
||||||
|
|||||||
@@ -53,13 +53,14 @@ import org.schabi.newpipe.util.NavigationHelper;
|
|||||||
import org.schabi.newpipe.util.StateSaver;
|
import org.schabi.newpipe.util.StateSaver;
|
||||||
import org.schabi.newpipe.util.ThemeHelper;
|
import org.schabi.newpipe.util.ThemeHelper;
|
||||||
import org.schabi.newpipe.util.external_communication.ShareUtils;
|
import org.schabi.newpipe.util.external_communication.ShareUtils;
|
||||||
|
import org.schabi.newpipe.util.image.CoilHelper;
|
||||||
import org.schabi.newpipe.util.image.ImageStrategy;
|
import org.schabi.newpipe.util.image.ImageStrategy;
|
||||||
import org.schabi.newpipe.util.image.PicassoHelper;
|
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Queue;
|
import java.util.Queue;
|
||||||
import java.util.concurrent.TimeUnit;
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
|
import coil3.util.CoilUtils;
|
||||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
|
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
|
||||||
import io.reactivex.rxjava3.core.Observable;
|
import io.reactivex.rxjava3.core.Observable;
|
||||||
import io.reactivex.rxjava3.disposables.CompositeDisposable;
|
import io.reactivex.rxjava3.disposables.CompositeDisposable;
|
||||||
@@ -73,7 +74,6 @@ public class ChannelFragment extends BaseStateFragment<ChannelInfo>
|
|||||||
implements StateSaver.WriteRead {
|
implements StateSaver.WriteRead {
|
||||||
|
|
||||||
private static final int BUTTON_DEBOUNCE_INTERVAL = 100;
|
private static final int BUTTON_DEBOUNCE_INTERVAL = 100;
|
||||||
private static final String PICASSO_CHANNEL_TAG = "PICASSO_CHANNEL_TAG";
|
|
||||||
|
|
||||||
@State
|
@State
|
||||||
protected int serviceId = Constants.NO_SERVICE_ID;
|
protected int serviceId = Constants.NO_SERVICE_ID;
|
||||||
@@ -160,34 +160,29 @@ public class ChannelFragment extends BaseStateFragment<ChannelInfo>
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean onMenuItemSelected(@NonNull final MenuItem item) {
|
public boolean onMenuItemSelected(@NonNull final MenuItem item) {
|
||||||
switch (item.getItemId()) {
|
final int itemId = item.getItemId();
|
||||||
case R.id.menu_item_notify:
|
if (itemId == R.id.menu_item_notify) {
|
||||||
final boolean value = !item.isChecked();
|
final boolean value = !item.isChecked();
|
||||||
item.setEnabled(false);
|
item.setEnabled(false);
|
||||||
setNotify(value);
|
setNotify(value);
|
||||||
break;
|
} else if (itemId == R.id.action_settings) {
|
||||||
case R.id.action_settings:
|
NavigationHelper.openSettings(requireContext());
|
||||||
NavigationHelper.openSettings(requireContext());
|
} else if (itemId == R.id.menu_item_rss) {
|
||||||
break;
|
if (currentInfo != null) {
|
||||||
case R.id.menu_item_rss:
|
ShareUtils.openUrlInApp(requireContext(), currentInfo.getFeedUrl());
|
||||||
if (currentInfo != null) {
|
}
|
||||||
ShareUtils.openUrlInApp(requireContext(), currentInfo.getFeedUrl());
|
} else if (itemId == R.id.menu_item_openInBrowser) {
|
||||||
}
|
if (currentInfo != null) {
|
||||||
break;
|
ShareUtils.openUrlInBrowser(requireContext(),
|
||||||
case R.id.menu_item_openInBrowser:
|
currentInfo.getOriginalUrl());
|
||||||
if (currentInfo != null) {
|
}
|
||||||
ShareUtils.openUrlInBrowser(requireContext(),
|
} else if (itemId == R.id.menu_item_share) {
|
||||||
currentInfo.getOriginalUrl());
|
if (currentInfo != null) {
|
||||||
}
|
ShareUtils.shareText(requireContext(), name,
|
||||||
break;
|
currentInfo.getOriginalUrl(), currentInfo.getAvatars());
|
||||||
case R.id.menu_item_share:
|
}
|
||||||
if (currentInfo != null) {
|
} else {
|
||||||
ShareUtils.shareText(requireContext(), name,
|
return false;
|
||||||
currentInfo.getOriginalUrl(), currentInfo.getAvatars());
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -583,7 +578,9 @@ public class ChannelFragment extends BaseStateFragment<ChannelInfo>
|
|||||||
@Override
|
@Override
|
||||||
public void showLoading() {
|
public void showLoading() {
|
||||||
super.showLoading();
|
super.showLoading();
|
||||||
PicassoHelper.cancelTag(PICASSO_CHANNEL_TAG);
|
CoilUtils.dispose(binding.channelAvatarView);
|
||||||
|
CoilUtils.dispose(binding.channelBannerImage);
|
||||||
|
CoilUtils.dispose(binding.subChannelAvatarView);
|
||||||
animate(binding.channelSubscribeButton, false, 100);
|
animate(binding.channelSubscribeButton, false, 100);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -594,17 +591,15 @@ public class ChannelFragment extends BaseStateFragment<ChannelInfo>
|
|||||||
setInitialData(result.getServiceId(), result.getOriginalUrl(), result.getName());
|
setInitialData(result.getServiceId(), result.getOriginalUrl(), result.getName());
|
||||||
|
|
||||||
if (ImageStrategy.shouldLoadImages() && !result.getBanners().isEmpty()) {
|
if (ImageStrategy.shouldLoadImages() && !result.getBanners().isEmpty()) {
|
||||||
PicassoHelper.loadBanner(result.getBanners()).tag(PICASSO_CHANNEL_TAG)
|
CoilHelper.INSTANCE.loadBanner(binding.channelBannerImage, result.getBanners());
|
||||||
.into(binding.channelBannerImage);
|
|
||||||
} else {
|
} else {
|
||||||
// do not waste space for the banner, if the user disabled images or there is not one
|
// do not waste space for the banner, if the user disabled images or there is not one
|
||||||
binding.channelBannerImage.setImageDrawable(null);
|
binding.channelBannerImage.setImageDrawable(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
PicassoHelper.loadAvatar(result.getAvatars()).tag(PICASSO_CHANNEL_TAG)
|
CoilHelper.INSTANCE.loadAvatar(binding.channelAvatarView, result.getAvatars());
|
||||||
.into(binding.channelAvatarView);
|
CoilHelper.INSTANCE.loadAvatar(binding.subChannelAvatarView,
|
||||||
PicassoHelper.loadAvatar(result.getParentChannelAvatars()).tag(PICASSO_CHANNEL_TAG)
|
result.getParentChannelAvatars());
|
||||||
.into(binding.subChannelAvatarView);
|
|
||||||
|
|
||||||
binding.channelTitleView.setText(result.getName());
|
binding.channelTitleView.setText(result.getName());
|
||||||
binding.channelSubscriberView.setVisibility(View.VISIBLE);
|
binding.channelSubscriberView.setVisibility(View.VISIBLE);
|
||||||
|
|||||||
@@ -25,8 +25,8 @@ import org.schabi.newpipe.util.DeviceUtils;
|
|||||||
import org.schabi.newpipe.util.ExtractorHelper;
|
import org.schabi.newpipe.util.ExtractorHelper;
|
||||||
import org.schabi.newpipe.util.Localization;
|
import org.schabi.newpipe.util.Localization;
|
||||||
import org.schabi.newpipe.util.NavigationHelper;
|
import org.schabi.newpipe.util.NavigationHelper;
|
||||||
|
import org.schabi.newpipe.util.image.CoilHelper;
|
||||||
import org.schabi.newpipe.util.image.ImageStrategy;
|
import org.schabi.newpipe.util.image.ImageStrategy;
|
||||||
import org.schabi.newpipe.util.image.PicassoHelper;
|
|
||||||
import org.schabi.newpipe.util.text.TextLinkifier;
|
import org.schabi.newpipe.util.text.TextLinkifier;
|
||||||
import org.schabi.newpipe.util.text.LongPressLinkMovementMethod;
|
import org.schabi.newpipe.util.text.LongPressLinkMovementMethod;
|
||||||
|
|
||||||
@@ -84,7 +84,7 @@ public final class CommentRepliesFragment
|
|||||||
final CommentsInfoItem item = commentsInfoItem;
|
final CommentsInfoItem item = commentsInfoItem;
|
||||||
|
|
||||||
// load the author avatar
|
// load the author avatar
|
||||||
PicassoHelper.loadAvatar(item.getUploaderAvatars()).into(binding.authorAvatar);
|
CoilHelper.INSTANCE.loadAvatar(binding.authorAvatar, item.getUploaderAvatars());
|
||||||
binding.authorAvatar.setVisibility(ImageStrategy.shouldLoadImages()
|
binding.authorAvatar.setVisibility(ImageStrategy.shouldLoadImages()
|
||||||
? View.VISIBLE : View.GONE);
|
? View.VISIBLE : View.GONE);
|
||||||
|
|
||||||
|
|||||||
@@ -53,7 +53,7 @@ import org.schabi.newpipe.util.Localization;
|
|||||||
import org.schabi.newpipe.util.NavigationHelper;
|
import org.schabi.newpipe.util.NavigationHelper;
|
||||||
import org.schabi.newpipe.util.PlayButtonHelper;
|
import org.schabi.newpipe.util.PlayButtonHelper;
|
||||||
import org.schabi.newpipe.util.external_communication.ShareUtils;
|
import org.schabi.newpipe.util.external_communication.ShareUtils;
|
||||||
import org.schabi.newpipe.util.image.PicassoHelper;
|
import org.schabi.newpipe.util.image.CoilHelper;
|
||||||
import org.schabi.newpipe.util.text.TextEllipsizer;
|
import org.schabi.newpipe.util.text.TextEllipsizer;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
@@ -62,6 +62,7 @@ import java.util.concurrent.atomic.AtomicBoolean;
|
|||||||
import java.util.function.Supplier;
|
import java.util.function.Supplier;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
import coil3.util.CoilUtils;
|
||||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
|
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
|
||||||
import io.reactivex.rxjava3.core.Flowable;
|
import io.reactivex.rxjava3.core.Flowable;
|
||||||
import io.reactivex.rxjava3.core.Single;
|
import io.reactivex.rxjava3.core.Single;
|
||||||
@@ -71,8 +72,6 @@ import io.reactivex.rxjava3.disposables.Disposable;
|
|||||||
public class PlaylistFragment extends BaseListInfoFragment<StreamInfoItem, PlaylistInfo>
|
public class PlaylistFragment extends BaseListInfoFragment<StreamInfoItem, PlaylistInfo>
|
||||||
implements PlaylistControlViewHolder {
|
implements PlaylistControlViewHolder {
|
||||||
|
|
||||||
private static final String PICASSO_PLAYLIST_TAG = "PICASSO_PLAYLIST_TAG";
|
|
||||||
|
|
||||||
private CompositeDisposable disposables;
|
private CompositeDisposable disposables;
|
||||||
private Subscription bookmarkReactor;
|
private Subscription bookmarkReactor;
|
||||||
private AtomicBoolean isBookmarkButtonReady;
|
private AtomicBoolean isBookmarkButtonReady;
|
||||||
@@ -232,35 +231,30 @@ public class PlaylistFragment extends BaseListInfoFragment<StreamInfoItem, Playl
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean onOptionsItemSelected(final MenuItem item) {
|
public boolean onOptionsItemSelected(final MenuItem item) {
|
||||||
switch (item.getItemId()) {
|
final int itemId = item.getItemId();
|
||||||
case R.id.action_settings:
|
if (itemId == R.id.action_settings) {
|
||||||
NavigationHelper.openSettings(requireContext());
|
NavigationHelper.openSettings(requireContext());
|
||||||
break;
|
} else if (itemId == R.id.menu_item_openInBrowser) {
|
||||||
case R.id.menu_item_openInBrowser:
|
ShareUtils.openUrlInBrowser(requireContext(), url);
|
||||||
ShareUtils.openUrlInBrowser(requireContext(), url);
|
} else if (itemId == R.id.menu_item_share) {
|
||||||
break;
|
ShareUtils.shareText(requireContext(), name, url,
|
||||||
case R.id.menu_item_share:
|
currentInfo == null ? List.of() : currentInfo.getThumbnails());
|
||||||
ShareUtils.shareText(requireContext(), name, url,
|
} else if (itemId == R.id.menu_item_bookmark) {
|
||||||
currentInfo == null ? List.of() : currentInfo.getThumbnails());
|
onBookmarkClicked();
|
||||||
break;
|
} else if (itemId == R.id.menu_item_append_playlist) {
|
||||||
case R.id.menu_item_bookmark:
|
if (currentInfo != null) {
|
||||||
onBookmarkClicked();
|
disposables.add(PlaylistDialog.createCorrespondingDialog(
|
||||||
break;
|
getContext(),
|
||||||
case R.id.menu_item_append_playlist:
|
getPlayQueue()
|
||||||
if (currentInfo != null) {
|
.getStreams()
|
||||||
disposables.add(PlaylistDialog.createCorrespondingDialog(
|
.stream()
|
||||||
getContext(),
|
.map(StreamEntity::new)
|
||||||
getPlayQueue()
|
.collect(Collectors.toList()),
|
||||||
.getStreams()
|
dialog -> dialog.show(getFM(), TAG)
|
||||||
.stream()
|
));
|
||||||
.map(StreamEntity::new)
|
}
|
||||||
.collect(Collectors.toList()),
|
} else {
|
||||||
dialog -> dialog.show(getFM(), TAG)
|
return super.onOptionsItemSelected(item);
|
||||||
));
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
return super.onOptionsItemSelected(item);
|
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -276,7 +270,7 @@ public class PlaylistFragment extends BaseListInfoFragment<StreamInfoItem, Playl
|
|||||||
animate(headerBinding.getRoot(), false, 200);
|
animate(headerBinding.getRoot(), false, 200);
|
||||||
animateHideRecyclerViewAllowingScrolling(itemsList);
|
animateHideRecyclerViewAllowingScrolling(itemsList);
|
||||||
|
|
||||||
PicassoHelper.cancelTag(PICASSO_PLAYLIST_TAG);
|
CoilUtils.dispose(headerBinding.uploaderAvatarView);
|
||||||
animate(headerBinding.uploaderLayout, false, 200);
|
animate(headerBinding.uploaderLayout, false, 200);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -327,8 +321,8 @@ public class PlaylistFragment extends BaseListInfoFragment<StreamInfoItem, Playl
|
|||||||
R.drawable.ic_radio)
|
R.drawable.ic_radio)
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
PicassoHelper.loadAvatar(result.getUploaderAvatars()).tag(PICASSO_PLAYLIST_TAG)
|
CoilHelper.INSTANCE.loadAvatar(headerBinding.uploaderAvatarView,
|
||||||
.into(headerBinding.uploaderAvatarView);
|
result.getUploaderAvatars());
|
||||||
}
|
}
|
||||||
|
|
||||||
streamCount = result.getStreamCount();
|
streamCount = result.getStreamCount();
|
||||||
|
|||||||
@@ -1009,7 +1009,11 @@ public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.I
|
|||||||
Log.d(TAG, "handleSuggestions() called with: suggestions = [" + suggestions + "]");
|
Log.d(TAG, "handleSuggestions() called with: suggestions = [" + suggestions + "]");
|
||||||
}
|
}
|
||||||
suggestionListAdapter.submitList(suggestions,
|
suggestionListAdapter.submitList(suggestions,
|
||||||
() -> searchBinding.suggestionsList.scrollToPosition(0));
|
() -> {
|
||||||
|
if (searchBinding != null) {
|
||||||
|
searchBinding.suggestionsList.scrollToPosition(0);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
if (suggestionsPanelVisible && isErrorPanelVisible()) {
|
if (suggestionsPanelVisible && isErrorPanelVisible()) {
|
||||||
hideLoading();
|
hideLoading();
|
||||||
|
|||||||
@@ -1,131 +0,0 @@
|
|||||||
package org.schabi.newpipe.info_list;
|
|
||||||
|
|
||||||
import android.content.Context;
|
|
||||||
import android.view.View;
|
|
||||||
import android.view.ViewGroup;
|
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
|
||||||
|
|
||||||
import org.schabi.newpipe.extractor.InfoItem;
|
|
||||||
import org.schabi.newpipe.extractor.channel.ChannelInfoItem;
|
|
||||||
import org.schabi.newpipe.extractor.comments.CommentsInfoItem;
|
|
||||||
import org.schabi.newpipe.extractor.playlist.PlaylistInfoItem;
|
|
||||||
import org.schabi.newpipe.extractor.stream.StreamInfoItem;
|
|
||||||
import org.schabi.newpipe.info_list.holder.ChannelInfoItemHolder;
|
|
||||||
import org.schabi.newpipe.info_list.holder.ChannelMiniInfoItemHolder;
|
|
||||||
import org.schabi.newpipe.info_list.holder.CommentInfoItemHolder;
|
|
||||||
import org.schabi.newpipe.info_list.holder.InfoItemHolder;
|
|
||||||
import org.schabi.newpipe.info_list.holder.PlaylistInfoItemHolder;
|
|
||||||
import org.schabi.newpipe.info_list.holder.PlaylistMiniInfoItemHolder;
|
|
||||||
import org.schabi.newpipe.info_list.holder.StreamInfoItemHolder;
|
|
||||||
import org.schabi.newpipe.info_list.holder.StreamMiniInfoItemHolder;
|
|
||||||
import org.schabi.newpipe.local.history.HistoryRecordManager;
|
|
||||||
import org.schabi.newpipe.util.OnClickGesture;
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Created by Christian Schabesberger on 26.09.16.
|
|
||||||
* <p>
|
|
||||||
* Copyright (C) Christian Schabesberger 2016 <chris.schabesberger@mailbox.org>
|
|
||||||
* InfoItemBuilder.java is part of NewPipe.
|
|
||||||
* </p>
|
|
||||||
* <p>
|
|
||||||
* NewPipe is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
* </p>
|
|
||||||
* <p>
|
|
||||||
* NewPipe is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
* </p>
|
|
||||||
* <p>
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with NewPipe. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
* </p>
|
|
||||||
*/
|
|
||||||
|
|
||||||
public class InfoItemBuilder {
|
|
||||||
private final Context context;
|
|
||||||
|
|
||||||
private OnClickGesture<StreamInfoItem> onStreamSelectedListener;
|
|
||||||
private OnClickGesture<ChannelInfoItem> onChannelSelectedListener;
|
|
||||||
private OnClickGesture<PlaylistInfoItem> onPlaylistSelectedListener;
|
|
||||||
private OnClickGesture<CommentsInfoItem> onCommentsSelectedListener;
|
|
||||||
|
|
||||||
public InfoItemBuilder(final Context context) {
|
|
||||||
this.context = context;
|
|
||||||
}
|
|
||||||
|
|
||||||
public View buildView(@NonNull final ViewGroup parent, @NonNull final InfoItem infoItem,
|
|
||||||
final HistoryRecordManager historyRecordManager) {
|
|
||||||
return buildView(parent, infoItem, historyRecordManager, false);
|
|
||||||
}
|
|
||||||
|
|
||||||
public View buildView(@NonNull final ViewGroup parent, @NonNull final InfoItem infoItem,
|
|
||||||
final HistoryRecordManager historyRecordManager,
|
|
||||||
final boolean useMiniVariant) {
|
|
||||||
final InfoItemHolder holder =
|
|
||||||
holderFromInfoType(parent, infoItem.getInfoType(), useMiniVariant);
|
|
||||||
holder.updateFromItem(infoItem, historyRecordManager);
|
|
||||||
return holder.itemView;
|
|
||||||
}
|
|
||||||
|
|
||||||
private InfoItemHolder holderFromInfoType(@NonNull final ViewGroup parent,
|
|
||||||
@NonNull final InfoItem.InfoType infoType,
|
|
||||||
final boolean useMiniVariant) {
|
|
||||||
switch (infoType) {
|
|
||||||
case STREAM:
|
|
||||||
return useMiniVariant ? new StreamMiniInfoItemHolder(this, parent)
|
|
||||||
: new StreamInfoItemHolder(this, parent);
|
|
||||||
case CHANNEL:
|
|
||||||
return useMiniVariant ? new ChannelMiniInfoItemHolder(this, parent)
|
|
||||||
: new ChannelInfoItemHolder(this, parent);
|
|
||||||
case PLAYLIST:
|
|
||||||
return useMiniVariant ? new PlaylistMiniInfoItemHolder(this, parent)
|
|
||||||
: new PlaylistInfoItemHolder(this, parent);
|
|
||||||
case COMMENT:
|
|
||||||
return new CommentInfoItemHolder(this, parent);
|
|
||||||
default:
|
|
||||||
throw new RuntimeException("InfoType not expected = " + infoType.name());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public Context getContext() {
|
|
||||||
return context;
|
|
||||||
}
|
|
||||||
|
|
||||||
public OnClickGesture<StreamInfoItem> getOnStreamSelectedListener() {
|
|
||||||
return onStreamSelectedListener;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setOnStreamSelectedListener(final OnClickGesture<StreamInfoItem> listener) {
|
|
||||||
this.onStreamSelectedListener = listener;
|
|
||||||
}
|
|
||||||
|
|
||||||
public OnClickGesture<ChannelInfoItem> getOnChannelSelectedListener() {
|
|
||||||
return onChannelSelectedListener;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setOnChannelSelectedListener(final OnClickGesture<ChannelInfoItem> listener) {
|
|
||||||
this.onChannelSelectedListener = listener;
|
|
||||||
}
|
|
||||||
|
|
||||||
public OnClickGesture<PlaylistInfoItem> getOnPlaylistSelectedListener() {
|
|
||||||
return onPlaylistSelectedListener;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setOnPlaylistSelectedListener(final OnClickGesture<PlaylistInfoItem> listener) {
|
|
||||||
this.onPlaylistSelectedListener = listener;
|
|
||||||
}
|
|
||||||
|
|
||||||
public OnClickGesture<CommentsInfoItem> getOnCommentsSelectedListener() {
|
|
||||||
return onCommentsSelectedListener;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setOnCommentsSelectedListener(
|
|
||||||
final OnClickGesture<CommentsInfoItem> onCommentsSelectedListener) {
|
|
||||||
this.onCommentsSelectedListener = onCommentsSelectedListener;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: 2016-2026 NewPipe contributors <https://newpipe.net>
|
||||||
|
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.schabi.newpipe.info_list
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import org.schabi.newpipe.extractor.channel.ChannelInfoItem
|
||||||
|
import org.schabi.newpipe.extractor.comments.CommentsInfoItem
|
||||||
|
import org.schabi.newpipe.extractor.playlist.PlaylistInfoItem
|
||||||
|
import org.schabi.newpipe.extractor.stream.StreamInfoItem
|
||||||
|
import org.schabi.newpipe.util.OnClickGesture
|
||||||
|
|
||||||
|
class InfoItemBuilder(val context: Context) {
|
||||||
|
var onStreamSelectedListener: OnClickGesture<StreamInfoItem>? = null
|
||||||
|
var onChannelSelectedListener: OnClickGesture<ChannelInfoItem>? = null
|
||||||
|
var onPlaylistSelectedListener: OnClickGesture<PlaylistInfoItem>? = null
|
||||||
|
var onCommentsSelectedListener: OnClickGesture<CommentsInfoItem>? = null
|
||||||
|
}
|
||||||
@@ -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.
|
||||||
|
|||||||
@@ -1,19 +1,18 @@
|
|||||||
package org.schabi.newpipe.info_list
|
package org.schabi.newpipe.info_list
|
||||||
|
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.widget.ImageView
|
import com.xwray.groupie.viewbinding.BindableItem
|
||||||
import android.widget.TextView
|
import com.xwray.groupie.viewbinding.GroupieViewHolder
|
||||||
import com.xwray.groupie.GroupieViewHolder
|
|
||||||
import com.xwray.groupie.Item
|
|
||||||
import org.schabi.newpipe.R
|
import org.schabi.newpipe.R
|
||||||
|
import org.schabi.newpipe.databinding.ItemStreamSegmentBinding
|
||||||
import org.schabi.newpipe.extractor.stream.StreamSegment
|
import org.schabi.newpipe.extractor.stream.StreamSegment
|
||||||
import org.schabi.newpipe.util.Localization
|
import org.schabi.newpipe.util.Localization
|
||||||
import org.schabi.newpipe.util.image.PicassoHelper
|
import org.schabi.newpipe.util.image.CoilHelper
|
||||||
|
|
||||||
class StreamSegmentItem(
|
class StreamSegmentItem(
|
||||||
private val item: StreamSegment,
|
private val item: StreamSegment,
|
||||||
private val onClick: StreamSegmentAdapter.StreamSegmentListener
|
private val onClick: StreamSegmentAdapter.StreamSegmentListener
|
||||||
) : Item<GroupieViewHolder>() {
|
) : BindableItem<ItemStreamSegmentBinding>() {
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
const val PAYLOAD_SELECT = 1
|
const val PAYLOAD_SELECT = 1
|
||||||
@@ -21,31 +20,35 @@ class StreamSegmentItem(
|
|||||||
|
|
||||||
var isSelected = false
|
var isSelected = false
|
||||||
|
|
||||||
override fun bind(viewHolder: GroupieViewHolder, position: Int) {
|
override fun bind(viewBinding: ItemStreamSegmentBinding, position: Int) {
|
||||||
item.previewUrl?.let {
|
CoilHelper.loadThumbnail(viewBinding.previewImage, item.previewUrl)
|
||||||
PicassoHelper.loadThumbnail(it)
|
viewBinding.textViewTitle.text = item.title
|
||||||
.into(viewHolder.root.findViewById<ImageView>(R.id.previewImage))
|
|
||||||
}
|
|
||||||
viewHolder.root.findViewById<TextView>(R.id.textViewTitle).text = item.title
|
|
||||||
if (item.channelName == null) {
|
if (item.channelName == null) {
|
||||||
viewHolder.root.findViewById<TextView>(R.id.textViewChannel).visibility = View.GONE
|
viewBinding.textViewChannel.visibility = View.GONE
|
||||||
// When the channel name is displayed there is less space
|
// When the channel name is displayed there is less space
|
||||||
// and thus the segment title needs to be only one line height.
|
// and thus the segment title needs to be only one line height.
|
||||||
// But when there is no channel name displayed, the title can be two lines long.
|
// But when there is no channel name displayed, the title can be two lines long.
|
||||||
// The default maxLines value is set to 1 to display all elements in the AS preview,
|
// The default maxLines value is set to 1 to display all elements in the AS preview,
|
||||||
viewHolder.root.findViewById<TextView>(R.id.textViewTitle).maxLines = 2
|
viewBinding.textViewTitle.maxLines = 2
|
||||||
} else {
|
} else {
|
||||||
viewHolder.root.findViewById<TextView>(R.id.textViewChannel).text = item.channelName
|
viewBinding.textViewChannel.text = item.channelName
|
||||||
viewHolder.root.findViewById<TextView>(R.id.textViewChannel).visibility = View.VISIBLE
|
viewBinding.textViewChannel.visibility = View.VISIBLE
|
||||||
}
|
}
|
||||||
viewHolder.root.findViewById<TextView>(R.id.textViewStartSeconds).text =
|
viewBinding.textViewStartSeconds.text =
|
||||||
Localization.getDurationString(item.startTimeSeconds.toLong())
|
Localization.getDurationString(item.startTimeSeconds.toLong())
|
||||||
viewHolder.root.setOnClickListener { onClick.onItemClick(this, item.startTimeSeconds) }
|
viewBinding.root.setOnClickListener { onClick.onItemClick(this, item.startTimeSeconds) }
|
||||||
viewHolder.root.setOnLongClickListener { onClick.onItemLongClick(this, item.startTimeSeconds); true }
|
viewBinding.root.setOnLongClickListener {
|
||||||
viewHolder.root.isSelected = isSelected
|
onClick.onItemLongClick(this, item.startTimeSeconds)
|
||||||
|
true
|
||||||
|
}
|
||||||
|
viewBinding.root.isSelected = isSelected
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun bind(viewHolder: GroupieViewHolder, position: Int, payloads: MutableList<Any>) {
|
override fun bind(
|
||||||
|
viewHolder: GroupieViewHolder<ItemStreamSegmentBinding>,
|
||||||
|
position: Int,
|
||||||
|
payloads: MutableList<Any>
|
||||||
|
) {
|
||||||
if (payloads.contains(PAYLOAD_SELECT)) {
|
if (payloads.contains(PAYLOAD_SELECT)) {
|
||||||
viewHolder.root.isSelected = isSelected
|
viewHolder.root.isSelected = isSelected
|
||||||
return
|
return
|
||||||
@@ -54,4 +57,6 @@ class StreamSegmentItem(
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun getLayout() = R.layout.item_stream_segment
|
override fun getLayout() = R.layout.item_stream_segment
|
||||||
|
|
||||||
|
override fun initializeViewBinding(view: View) = ItemStreamSegmentBinding.bind(view)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -346,7 +346,7 @@ public final class InfoItemDialog {
|
|||||||
|
|
||||||
public static void reportErrorDuringInitialization(final Throwable throwable,
|
public static void reportErrorDuringInitialization(final Throwable throwable,
|
||||||
final InfoItem item) {
|
final InfoItem item) {
|
||||||
ErrorUtil.showSnackbar(App.getApp().getBaseContext(), new ErrorInfo(
|
ErrorUtil.showSnackbar(App.getInstance().getBaseContext(), new ErrorInfo(
|
||||||
throwable,
|
throwable,
|
||||||
UserAction.OPEN_INFO_ITEM_DIALOG,
|
UserAction.OPEN_INFO_ITEM_DIALOG,
|
||||||
"none",
|
"none",
|
||||||
|
|||||||
@@ -13,8 +13,8 @@ import org.schabi.newpipe.extractor.channel.ChannelInfoItem;
|
|||||||
import org.schabi.newpipe.extractor.utils.Utils;
|
import org.schabi.newpipe.extractor.utils.Utils;
|
||||||
import org.schabi.newpipe.info_list.InfoItemBuilder;
|
import org.schabi.newpipe.info_list.InfoItemBuilder;
|
||||||
import org.schabi.newpipe.local.history.HistoryRecordManager;
|
import org.schabi.newpipe.local.history.HistoryRecordManager;
|
||||||
import org.schabi.newpipe.util.image.PicassoHelper;
|
|
||||||
import org.schabi.newpipe.util.Localization;
|
import org.schabi.newpipe.util.Localization;
|
||||||
|
import org.schabi.newpipe.util.image.CoilHelper;
|
||||||
|
|
||||||
public class ChannelMiniInfoItemHolder extends InfoItemHolder {
|
public class ChannelMiniInfoItemHolder extends InfoItemHolder {
|
||||||
private final ImageView itemThumbnailView;
|
private final ImageView itemThumbnailView;
|
||||||
@@ -56,7 +56,7 @@ public class ChannelMiniInfoItemHolder extends InfoItemHolder {
|
|||||||
itemAdditionalDetailView.setText(getDetailLine(item));
|
itemAdditionalDetailView.setText(getDetailLine(item));
|
||||||
}
|
}
|
||||||
|
|
||||||
PicassoHelper.loadAvatar(item.getThumbnails()).into(itemThumbnailView);
|
CoilHelper.INSTANCE.loadAvatar(itemThumbnailView, item.getThumbnails());
|
||||||
|
|
||||||
itemView.setOnClickListener(view -> {
|
itemView.setOnClickListener(view -> {
|
||||||
if (itemBuilder.getOnChannelSelectedListener() != null) {
|
if (itemBuilder.getOnChannelSelectedListener() != null) {
|
||||||
|
|||||||
@@ -27,8 +27,8 @@ import org.schabi.newpipe.util.DeviceUtils;
|
|||||||
import org.schabi.newpipe.util.Localization;
|
import org.schabi.newpipe.util.Localization;
|
||||||
import org.schabi.newpipe.util.NavigationHelper;
|
import org.schabi.newpipe.util.NavigationHelper;
|
||||||
import org.schabi.newpipe.util.external_communication.ShareUtils;
|
import org.schabi.newpipe.util.external_communication.ShareUtils;
|
||||||
|
import org.schabi.newpipe.util.image.CoilHelper;
|
||||||
import org.schabi.newpipe.util.image.ImageStrategy;
|
import org.schabi.newpipe.util.image.ImageStrategy;
|
||||||
import org.schabi.newpipe.util.image.PicassoHelper;
|
|
||||||
import org.schabi.newpipe.util.text.TextEllipsizer;
|
import org.schabi.newpipe.util.text.TextEllipsizer;
|
||||||
|
|
||||||
public class CommentInfoItemHolder extends InfoItemHolder {
|
public class CommentInfoItemHolder extends InfoItemHolder {
|
||||||
@@ -82,14 +82,12 @@ public class CommentInfoItemHolder extends InfoItemHolder {
|
|||||||
@Override
|
@Override
|
||||||
public void updateFromItem(final InfoItem infoItem,
|
public void updateFromItem(final InfoItem infoItem,
|
||||||
final HistoryRecordManager historyRecordManager) {
|
final HistoryRecordManager historyRecordManager) {
|
||||||
if (!(infoItem instanceof CommentsInfoItem)) {
|
if (!(infoItem instanceof CommentsInfoItem item)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
final CommentsInfoItem item = (CommentsInfoItem) infoItem;
|
|
||||||
|
|
||||||
|
|
||||||
// load the author avatar
|
// load the author avatar
|
||||||
PicassoHelper.loadAvatar(item.getUploaderAvatars()).into(itemThumbnailView);
|
CoilHelper.INSTANCE.loadAvatar(itemThumbnailView, item.getUploaderAvatars());
|
||||||
if (ImageStrategy.shouldLoadImages()) {
|
if (ImageStrategy.shouldLoadImages()) {
|
||||||
itemThumbnailView.setVisibility(View.VISIBLE);
|
itemThumbnailView.setVisibility(View.VISIBLE);
|
||||||
itemRoot.setPadding(commentVerticalPadding, commentVerticalPadding,
|
itemRoot.setPadding(commentVerticalPadding, commentVerticalPadding,
|
||||||
|
|||||||
@@ -9,8 +9,8 @@ import org.schabi.newpipe.extractor.InfoItem;
|
|||||||
import org.schabi.newpipe.extractor.playlist.PlaylistInfoItem;
|
import org.schabi.newpipe.extractor.playlist.PlaylistInfoItem;
|
||||||
import org.schabi.newpipe.info_list.InfoItemBuilder;
|
import org.schabi.newpipe.info_list.InfoItemBuilder;
|
||||||
import org.schabi.newpipe.local.history.HistoryRecordManager;
|
import org.schabi.newpipe.local.history.HistoryRecordManager;
|
||||||
import org.schabi.newpipe.util.image.PicassoHelper;
|
|
||||||
import org.schabi.newpipe.util.Localization;
|
import org.schabi.newpipe.util.Localization;
|
||||||
|
import org.schabi.newpipe.util.image.CoilHelper;
|
||||||
|
|
||||||
public class PlaylistMiniInfoItemHolder extends InfoItemHolder {
|
public class PlaylistMiniInfoItemHolder extends InfoItemHolder {
|
||||||
public final ImageView itemThumbnailView;
|
public final ImageView itemThumbnailView;
|
||||||
@@ -46,7 +46,7 @@ public class PlaylistMiniInfoItemHolder extends InfoItemHolder {
|
|||||||
.localizeStreamCountMini(itemStreamCountView.getContext(), item.getStreamCount()));
|
.localizeStreamCountMini(itemStreamCountView.getContext(), item.getStreamCount()));
|
||||||
itemUploaderView.setText(item.getUploaderName());
|
itemUploaderView.setText(item.getUploaderName());
|
||||||
|
|
||||||
PicassoHelper.loadPlaylistThumbnail(item.getThumbnails()).into(itemThumbnailView);
|
CoilHelper.INSTANCE.loadPlaylistThumbnail(itemThumbnailView, item.getThumbnails());
|
||||||
|
|
||||||
itemView.setOnClickListener(view -> {
|
itemView.setOnClickListener(view -> {
|
||||||
if (itemBuilder.getOnPlaylistSelectedListener() != null) {
|
if (itemBuilder.getOnPlaylistSelectedListener() != null) {
|
||||||
|
|||||||
@@ -16,8 +16,8 @@ import org.schabi.newpipe.ktx.ViewUtils;
|
|||||||
import org.schabi.newpipe.local.history.HistoryRecordManager;
|
import org.schabi.newpipe.local.history.HistoryRecordManager;
|
||||||
import org.schabi.newpipe.util.DependentPreferenceHelper;
|
import org.schabi.newpipe.util.DependentPreferenceHelper;
|
||||||
import org.schabi.newpipe.util.Localization;
|
import org.schabi.newpipe.util.Localization;
|
||||||
import org.schabi.newpipe.util.image.PicassoHelper;
|
|
||||||
import org.schabi.newpipe.util.StreamTypeUtil;
|
import org.schabi.newpipe.util.StreamTypeUtil;
|
||||||
|
import org.schabi.newpipe.util.image.CoilHelper;
|
||||||
import org.schabi.newpipe.views.AnimatedProgressBar;
|
import org.schabi.newpipe.views.AnimatedProgressBar;
|
||||||
|
|
||||||
import java.util.concurrent.TimeUnit;
|
import java.util.concurrent.TimeUnit;
|
||||||
@@ -87,7 +87,7 @@ public class StreamMiniInfoItemHolder extends InfoItemHolder {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Default thumbnail is shown on error, while loading and if the url is empty
|
// Default thumbnail is shown on error, while loading and if the url is empty
|
||||||
PicassoHelper.loadThumbnail(item.getThumbnails()).into(itemThumbnailView);
|
CoilHelper.INSTANCE.loadThumbnail(itemThumbnailView, item.getThumbnails());
|
||||||
|
|
||||||
itemView.setOnClickListener(view -> {
|
itemView.setOnClickListener(view -> {
|
||||||
if (itemBuilder.getOnStreamSelectedListener() != null) {
|
if (itemBuilder.getOnStreamSelectedListener() != null) {
|
||||||
|
|||||||
13
app/src/main/java/org/schabi/newpipe/ktx/Bitmap.kt
Normal file
13
app/src/main/java/org/schabi/newpipe/ktx/Bitmap.kt
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
package org.schabi.newpipe.ktx
|
||||||
|
|
||||||
|
import android.graphics.Bitmap
|
||||||
|
import android.graphics.Rect
|
||||||
|
import androidx.core.graphics.BitmapCompat
|
||||||
|
|
||||||
|
@Suppress("NOTHING_TO_INLINE")
|
||||||
|
inline fun Bitmap.scale(
|
||||||
|
width: Int,
|
||||||
|
height: Int,
|
||||||
|
srcRect: Rect? = null,
|
||||||
|
scaleInLinearSpace: Boolean = true
|
||||||
|
) = BitmapCompat.createScaledBitmap(this, width, height, srcRect, scaleInLinearSpace)
|
||||||
@@ -41,14 +41,16 @@ fun View.animate(
|
|||||||
execOnEnd: Runnable? = null
|
execOnEnd: Runnable? = null
|
||||||
) {
|
) {
|
||||||
if (DEBUG) {
|
if (DEBUG) {
|
||||||
val id = try {
|
val id = runCatching { resources.getResourceEntryName(id) }.getOrDefault(id.toString())
|
||||||
resources.getResourceEntryName(id)
|
|
||||||
} catch (e: Exception) {
|
|
||||||
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")
|
||||||
}
|
}
|
||||||
@@ -291,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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,6 +28,8 @@ import static org.schabi.newpipe.ktx.ViewUtils.animate;
|
|||||||
import static org.schabi.newpipe.ktx.ViewUtils.animateHideRecyclerViewAllowingScrolling;
|
import static org.schabi.newpipe.ktx.ViewUtils.animateHideRecyclerViewAllowingScrolling;
|
||||||
import static org.schabi.newpipe.util.ThemeHelper.getItemViewMode;
|
import static org.schabi.newpipe.util.ThemeHelper.getItemViewMode;
|
||||||
|
|
||||||
|
import java.util.function.Supplier;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This fragment is design to be used with persistent data such as
|
* This fragment is design to be used with persistent data such as
|
||||||
* {@link org.schabi.newpipe.database.LocalItem}, and does not cache the data contained
|
* {@link org.schabi.newpipe.database.LocalItem}, and does not cache the data contained
|
||||||
@@ -100,7 +102,7 @@ public abstract class BaseLocalListFragment<I, N> extends BaseStateFragment<I>
|
|||||||
//////////////////////////////////////////////////////////////////////////*/
|
//////////////////////////////////////////////////////////////////////////*/
|
||||||
|
|
||||||
@Nullable
|
@Nullable
|
||||||
protected ViewBinding getListHeader() {
|
protected Supplier<View> getListHeaderSupplier() {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -131,9 +133,9 @@ public abstract class BaseLocalListFragment<I, N> extends BaseStateFragment<I>
|
|||||||
itemsList = rootView.findViewById(R.id.items_list);
|
itemsList = rootView.findViewById(R.id.items_list);
|
||||||
refreshItemViewMode();
|
refreshItemViewMode();
|
||||||
|
|
||||||
headerRootBinding = getListHeader();
|
final Supplier<View> listHeaderSupplier = getListHeaderSupplier();
|
||||||
if (headerRootBinding != null) {
|
if (listHeaderSupplier != null) {
|
||||||
itemListAdapter.setHeader(headerRootBinding.getRoot());
|
itemListAdapter.setHeaderSupplier(listHeaderSupplier);
|
||||||
}
|
}
|
||||||
footerRootBinding = getListFooter();
|
footerRootBinding = getListFooter();
|
||||||
itemListAdapter.setFooter(footerRootBinding.getRoot());
|
itemListAdapter.setFooter(footerRootBinding.getRoot());
|
||||||
@@ -210,6 +212,8 @@ public abstract class BaseLocalListFragment<I, N> extends BaseStateFragment<I>
|
|||||||
showListFooter(false);
|
showListFooter(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Deprecated(since = "Calling this method with `true` may cause crashes, see "
|
||||||
|
+ "https://github.com/TeamNewPipe/NewPipe/pull/12996#pullrequestreview-3713317115")
|
||||||
@Override
|
@Override
|
||||||
public void showListFooter(final boolean show) {
|
public void showListFooter(final boolean show) {
|
||||||
if (itemsList == null) {
|
if (itemsList == null) {
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ import java.time.format.DateTimeFormatter;
|
|||||||
import java.time.format.FormatStyle;
|
import java.time.format.FormatStyle;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.function.Supplier;
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Created by Christian Schabesberger on 01.08.16.
|
* Created by Christian Schabesberger on 01.08.16.
|
||||||
@@ -88,7 +89,7 @@ public class LocalItemListAdapter extends RecyclerView.Adapter<RecyclerView.View
|
|||||||
private final DateTimeFormatter dateTimeFormatter;
|
private final DateTimeFormatter dateTimeFormatter;
|
||||||
|
|
||||||
private boolean showFooter = false;
|
private boolean showFooter = false;
|
||||||
private View header = null;
|
private Supplier<View> headerSupplier = null;
|
||||||
private View footer = null;
|
private View footer = null;
|
||||||
private ItemViewMode itemViewMode = ItemViewMode.LIST;
|
private ItemViewMode itemViewMode = ItemViewMode.LIST;
|
||||||
private boolean useItemHandle = false;
|
private boolean useItemHandle = false;
|
||||||
@@ -97,6 +98,7 @@ public class LocalItemListAdapter extends RecyclerView.Adapter<RecyclerView.View
|
|||||||
recordManager = new HistoryRecordManager(context);
|
recordManager = new HistoryRecordManager(context);
|
||||||
localItemBuilder = new LocalItemBuilder(context);
|
localItemBuilder = new LocalItemBuilder(context);
|
||||||
localItems = new ArrayList<>();
|
localItems = new ArrayList<>();
|
||||||
|
|
||||||
dateTimeFormatter = DateTimeFormatter.ofLocalizedDate(FormatStyle.SHORT)
|
dateTimeFormatter = DateTimeFormatter.ofLocalizedDate(FormatStyle.SHORT)
|
||||||
.withLocale(Localization.getPreferredLocale(context));
|
.withLocale(Localization.getPreferredLocale(context));
|
||||||
}
|
}
|
||||||
@@ -124,7 +126,7 @@ public class LocalItemListAdapter extends RecyclerView.Adapter<RecyclerView.View
|
|||||||
if (DEBUG) {
|
if (DEBUG) {
|
||||||
Log.d(TAG, "addItems() after > offsetStart = " + offsetStart + ", "
|
Log.d(TAG, "addItems() after > offsetStart = " + offsetStart + ", "
|
||||||
+ "localItems.size() = " + localItems.size() + ", "
|
+ "localItems.size() = " + localItems.size() + ", "
|
||||||
+ "header = " + header + ", footer = " + footer + ", "
|
+ "header = " + hasHeader() + ", footer = " + footer + ", "
|
||||||
+ "showFooter = " + showFooter);
|
+ "showFooter = " + showFooter);
|
||||||
}
|
}
|
||||||
notifyItemRangeInserted(offsetStart, data.size());
|
notifyItemRangeInserted(offsetStart, data.size());
|
||||||
@@ -144,7 +146,7 @@ public class LocalItemListAdapter extends RecyclerView.Adapter<RecyclerView.View
|
|||||||
final int index = localItems.indexOf(data);
|
final int index = localItems.indexOf(data);
|
||||||
if (index != -1) {
|
if (index != -1) {
|
||||||
localItems.remove(index);
|
localItems.remove(index);
|
||||||
notifyItemRemoved(index + (header != null ? 1 : 0));
|
notifyItemRemoved(index + (hasHeader() ? 1 : 0));
|
||||||
} else {
|
} else {
|
||||||
// this happens when
|
// this happens when
|
||||||
// 1) removeItem is called on infoItemDuplicate as in showStreamItemDialog of
|
// 1) removeItem is called on infoItemDuplicate as in showStreamItemDialog of
|
||||||
@@ -189,9 +191,9 @@ public class LocalItemListAdapter extends RecyclerView.Adapter<RecyclerView.View
|
|||||||
this.useItemHandle = useItemHandle;
|
this.useItemHandle = useItemHandle;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setHeader(final View header) {
|
public void setHeaderSupplier(@Nullable final Supplier<View> headerSupplier) {
|
||||||
final boolean changed = header != this.header;
|
final boolean changed = headerSupplier != this.headerSupplier;
|
||||||
this.header = header;
|
this.headerSupplier = headerSupplier;
|
||||||
if (changed) {
|
if (changed) {
|
||||||
notifyDataSetChanged();
|
notifyDataSetChanged();
|
||||||
}
|
}
|
||||||
@@ -201,6 +203,12 @@ public class LocalItemListAdapter extends RecyclerView.Adapter<RecyclerView.View
|
|||||||
this.footer = view;
|
this.footer = view;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected boolean hasHeader() {
|
||||||
|
return this.headerSupplier != null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Deprecated(since = "Calling this method with `true` may cause crashes, see "
|
||||||
|
+ "https://github.com/TeamNewPipe/NewPipe/pull/12996#pullrequestreview-3713317115")
|
||||||
public void showFooter(final boolean show) {
|
public void showFooter(final boolean show) {
|
||||||
if (DEBUG) {
|
if (DEBUG) {
|
||||||
Log.d(TAG, "showFooter() called with: show = [" + show + "]");
|
Log.d(TAG, "showFooter() called with: show = [" + show + "]");
|
||||||
@@ -211,6 +219,8 @@ public class LocalItemListAdapter extends RecyclerView.Adapter<RecyclerView.View
|
|||||||
|
|
||||||
showFooter = show;
|
showFooter = show;
|
||||||
if (show) {
|
if (show) {
|
||||||
|
Log.w(TAG, "Calling LocalItemListAdapter.showFooter(true) may cause crashes, see https"
|
||||||
|
+ "://github.com/TeamNewPipe/NewPipe/pull/12996#pullrequestreview-3713317115");
|
||||||
notifyItemInserted(sizeConsideringHeader());
|
notifyItemInserted(sizeConsideringHeader());
|
||||||
} else {
|
} else {
|
||||||
notifyItemRemoved(sizeConsideringHeader());
|
notifyItemRemoved(sizeConsideringHeader());
|
||||||
@@ -218,11 +228,11 @@ public class LocalItemListAdapter extends RecyclerView.Adapter<RecyclerView.View
|
|||||||
}
|
}
|
||||||
|
|
||||||
private int adapterOffsetWithoutHeader(final int offset) {
|
private int adapterOffsetWithoutHeader(final int offset) {
|
||||||
return offset - (header != null ? 1 : 0);
|
return offset - (hasHeader() ? 1 : 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
private int sizeConsideringHeader() {
|
private int sizeConsideringHeader() {
|
||||||
return localItems.size() + (header != null ? 1 : 0);
|
return localItems.size() + (hasHeader() ? 1 : 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
public ArrayList<LocalItem> getItemsList() {
|
public ArrayList<LocalItem> getItemsList() {
|
||||||
@@ -232,7 +242,7 @@ public class LocalItemListAdapter extends RecyclerView.Adapter<RecyclerView.View
|
|||||||
@Override
|
@Override
|
||||||
public int getItemCount() {
|
public int getItemCount() {
|
||||||
int count = localItems.size();
|
int count = localItems.size();
|
||||||
if (header != null) {
|
if (hasHeader()) {
|
||||||
count++;
|
count++;
|
||||||
}
|
}
|
||||||
if (footer != null && showFooter) {
|
if (footer != null && showFooter) {
|
||||||
@@ -242,7 +252,7 @@ public class LocalItemListAdapter extends RecyclerView.Adapter<RecyclerView.View
|
|||||||
if (DEBUG) {
|
if (DEBUG) {
|
||||||
Log.d(TAG, "getItemCount() called, count = " + count + ", "
|
Log.d(TAG, "getItemCount() called, count = " + count + ", "
|
||||||
+ "localItems.size() = " + localItems.size() + ", "
|
+ "localItems.size() = " + localItems.size() + ", "
|
||||||
+ "header = " + header + ", footer = " + footer + ", "
|
+ "header = " + hasHeader() + ", footer = " + footer + ", "
|
||||||
+ "showFooter = " + showFooter);
|
+ "showFooter = " + showFooter);
|
||||||
}
|
}
|
||||||
return count;
|
return count;
|
||||||
@@ -255,9 +265,9 @@ public class LocalItemListAdapter extends RecyclerView.Adapter<RecyclerView.View
|
|||||||
Log.d(TAG, "getItemViewType() called with: position = [" + position + "]");
|
Log.d(TAG, "getItemViewType() called with: position = [" + position + "]");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (header != null && position == 0) {
|
if (hasHeader() && position == 0) {
|
||||||
return HEADER_TYPE;
|
return HEADER_TYPE;
|
||||||
} else if (header != null) {
|
} else if (hasHeader()) {
|
||||||
position--;
|
position--;
|
||||||
}
|
}
|
||||||
if (footer != null && position == localItems.size() && showFooter) {
|
if (footer != null && position == localItems.size() && showFooter) {
|
||||||
@@ -318,7 +328,7 @@ public class LocalItemListAdapter extends RecyclerView.Adapter<RecyclerView.View
|
|||||||
}
|
}
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case HEADER_TYPE:
|
case HEADER_TYPE:
|
||||||
return new HeaderFooterHolder(header);
|
return new HeaderFooterHolder(headerSupplier.get());
|
||||||
case FOOTER_TYPE:
|
case FOOTER_TYPE:
|
||||||
return new HeaderFooterHolder(footer);
|
return new HeaderFooterHolder(footer);
|
||||||
case LOCAL_PLAYLIST_HOLDER_TYPE:
|
case LOCAL_PLAYLIST_HOLDER_TYPE:
|
||||||
@@ -366,14 +376,14 @@ public class LocalItemListAdapter extends RecyclerView.Adapter<RecyclerView.View
|
|||||||
|
|
||||||
if (holder instanceof LocalItemHolder) {
|
if (holder instanceof LocalItemHolder) {
|
||||||
// If header isn't null, offset the items by -1
|
// If header isn't null, offset the items by -1
|
||||||
if (header != null) {
|
if (hasHeader()) {
|
||||||
position--;
|
position--;
|
||||||
}
|
}
|
||||||
|
|
||||||
((LocalItemHolder) holder)
|
((LocalItemHolder) holder)
|
||||||
.updateFromItem(localItems.get(position), recordManager, dateTimeFormatter);
|
.updateFromItem(localItems.get(position), recordManager, dateTimeFormatter);
|
||||||
} else if (holder instanceof HeaderFooterHolder && position == 0 && header != null) {
|
} else if (holder instanceof HeaderFooterHolder && position == 0 && hasHeader()) {
|
||||||
((HeaderFooterHolder) holder).view = header;
|
((HeaderFooterHolder) holder).view = headerSupplier.get();
|
||||||
} else if (holder instanceof HeaderFooterHolder && position == sizeConsideringHeader()
|
} else if (holder instanceof HeaderFooterHolder && position == sizeConsideringHeader()
|
||||||
&& footer != null && showFooter) {
|
&& footer != null && showFooter) {
|
||||||
((HeaderFooterHolder) holder).view = footer;
|
((HeaderFooterHolder) holder).view = footer;
|
||||||
@@ -387,10 +397,10 @@ public class LocalItemListAdapter extends RecyclerView.Adapter<RecyclerView.View
|
|||||||
for (final Object payload : payloads) {
|
for (final Object payload : payloads) {
|
||||||
if (payload instanceof StreamStateEntity) {
|
if (payload instanceof StreamStateEntity) {
|
||||||
((LocalItemHolder) holder).updateState(localItems
|
((LocalItemHolder) holder).updateState(localItems
|
||||||
.get(header == null ? position : position - 1), recordManager);
|
.get(hasHeader() ? position - 1 : position), recordManager);
|
||||||
} else if (payload instanceof Boolean) {
|
} else if (payload instanceof Boolean) {
|
||||||
((LocalItemHolder) holder).updateState(localItems
|
((LocalItemHolder) holder).updateState(localItems
|
||||||
.get(header == null ? position : position - 1), recordManager);
|
.get(hasHeader() ? position - 1 : position), recordManager);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -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)
|
||||||
@@ -85,14 +85,13 @@ class FeedDatabaseManager(context: Context) {
|
|||||||
items: List<StreamInfoItem>,
|
items: List<StreamInfoItem>,
|
||||||
oldestAllowedDate: OffsetDateTime = FEED_OLDEST_ALLOWED_DATE
|
oldestAllowedDate: OffsetDateTime = FEED_OLDEST_ALLOWED_DATE
|
||||||
) {
|
) {
|
||||||
val itemsToInsert = ArrayList<StreamInfoItem>()
|
val itemsToInsert = items.mapNotNull { stream ->
|
||||||
loop@ for (streamItem in items) {
|
val uploadDate = stream.uploadDate
|
||||||
val uploadDate = streamItem.uploadDate
|
|
||||||
|
|
||||||
itemsToInsert += when {
|
when {
|
||||||
uploadDate == null && streamItem.streamType == StreamType.LIVE_STREAM -> streamItem
|
uploadDate == null && stream.streamType == StreamType.LIVE_STREAM -> stream
|
||||||
uploadDate != null && uploadDate.offsetDateTime() >= oldestAllowedDate -> streamItem
|
uploadDate != null && uploadDate.offsetDateTime() >= oldestAllowedDate -> stream
|
||||||
else -> continue@loop
|
else -> null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
@@ -253,7 +255,7 @@ class FeedFragment : BaseStateFragment<FeedState>() {
|
|||||||
viewModel.getShowFutureItemsFromPreferences()
|
viewModel.getShowFutureItemsFromPreferences()
|
||||||
)
|
)
|
||||||
|
|
||||||
AlertDialog.Builder(context!!)
|
AlertDialog.Builder(requireContext())
|
||||||
.setTitle(R.string.feed_hide_streams_title)
|
.setTitle(R.string.feed_hide_streams_title)
|
||||||
.setMultiChoiceItems(dialogItems, checkedDialogItems) { _, which, isChecked ->
|
.setMultiChoiceItems(dialogItems, checkedDialogItems) { _, which, isChecked ->
|
||||||
checkedDialogItems[which] = isChecked
|
checkedDialogItems[which] = isChecked
|
||||||
@@ -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)
|
||||||
}
|
}
|
||||||
@@ -122,8 +129,7 @@ class FeedViewModel(
|
|||||||
fun setSaveShowPlayedItems(showPlayedItems: Boolean) {
|
fun setSaveShowPlayedItems(showPlayedItems: Boolean) {
|
||||||
this.showPlayedItems.onNext(showPlayedItems)
|
this.showPlayedItems.onNext(showPlayedItems)
|
||||||
PreferenceManager.getDefaultSharedPreferences(application).edit {
|
PreferenceManager.getDefaultSharedPreferences(application).edit {
|
||||||
this.putBoolean(application.getString(R.string.feed_show_watched_items_key), showPlayedItems)
|
putBoolean(application.getString(R.string.feed_show_watched_items_key), showPlayedItems)
|
||||||
this.apply()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -132,8 +138,7 @@ class FeedViewModel(
|
|||||||
fun setSaveShowPartiallyPlayedItems(showPartiallyPlayedItems: Boolean) {
|
fun setSaveShowPartiallyPlayedItems(showPartiallyPlayedItems: Boolean) {
|
||||||
this.showPartiallyPlayedItems.onNext(showPartiallyPlayedItems)
|
this.showPartiallyPlayedItems.onNext(showPartiallyPlayedItems)
|
||||||
PreferenceManager.getDefaultSharedPreferences(application).edit {
|
PreferenceManager.getDefaultSharedPreferences(application).edit {
|
||||||
this.putBoolean(application.getString(R.string.feed_show_partially_watched_items_key), showPartiallyPlayedItems)
|
putBoolean(application.getString(R.string.feed_show_partially_watched_items_key), showPartiallyPlayedItems)
|
||||||
this.apply()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -142,30 +147,26 @@ class FeedViewModel(
|
|||||||
fun setSaveShowFutureItems(showFutureItems: Boolean) {
|
fun setSaveShowFutureItems(showFutureItems: Boolean) {
|
||||||
this.showFutureItems.onNext(showFutureItems)
|
this.showFutureItems.onNext(showFutureItems)
|
||||||
PreferenceManager.getDefaultSharedPreferences(application).edit {
|
PreferenceManager.getDefaultSharedPreferences(application).edit {
|
||||||
this.putBoolean(application.getString(R.string.feed_show_future_items_key), showFutureItems)
|
putBoolean(application.getString(R.string.feed_show_future_items_key), showFutureItems)
|
||||||
this.apply()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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 {
|
||||||
FeedViewModel(
|
FeedViewModel(
|
||||||
App.getApp(),
|
App.instance,
|
||||||
groupId,
|
groupId,
|
||||||
// Read initial value from preferences
|
// Read initial value from preferences
|
||||||
getShowPlayedItemsFromPreferences(context.applicationContext),
|
getShowPlayedItemsFromPreferences(context.applicationContext),
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -19,9 +21,7 @@ import org.schabi.newpipe.extractor.stream.StreamType.POST_LIVE_STREAM
|
|||||||
import org.schabi.newpipe.extractor.stream.StreamType.VIDEO_STREAM
|
import org.schabi.newpipe.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.CoilHelper
|
||||||
import java.util.concurrent.TimeUnit
|
|
||||||
import java.util.function.Consumer
|
|
||||||
|
|
||||||
data class StreamItem(
|
data class StreamItem(
|
||||||
val streamWithState: StreamWithState,
|
val streamWithState: StreamWithState,
|
||||||
@@ -101,7 +101,7 @@ data class StreamItem(
|
|||||||
viewBinding.itemProgressView.visibility = View.GONE
|
viewBinding.itemProgressView.visibility = View.GONE
|
||||||
}
|
}
|
||||||
|
|
||||||
PicassoHelper.loadThumbnail(stream.thumbnailUrl).into(viewBinding.itemThumbnailView)
|
CoilHelper.loadThumbnail(viewBinding.itemThumbnailView, stream.thumbnailUrl)
|
||||||
|
|
||||||
if (itemVersion != ItemVersion.MINI) {
|
if (itemVersion != ItemVersion.MINI) {
|
||||||
viewBinding.itemAdditionalDetails.text =
|
viewBinding.itemAdditionalDetails.text =
|
||||||
@@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ import android.app.PendingIntent
|
|||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.graphics.Bitmap
|
import android.graphics.Bitmap
|
||||||
import android.graphics.drawable.Drawable
|
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.provider.Settings
|
import android.provider.Settings
|
||||||
@@ -15,21 +14,19 @@ import androidx.core.app.NotificationManagerCompat
|
|||||||
import androidx.core.app.PendingIntentCompat
|
import androidx.core.app.PendingIntentCompat
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
import androidx.core.content.getSystemService
|
import androidx.core.content.getSystemService
|
||||||
|
import androidx.core.net.toUri
|
||||||
import androidx.preference.PreferenceManager
|
import androidx.preference.PreferenceManager
|
||||||
import com.squareup.picasso.Picasso
|
|
||||||
import com.squareup.picasso.Target
|
|
||||||
import org.schabi.newpipe.R
|
import org.schabi.newpipe.R
|
||||||
import org.schabi.newpipe.extractor.stream.StreamInfoItem
|
import org.schabi.newpipe.extractor.stream.StreamInfoItem
|
||||||
import org.schabi.newpipe.local.feed.service.FeedUpdateInfo
|
import org.schabi.newpipe.local.feed.service.FeedUpdateInfo
|
||||||
import org.schabi.newpipe.util.NavigationHelper
|
import org.schabi.newpipe.util.NavigationHelper
|
||||||
import org.schabi.newpipe.util.image.PicassoHelper
|
import org.schabi.newpipe.util.image.CoilHelper
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Helper for everything related to show notifications about new streams to the user.
|
* Helper for everything related to show notifications about new streams to the user.
|
||||||
*/
|
*/
|
||||||
class NotificationHelper(val context: Context) {
|
class NotificationHelper(val context: Context) {
|
||||||
private val manager = NotificationManagerCompat.from(context)
|
private val manager = NotificationManagerCompat.from(context)
|
||||||
private val iconLoadingTargets = ArrayList<Target>()
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Show notifications for new streams from a single channel. The individual notifications are
|
* Show notifications for new streams from a single channel. The individual notifications are
|
||||||
@@ -41,7 +38,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,
|
||||||
@@ -68,69 +67,42 @@ class NotificationHelper(val context: Context) {
|
|||||||
summaryBuilder.setStyle(style)
|
summaryBuilder.setStyle(style)
|
||||||
|
|
||||||
// open the channel page when clicking on the summary notification
|
// open the channel page when clicking on the summary notification
|
||||||
|
val intent = NavigationHelper
|
||||||
|
.getChannelIntent(context, data.serviceId, data.url)
|
||||||
|
.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||||
summaryBuilder.setContentIntent(
|
summaryBuilder.setContentIntent(
|
||||||
PendingIntentCompat.getActivity(
|
PendingIntentCompat.getActivity(context, data.pseudoId, intent, 0, false)
|
||||||
context,
|
|
||||||
data.pseudoId,
|
|
||||||
NavigationHelper
|
|
||||||
.getChannelIntent(context, data.serviceId, data.url)
|
|
||||||
.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK),
|
|
||||||
0,
|
|
||||||
false
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// a Target is like a listener for image loading events
|
val avatarIcon =
|
||||||
val target = object : Target {
|
CoilHelper.loadBitmapBlocking(context, data.avatarUrl, R.drawable.ic_newpipe_triangle_white)
|
||||||
override fun onBitmapLoaded(bitmap: Bitmap, from: Picasso.LoadedFrom) {
|
summaryBuilder.setLargeIcon(avatarIcon)
|
||||||
// set channel icon only if there is actually one (for Android versions < 7.0)
|
|
||||||
summaryBuilder.setLargeIcon(bitmap)
|
|
||||||
|
|
||||||
// Show individual stream notifications, set channel icon only if there is actually
|
// Show individual stream notifications, set channel icon only if there is actually one
|
||||||
// one
|
showStreamNotifications(newStreams, data.serviceId, avatarIcon)
|
||||||
showStreamNotifications(newStreams, data.serviceId, data.url, bitmap)
|
// Show summary notification
|
||||||
// Show summary notification
|
if (manager.areNotificationsEnabled()) {
|
||||||
manager.notify(data.pseudoId, summaryBuilder.build())
|
manager.notify(data.pseudoId, summaryBuilder.build())
|
||||||
|
|
||||||
iconLoadingTargets.remove(this) // allow it to be garbage-collected
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onBitmapFailed(e: Exception, errorDrawable: Drawable) {
|
|
||||||
// Show individual stream notifications
|
|
||||||
showStreamNotifications(newStreams, data.serviceId, data.url, null)
|
|
||||||
// Show summary notification
|
|
||||||
manager.notify(data.pseudoId, summaryBuilder.build())
|
|
||||||
iconLoadingTargets.remove(this) // allow it to be garbage-collected
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onPrepareLoad(placeHolderDrawable: Drawable) {
|
|
||||||
// Nothing to do
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// add the target to the list to hold a strong reference and prevent it from being garbage
|
|
||||||
// collected, since Picasso only holds weak references to targets
|
|
||||||
iconLoadingTargets.add(target)
|
|
||||||
|
|
||||||
PicassoHelper.loadNotificationIcon(data.avatarUrl).into(target)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun showStreamNotifications(
|
private fun showStreamNotifications(
|
||||||
newStreams: List<StreamInfoItem>,
|
newStreams: List<StreamInfoItem>,
|
||||||
serviceId: Int,
|
serviceId: Int,
|
||||||
channelUrl: String,
|
|
||||||
channelIcon: Bitmap?
|
channelIcon: Bitmap?
|
||||||
) {
|
) {
|
||||||
for (stream in newStreams) {
|
if (manager.areNotificationsEnabled()) {
|
||||||
val notification = createStreamNotification(stream, serviceId, channelUrl, channelIcon)
|
newStreams.forEach { stream ->
|
||||||
manager.notify(stream.url.hashCode(), notification)
|
val notification =
|
||||||
|
createStreamNotification(stream, serviceId, channelIcon)
|
||||||
|
manager.notify(stream.url.hashCode(), notification)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun createStreamNotification(
|
private fun createStreamNotification(
|
||||||
item: StreamInfoItem,
|
item: StreamInfoItem,
|
||||||
serviceId: Int,
|
serviceId: Int,
|
||||||
channelUrl: String,
|
|
||||||
channelIcon: Bitmap?
|
channelIcon: Bitmap?
|
||||||
): Notification {
|
): Notification {
|
||||||
return NotificationCompat.Builder(
|
return NotificationCompat.Builder(
|
||||||
@@ -141,7 +113,7 @@ class NotificationHelper(val context: Context) {
|
|||||||
.setLargeIcon(channelIcon)
|
.setLargeIcon(channelIcon)
|
||||||
.setContentTitle(item.name)
|
.setContentTitle(item.name)
|
||||||
.setContentText(item.uploaderName)
|
.setContentText(item.uploaderName)
|
||||||
.setGroup(channelUrl)
|
.setGroup(item.uploaderUrl)
|
||||||
.setColor(ContextCompat.getColor(context, R.color.ic_launcher_background))
|
.setColor(ContextCompat.getColor(context, R.color.ic_launcher_background))
|
||||||
.setColorized(true)
|
.setColorized(true)
|
||||||
.setAutoCancel(true)
|
.setAutoCancel(true)
|
||||||
@@ -181,8 +153,7 @@ class NotificationHelper(val context: Context) {
|
|||||||
val manager = context.getSystemService<NotificationManager>()!!
|
val manager = context.getSystemService<NotificationManager>()!!
|
||||||
val enabled = manager.areNotificationsEnabled()
|
val enabled = manager.areNotificationsEnabled()
|
||||||
val channel = manager.getNotificationChannel(channelId)
|
val channel = manager.getNotificationChannel(channelId)
|
||||||
val importance = channel?.importance
|
enabled && channel?.importance != NotificationManager.IMPORTANCE_NONE
|
||||||
enabled && channel != null && importance != NotificationManager.IMPORTANCE_NONE
|
|
||||||
} else {
|
} else {
|
||||||
NotificationManagerCompat.from(context).areNotificationsEnabled()
|
NotificationManagerCompat.from(context).areNotificationsEnabled()
|
||||||
}
|
}
|
||||||
@@ -212,7 +183,7 @@ class NotificationHelper(val context: Context) {
|
|||||||
context.startActivity(intent)
|
context.startActivity(intent)
|
||||||
} else {
|
} else {
|
||||||
val intent = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS)
|
val intent = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS)
|
||||||
intent.data = Uri.parse("package:" + context.packageName)
|
intent.data = "package:${context.packageName}".toUri()
|
||||||
context.startActivity(intent)
|
context.startActivity(intent)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,8 +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 org.schabi.newpipe.R
|
|
||||||
import java.util.concurrent.TimeUnit
|
import java.util.concurrent.TimeUnit
|
||||||
|
import org.schabi.newpipe.R
|
||||||
|
import org.schabi.newpipe.ktx.getStringSafe
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Information for the Scheduler which checks for new streams.
|
* Information for the Scheduler which checks for new streams.
|
||||||
@@ -20,11 +21,9 @@ data class ScheduleOptions(
|
|||||||
val preferences = PreferenceManager.getDefaultSharedPreferences(context)
|
val preferences = PreferenceManager.getDefaultSharedPreferences(context)
|
||||||
return ScheduleOptions(
|
return ScheduleOptions(
|
||||||
interval = TimeUnit.SECONDS.toMillis(
|
interval = TimeUnit.SECONDS.toMillis(
|
||||||
preferences.getString(
|
preferences.getStringSafe(
|
||||||
context.getString(R.string.streams_notifications_interval_key),
|
context.getString(R.string.streams_notifications_interval_key),
|
||||||
null
|
context.getString(R.string.streams_notifications_interval_default)
|
||||||
)?.toLongOrNull() ?: context.getString(
|
|
||||||
R.string.streams_notifications_interval_default
|
|
||||||
).toLong()
|
).toLong()
|
||||||
),
|
),
|
||||||
isRequireNonMeteredNetwork = preferences.getString(
|
isRequireNonMeteredNetwork = preferences.getString(
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -108,7 +111,8 @@ class FeedLoadManager(private val context: Context) {
|
|||||||
broadcastProgress()
|
broadcastProgress()
|
||||||
}
|
}
|
||||||
.observeOn(Schedulers.io())
|
.observeOn(Schedulers.io())
|
||||||
.flatMap { Flowable.fromIterable(it) }
|
// Randomize user subscription ordering to attempt to resist fingerprinting
|
||||||
|
.flatMap { Flowable.fromIterable(it.shuffled()) }
|
||||||
.takeWhile { !cancelSignal.get() }
|
.takeWhile { !cancelSignal.get() }
|
||||||
.doOnNext { subscriptionEntity ->
|
.doOnNext { subscriptionEntity ->
|
||||||
// throttle YouTube extractions once every BATCH_SIZE to avoid being rate limited
|
// throttle YouTube extractions once every BATCH_SIZE to avoid being rate limited
|
||||||
@@ -186,7 +190,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 +221,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 +240,7 @@ class FeedLoadManager(private val context: Context) {
|
|||||||
subscriptionEntity,
|
subscriptionEntity,
|
||||||
originalInfo!!,
|
originalInfo!!,
|
||||||
streams!!,
|
streams!!,
|
||||||
errors,
|
errors
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
} catch (e: Throwable) {
|
} catch (e: Throwable) {
|
||||||
@@ -305,6 +311,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)
|
||||||
@@ -184,7 +185,9 @@ class FeedLoadService : Service() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
notificationManager.notify(NOTIFICATION_ID, notificationBuilder.build())
|
if (notificationManager.areNotificationsEnabled()) {
|
||||||
|
notificationManager.notify(NOTIFICATION_ID, notificationBuilder.build())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// /////////////////////////////////////////////////////////////////////////
|
// /////////////////////////////////////////////////////////////////////////
|
||||||
|
|||||||
@@ -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
|
||||||
)
|
)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -13,7 +13,6 @@ import android.widget.Toast;
|
|||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
import androidx.viewbinding.ViewBinding;
|
|
||||||
|
|
||||||
import com.evernote.android.state.State;
|
import com.evernote.android.state.State;
|
||||||
import com.google.android.material.snackbar.Snackbar;
|
import com.google.android.material.snackbar.Snackbar;
|
||||||
@@ -45,6 +44,7 @@ import java.util.Collections;
|
|||||||
import java.util.Comparator;
|
import java.util.Comparator;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
|
import java.util.function.Supplier;
|
||||||
|
|
||||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
|
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
|
||||||
import io.reactivex.rxjava3.disposables.CompositeDisposable;
|
import io.reactivex.rxjava3.disposables.CompositeDisposable;
|
||||||
@@ -126,12 +126,12 @@ public class StatisticsPlaylistFragment
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected ViewBinding getListHeader() {
|
protected Supplier<View> getListHeaderSupplier() {
|
||||||
headerBinding = StatisticPlaylistControlBinding.inflate(activity.getLayoutInflater(),
|
headerBinding = StatisticPlaylistControlBinding.inflate(activity.getLayoutInflater(),
|
||||||
itemsList, false);
|
itemsList, false);
|
||||||
playlistControlBinding = headerBinding.playlistControl;
|
playlistControlBinding = headerBinding.playlistControl;
|
||||||
|
|
||||||
return headerBinding;
|
return headerBinding::getRoot;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|||||||
@@ -8,8 +8,8 @@ import org.schabi.newpipe.database.playlist.PlaylistDuplicatesEntry;
|
|||||||
import org.schabi.newpipe.database.playlist.PlaylistMetadataEntry;
|
import org.schabi.newpipe.database.playlist.PlaylistMetadataEntry;
|
||||||
import org.schabi.newpipe.local.LocalItemBuilder;
|
import org.schabi.newpipe.local.LocalItemBuilder;
|
||||||
import org.schabi.newpipe.local.history.HistoryRecordManager;
|
import org.schabi.newpipe.local.history.HistoryRecordManager;
|
||||||
import org.schabi.newpipe.util.image.PicassoHelper;
|
|
||||||
import org.schabi.newpipe.util.Localization;
|
import org.schabi.newpipe.util.Localization;
|
||||||
|
import org.schabi.newpipe.util.image.CoilHelper;
|
||||||
|
|
||||||
import java.time.format.DateTimeFormatter;
|
import java.time.format.DateTimeFormatter;
|
||||||
|
|
||||||
@@ -30,17 +30,16 @@ public class LocalPlaylistItemHolder extends PlaylistItemHolder {
|
|||||||
public void updateFromItem(final LocalItem localItem,
|
public void updateFromItem(final LocalItem localItem,
|
||||||
final HistoryRecordManager historyRecordManager,
|
final HistoryRecordManager historyRecordManager,
|
||||||
final DateTimeFormatter dateTimeFormatter) {
|
final DateTimeFormatter dateTimeFormatter) {
|
||||||
if (!(localItem instanceof PlaylistMetadataEntry)) {
|
if (!(localItem instanceof PlaylistMetadataEntry item)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
final PlaylistMetadataEntry item = (PlaylistMetadataEntry) localItem;
|
|
||||||
|
|
||||||
itemTitleView.setText(item.getOrderingName());
|
itemTitleView.setText(item.getOrderingName());
|
||||||
itemStreamCountView.setText(Localization.localizeStreamCountMini(
|
itemStreamCountView.setText(Localization.localizeStreamCountMini(
|
||||||
itemStreamCountView.getContext(), item.getStreamCount()));
|
itemStreamCountView.getContext(), item.getStreamCount()));
|
||||||
itemUploaderView.setVisibility(View.INVISIBLE);
|
itemUploaderView.setVisibility(View.INVISIBLE);
|
||||||
|
|
||||||
PicassoHelper.loadPlaylistThumbnail(item.getThumbnailUrl()).into(itemThumbnailView);
|
CoilHelper.INSTANCE.loadPlaylistThumbnail(itemThumbnailView, item.getThumbnailUrl());
|
||||||
|
|
||||||
if (item instanceof PlaylistDuplicatesEntry
|
if (item instanceof PlaylistDuplicatesEntry
|
||||||
&& ((PlaylistDuplicatesEntry) item).getTimesStreamIsContained() > 0) {
|
&& ((PlaylistDuplicatesEntry) item).getTimesStreamIsContained() > 0) {
|
||||||
|
|||||||
@@ -16,8 +16,8 @@ import org.schabi.newpipe.local.LocalItemBuilder;
|
|||||||
import org.schabi.newpipe.local.history.HistoryRecordManager;
|
import org.schabi.newpipe.local.history.HistoryRecordManager;
|
||||||
import org.schabi.newpipe.util.DependentPreferenceHelper;
|
import org.schabi.newpipe.util.DependentPreferenceHelper;
|
||||||
import org.schabi.newpipe.util.Localization;
|
import org.schabi.newpipe.util.Localization;
|
||||||
import org.schabi.newpipe.util.image.PicassoHelper;
|
|
||||||
import org.schabi.newpipe.util.ServiceHelper;
|
import org.schabi.newpipe.util.ServiceHelper;
|
||||||
|
import org.schabi.newpipe.util.image.CoilHelper;
|
||||||
import org.schabi.newpipe.views.AnimatedProgressBar;
|
import org.schabi.newpipe.views.AnimatedProgressBar;
|
||||||
|
|
||||||
import java.time.format.DateTimeFormatter;
|
import java.time.format.DateTimeFormatter;
|
||||||
@@ -83,8 +83,8 @@ public class LocalPlaylistStreamItemHolder extends LocalItemHolder {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Default thumbnail is shown on error, while loading and if the url is empty
|
// Default thumbnail is shown on error, while loading and if the url is empty
|
||||||
PicassoHelper.loadThumbnail(item.getStreamEntity().getThumbnailUrl())
|
CoilHelper.INSTANCE.loadThumbnail(itemThumbnailView,
|
||||||
.into(itemThumbnailView);
|
item.getStreamEntity().getThumbnailUrl());
|
||||||
|
|
||||||
itemView.setOnClickListener(view -> {
|
itemView.setOnClickListener(view -> {
|
||||||
if (itemBuilder.getOnItemSelectedListener() != null) {
|
if (itemBuilder.getOnItemSelectedListener() != null) {
|
||||||
|
|||||||
@@ -16,8 +16,8 @@ import org.schabi.newpipe.local.LocalItemBuilder;
|
|||||||
import org.schabi.newpipe.local.history.HistoryRecordManager;
|
import org.schabi.newpipe.local.history.HistoryRecordManager;
|
||||||
import org.schabi.newpipe.util.DependentPreferenceHelper;
|
import org.schabi.newpipe.util.DependentPreferenceHelper;
|
||||||
import org.schabi.newpipe.util.Localization;
|
import org.schabi.newpipe.util.Localization;
|
||||||
import org.schabi.newpipe.util.image.PicassoHelper;
|
|
||||||
import org.schabi.newpipe.util.ServiceHelper;
|
import org.schabi.newpipe.util.ServiceHelper;
|
||||||
|
import org.schabi.newpipe.util.image.CoilHelper;
|
||||||
import org.schabi.newpipe.views.AnimatedProgressBar;
|
import org.schabi.newpipe.views.AnimatedProgressBar;
|
||||||
|
|
||||||
import java.time.format.DateTimeFormatter;
|
import java.time.format.DateTimeFormatter;
|
||||||
@@ -117,8 +117,8 @@ public class LocalStatisticStreamItemHolder extends LocalItemHolder {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Default thumbnail is shown on error, while loading and if the url is empty
|
// Default thumbnail is shown on error, while loading and if the url is empty
|
||||||
PicassoHelper.loadThumbnail(item.getStreamEntity().getThumbnailUrl())
|
CoilHelper.INSTANCE.loadThumbnail(itemThumbnailView,
|
||||||
.into(itemThumbnailView);
|
item.getStreamEntity().getThumbnailUrl());
|
||||||
|
|
||||||
itemView.setOnClickListener(view -> {
|
itemView.setOnClickListener(view -> {
|
||||||
if (itemBuilder.getOnItemSelectedListener() != null) {
|
if (itemBuilder.getOnItemSelectedListener() != null) {
|
||||||
|
|||||||
@@ -8,8 +8,8 @@ import org.schabi.newpipe.database.playlist.model.PlaylistRemoteEntity;
|
|||||||
import org.schabi.newpipe.local.LocalItemBuilder;
|
import org.schabi.newpipe.local.LocalItemBuilder;
|
||||||
import org.schabi.newpipe.local.history.HistoryRecordManager;
|
import org.schabi.newpipe.local.history.HistoryRecordManager;
|
||||||
import org.schabi.newpipe.util.Localization;
|
import org.schabi.newpipe.util.Localization;
|
||||||
import org.schabi.newpipe.util.image.PicassoHelper;
|
|
||||||
import org.schabi.newpipe.util.ServiceHelper;
|
import org.schabi.newpipe.util.ServiceHelper;
|
||||||
|
import org.schabi.newpipe.util.image.CoilHelper;
|
||||||
|
|
||||||
import java.time.format.DateTimeFormatter;
|
import java.time.format.DateTimeFormatter;
|
||||||
|
|
||||||
@@ -29,10 +29,9 @@ public class RemotePlaylistItemHolder extends PlaylistItemHolder {
|
|||||||
public void updateFromItem(final LocalItem localItem,
|
public void updateFromItem(final LocalItem localItem,
|
||||||
final HistoryRecordManager historyRecordManager,
|
final HistoryRecordManager historyRecordManager,
|
||||||
final DateTimeFormatter dateTimeFormatter) {
|
final DateTimeFormatter dateTimeFormatter) {
|
||||||
if (!(localItem instanceof PlaylistRemoteEntity)) {
|
if (!(localItem instanceof PlaylistRemoteEntity item)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
final PlaylistRemoteEntity item = (PlaylistRemoteEntity) localItem;
|
|
||||||
|
|
||||||
itemTitleView.setText(item.getOrderingName());
|
itemTitleView.setText(item.getOrderingName());
|
||||||
itemStreamCountView.setText(Localization.localizeStreamCountMini(
|
itemStreamCountView.setText(Localization.localizeStreamCountMini(
|
||||||
@@ -45,7 +44,7 @@ public class RemotePlaylistItemHolder extends PlaylistItemHolder {
|
|||||||
itemUploaderView.setText(ServiceHelper.getNameOfServiceById(item.getServiceId()));
|
itemUploaderView.setText(ServiceHelper.getNameOfServiceById(item.getServiceId()));
|
||||||
}
|
}
|
||||||
|
|
||||||
PicassoHelper.loadPlaylistThumbnail(item.getThumbnailUrl()).into(itemThumbnailView);
|
CoilHelper.INSTANCE.loadPlaylistThumbnail(itemThumbnailView, item.getThumbnailUrl());
|
||||||
|
|
||||||
super.updateFromItem(localItem, historyRecordManager, dateTimeFormatter);
|
super.updateFromItem(localItem, historyRecordManager, dateTimeFormatter);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 try { linkHandler.getId(url) } catch (e: ParsingException) { null }
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
package org.schabi.newpipe.local.playlist;
|
package org.schabi.newpipe.local.playlist;
|
||||||
|
|
||||||
|
import static android.view.ViewGroup.LayoutParams.MATCH_PARENT;
|
||||||
|
import static android.view.ViewGroup.LayoutParams.WRAP_CONTENT;
|
||||||
import static org.schabi.newpipe.error.ErrorUtil.showUiErrorSnackbar;
|
import static org.schabi.newpipe.error.ErrorUtil.showUiErrorSnackbar;
|
||||||
import static org.schabi.newpipe.ktx.ViewUtils.animate;
|
import static org.schabi.newpipe.ktx.ViewUtils.animate;
|
||||||
import static org.schabi.newpipe.local.playlist.ExportPlaylistKt.export;
|
import static org.schabi.newpipe.local.playlist.ExportPlaylistKt.export;
|
||||||
@@ -22,6 +24,8 @@ import android.view.MenuInflater;
|
|||||||
import android.view.MenuItem;
|
import android.view.MenuItem;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
import android.view.ViewGroup;
|
import android.view.ViewGroup;
|
||||||
|
import android.widget.LinearLayout;
|
||||||
|
import android.widget.LinearLayout.LayoutParams;
|
||||||
import android.widget.Toast;
|
import android.widget.Toast;
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
@@ -29,7 +33,6 @@ import androidx.annotation.Nullable;
|
|||||||
import androidx.appcompat.app.AlertDialog;
|
import androidx.appcompat.app.AlertDialog;
|
||||||
import androidx.recyclerview.widget.ItemTouchHelper;
|
import androidx.recyclerview.widget.ItemTouchHelper;
|
||||||
import androidx.recyclerview.widget.RecyclerView;
|
import androidx.recyclerview.widget.RecyclerView;
|
||||||
import androidx.viewbinding.ViewBinding;
|
|
||||||
|
|
||||||
import com.evernote.android.state.State;
|
import com.evernote.android.state.State;
|
||||||
import org.reactivestreams.Subscriber;
|
import org.reactivestreams.Subscriber;
|
||||||
@@ -55,6 +58,7 @@ import org.schabi.newpipe.local.BaseLocalListFragment;
|
|||||||
import org.schabi.newpipe.local.history.HistoryRecordManager;
|
import org.schabi.newpipe.local.history.HistoryRecordManager;
|
||||||
import org.schabi.newpipe.player.playqueue.PlayQueue;
|
import org.schabi.newpipe.player.playqueue.PlayQueue;
|
||||||
import org.schabi.newpipe.player.playqueue.SinglePlayQueue;
|
import org.schabi.newpipe.player.playqueue.SinglePlayQueue;
|
||||||
|
import org.schabi.newpipe.util.DeviceUtils;
|
||||||
import org.schabi.newpipe.util.Localization;
|
import org.schabi.newpipe.util.Localization;
|
||||||
import org.schabi.newpipe.util.NavigationHelper;
|
import org.schabi.newpipe.util.NavigationHelper;
|
||||||
import org.schabi.newpipe.util.OnClickGesture;
|
import org.schabi.newpipe.util.OnClickGesture;
|
||||||
@@ -67,6 +71,7 @@ import java.util.ArrayList;
|
|||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.concurrent.atomic.AtomicBoolean;
|
import java.util.concurrent.atomic.AtomicBoolean;
|
||||||
|
import java.util.function.Supplier;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
|
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
|
||||||
@@ -158,14 +163,14 @@ public class LocalPlaylistFragment extends BaseLocalListFragment<List<PlaylistSt
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected ViewBinding getListHeader() {
|
protected Supplier<View> getListHeaderSupplier() {
|
||||||
headerBinding = LocalPlaylistHeaderBinding.inflate(activity.getLayoutInflater(), itemsList,
|
headerBinding = LocalPlaylistHeaderBinding.inflate(activity.getLayoutInflater(), itemsList,
|
||||||
false);
|
false);
|
||||||
playlistControlBinding = headerBinding.playlistControl;
|
playlistControlBinding = headerBinding.playlistControl;
|
||||||
|
|
||||||
headerBinding.playlistTitleView.setSelected(true);
|
headerBinding.playlistTitleView.setSelected(true);
|
||||||
|
|
||||||
return headerBinding;
|
return headerBinding::getRoot;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -365,17 +370,7 @@ public class LocalPlaylistFragment extends BaseLocalListFragment<List<PlaylistSt
|
|||||||
createRenameDialog();
|
createRenameDialog();
|
||||||
} else if (item.getItemId() == R.id.menu_item_remove_watched) {
|
} else if (item.getItemId() == R.id.menu_item_remove_watched) {
|
||||||
if (!isRewritingPlaylist) {
|
if (!isRewritingPlaylist) {
|
||||||
new AlertDialog.Builder(requireContext())
|
openRemoveWatchedConfirmationDialog();
|
||||||
.setMessage(R.string.remove_watched_popup_warning)
|
|
||||||
.setTitle(R.string.remove_watched_popup_title)
|
|
||||||
.setPositiveButton(R.string.ok, (d, id) ->
|
|
||||||
removeWatchedStreams(false))
|
|
||||||
.setNeutralButton(
|
|
||||||
R.string.remove_watched_popup_yes_and_partially_watched_videos,
|
|
||||||
(d, id) -> removeWatchedStreams(true))
|
|
||||||
.setNegativeButton(R.string.cancel,
|
|
||||||
(d, id) -> d.cancel())
|
|
||||||
.show();
|
|
||||||
}
|
}
|
||||||
} else if (item.getItemId() == R.id.menu_item_remove_duplicates) {
|
} else if (item.getItemId() == R.id.menu_item_remove_duplicates) {
|
||||||
if (!isRewritingPlaylist) {
|
if (!isRewritingPlaylist) {
|
||||||
@@ -447,39 +442,28 @@ public class LocalPlaylistFragment extends BaseLocalListFragment<List<PlaylistSt
|
|||||||
.getIsPlaylistThumbnailPermanent(playlistId);
|
.getIsPlaylistThumbnailPermanent(playlistId);
|
||||||
boolean thumbnailVideoRemoved = false;
|
boolean thumbnailVideoRemoved = false;
|
||||||
|
|
||||||
if (removePartiallyWatched) {
|
final var streamStates = recordManager
|
||||||
for (final var playlistItem : playlist) {
|
.loadLocalStreamStateBatch(playlist).blockingGet();
|
||||||
final int indexInHistory = Collections.binarySearch(historyStreamIds,
|
|
||||||
playlistItem.getStreamId());
|
|
||||||
|
|
||||||
if (indexInHistory < 0) {
|
for (int i = 0; i < playlist.size(); i++) {
|
||||||
itemsToKeep.add(playlistItem);
|
final var playlistItem = playlist.get(i);
|
||||||
} else if (!isThumbnailPermanent && !thumbnailVideoRemoved
|
final var streamStateEntity = streamStates.get(i);
|
||||||
&& playlistManager.getPlaylistThumbnailStreamId(playlistId)
|
final int indexInHistory = Collections.binarySearch(historyStreamIds,
|
||||||
== playlistItem.getStreamEntity().getUid()) {
|
playlistItem.getStreamId());
|
||||||
thumbnailVideoRemoved = true;
|
final long duration = playlistItem.toStreamInfoItem().getDuration();
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
final var streamStates = recordManager
|
|
||||||
.loadLocalStreamStateBatch(playlist).blockingGet();
|
|
||||||
|
|
||||||
for (int i = 0; i < playlist.size(); i++) {
|
if (indexInHistory < 0 // stream is not in history
|
||||||
final var playlistItem = playlist.get(i);
|
// stream is in history but the streamStateEntity is null
|
||||||
final var streamStateEntity = streamStates.get(i);
|
// if the stream was played for less than 5 seconds, see
|
||||||
|
// StreamStateEntity#PLAYBACK_SAVE_THRESHOLD_START_MILLISECONDS
|
||||||
final int indexInHistory = Collections.binarySearch(historyStreamIds,
|
|| streamStateEntity == null
|
||||||
playlistItem.getStreamId());
|
|| (!removePartiallyWatched
|
||||||
final long duration = playlistItem.toStreamInfoItem().getDuration();
|
&& !streamStateEntity.isFinished(duration))) {
|
||||||
|
itemsToKeep.add(playlistItem);
|
||||||
if (indexInHistory < 0 || (streamStateEntity != null
|
} else if (!isThumbnailPermanent && !thumbnailVideoRemoved
|
||||||
&& !streamStateEntity.isFinished(duration))) {
|
&& playlistManager.getPlaylistThumbnailStreamId(playlistId)
|
||||||
itemsToKeep.add(playlistItem);
|
== playlistItem.getStreamEntity().getUid()) {
|
||||||
} else if (!isThumbnailPermanent && !thumbnailVideoRemoved
|
thumbnailVideoRemoved = true;
|
||||||
&& playlistManager.getPlaylistThumbnailStreamId(playlistId)
|
|
||||||
== playlistItem.getStreamEntity().getUid()) {
|
|
||||||
thumbnailVideoRemoved = true;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -904,6 +888,35 @@ public class LocalPlaylistFragment extends BaseLocalListFragment<List<PlaylistSt
|
|||||||
.show();
|
.show();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Opens a confirmation dialog to remove watched streams from the playlist.
|
||||||
|
* The user can also choose to remove partially watched streams.
|
||||||
|
*/
|
||||||
|
private void openRemoveWatchedConfirmationDialog() {
|
||||||
|
final android.widget.CheckBox removePartiallyWatchedCheckbox =
|
||||||
|
new android.widget.CheckBox(requireContext());
|
||||||
|
removePartiallyWatchedCheckbox.setText(
|
||||||
|
R.string.remove_watched_popup_partially_watched_streams);
|
||||||
|
|
||||||
|
// Wrap the checkbox in a container with dialog-like horizontal padding
|
||||||
|
// so it aligns with the dialog title and message on the start side.
|
||||||
|
final LinearLayout checkboxContainer = new LinearLayout(requireContext());
|
||||||
|
checkboxContainer.setOrientation(LinearLayout.VERTICAL);
|
||||||
|
final int padding = DeviceUtils.dpToPx(20, requireContext());
|
||||||
|
checkboxContainer.setPadding(padding, padding, padding, 0);
|
||||||
|
checkboxContainer.addView(removePartiallyWatchedCheckbox,
|
||||||
|
new LayoutParams(MATCH_PARENT, WRAP_CONTENT));
|
||||||
|
|
||||||
|
new AlertDialog.Builder(requireContext())
|
||||||
|
.setMessage(R.string.remove_watched_popup_warning)
|
||||||
|
.setTitle(R.string.remove_watched_popup_title)
|
||||||
|
.setView(checkboxContainer)
|
||||||
|
.setPositiveButton(R.string.yes, (d, id) ->
|
||||||
|
removeWatchedStreams(removePartiallyWatchedCheckbox.isChecked()))
|
||||||
|
.setNegativeButton(R.string.cancel, (d, id) -> d.cancel())
|
||||||
|
.show();
|
||||||
|
}
|
||||||
|
|
||||||
public void setTabsPagerAdapter(
|
public void setTabsPagerAdapter(
|
||||||
@Nullable final MainFragment.SelectedTabsPagerAdapter tabsPagerAdapter) {
|
@Nullable final MainFragment.SelectedTabsPagerAdapter tabsPagerAdapter) {
|
||||||
this.tabsPagerAdapter = tabsPagerAdapter;
|
this.tabsPagerAdapter = tabsPagerAdapter;
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
@@ -300,7 +327,7 @@ class FeedGroupDialog : DialogFragment(), BackPressable {
|
|||||||
groupIcon = feedGroupEntity?.icon
|
groupIcon = feedGroupEntity?.icon
|
||||||
groupSortOrder = feedGroupEntity?.sortOrder ?: -1
|
groupSortOrder = feedGroupEntity?.sortOrder ?: -1
|
||||||
|
|
||||||
val feedGroupIcon = if (selectedIcon == null) icon else selectedIcon!!
|
val feedGroupIcon = selectedIcon ?: icon
|
||||||
feedGroupCreateBinding.iconPreview.setImageResource(feedGroupIcon.getDrawableRes())
|
feedGroupCreateBinding.iconPreview.setImageResource(feedGroupIcon.getDrawableRes())
|
||||||
|
|
||||||
if (feedGroupCreateBinding.groupNameInput.text.isNullOrBlank()) {
|
if (feedGroupCreateBinding.groupNameInput.text.isNullOrBlank()) {
|
||||||
@@ -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
|
||||||
@@ -478,7 +506,7 @@ class FeedGroupDialog : DialogFragment(), BackPressable {
|
|||||||
private fun hideKeyboardSearch() {
|
private fun hideKeyboardSearch() {
|
||||||
inputMethodManager.hideSoftInputFromWindow(
|
inputMethodManager.hideSoftInputFromWindow(
|
||||||
searchLayoutBinding.toolbarSearchEditText.windowToken,
|
searchLayoutBinding.toolbarSearchEditText.windowToken,
|
||||||
InputMethodManager.RESULT_UNCHANGED_SHOWN
|
InputMethodManager.HIDE_NOT_ALWAYS
|
||||||
)
|
)
|
||||||
searchLayoutBinding.toolbarSearchEditText.clearFocus()
|
searchLayoutBinding.toolbarSearchEditText.clearFocus()
|
||||||
}
|
}
|
||||||
@@ -495,7 +523,7 @@ class FeedGroupDialog : DialogFragment(), BackPressable {
|
|||||||
private fun hideKeyboard() {
|
private fun hideKeyboard() {
|
||||||
inputMethodManager.hideSoftInputFromWindow(
|
inputMethodManager.hideSoftInputFromWindow(
|
||||||
feedGroupCreateBinding.groupNameInput.windowToken,
|
feedGroupCreateBinding.groupNameInput.windowToken,
|
||||||
InputMethodManager.RESULT_UNCHANGED_SHOWN
|
InputMethodManager.HIDE_NOT_ALWAYS
|
||||||
)
|
)
|
||||||
feedGroupCreateBinding.groupNameInput.clearFocus()
|
feedGroupCreateBinding.groupNameInput.clearFocus()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import org.schabi.newpipe.R
|
|||||||
import org.schabi.newpipe.extractor.channel.ChannelInfoItem
|
import org.schabi.newpipe.extractor.channel.ChannelInfoItem
|
||||||
import org.schabi.newpipe.util.Localization
|
import org.schabi.newpipe.util.Localization
|
||||||
import org.schabi.newpipe.util.OnClickGesture
|
import org.schabi.newpipe.util.OnClickGesture
|
||||||
import org.schabi.newpipe.util.image.PicassoHelper
|
import org.schabi.newpipe.util.image.CoilHelper
|
||||||
|
|
||||||
class ChannelItem(
|
class ChannelItem(
|
||||||
private val infoItem: ChannelInfoItem,
|
private val infoItem: ChannelInfoItem,
|
||||||
@@ -39,11 +39,14 @@ class ChannelItem(
|
|||||||
itemChannelDescriptionView.text = infoItem.description
|
itemChannelDescriptionView.text = infoItem.description
|
||||||
}
|
}
|
||||||
|
|
||||||
PicassoHelper.loadAvatar(infoItem.thumbnails).into(itemThumbnailView)
|
CoilHelper.loadAvatar(itemThumbnailView, infoItem.thumbnails)
|
||||||
|
|
||||||
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)
|
||||||
|
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import org.schabi.newpipe.database.subscription.SubscriptionEntity
|
|||||||
import org.schabi.newpipe.databinding.PickerSubscriptionItemBinding
|
import org.schabi.newpipe.databinding.PickerSubscriptionItemBinding
|
||||||
import org.schabi.newpipe.ktx.AnimationType
|
import org.schabi.newpipe.ktx.AnimationType
|
||||||
import org.schabi.newpipe.ktx.animate
|
import org.schabi.newpipe.ktx.animate
|
||||||
import org.schabi.newpipe.util.image.PicassoHelper
|
import org.schabi.newpipe.util.image.CoilHelper
|
||||||
|
|
||||||
data class PickerSubscriptionItem(
|
data class PickerSubscriptionItem(
|
||||||
val subscriptionEntity: SubscriptionEntity,
|
val subscriptionEntity: SubscriptionEntity,
|
||||||
@@ -21,7 +21,7 @@ data class PickerSubscriptionItem(
|
|||||||
override fun getSpanSize(spanCount: Int, position: Int): Int = 1
|
override fun getSpanSize(spanCount: Int, position: Int): Int = 1
|
||||||
|
|
||||||
override fun bind(viewBinding: PickerSubscriptionItemBinding, position: Int) {
|
override fun bind(viewBinding: PickerSubscriptionItemBinding, position: Int) {
|
||||||
PicassoHelper.loadAvatar(subscriptionEntity.avatarUrl).into(viewBinding.thumbnailView)
|
CoilHelper.loadAvatar(viewBinding.thumbnailView, subscriptionEntity.avatarUrl)
|
||||||
viewBinding.titleView.text = subscriptionEntity.name
|
viewBinding.titleView.text = subscriptionEntity.name
|
||||||
viewBinding.selectedHighlight.isVisible = isSelected
|
viewBinding.selectedHighlight.isVisible = isSelected
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -144,7 +144,9 @@ public abstract class BaseImportExportService extends Service {
|
|||||||
notificationBuilder.setContentText(text);
|
notificationBuilder.setContentText(text);
|
||||||
}
|
}
|
||||||
|
|
||||||
notificationManager.notify(getNotificationId(), notificationBuilder.build());
|
if (notificationManager.areNotificationsEnabled()) {
|
||||||
|
notificationManager.notify(getNotificationId(), notificationBuilder.build());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected void stopService() {
|
protected void stopService() {
|
||||||
@@ -174,7 +176,10 @@ public abstract class BaseImportExportService extends Service {
|
|||||||
.setContentTitle(title)
|
.setContentTitle(title)
|
||||||
.setStyle(new NotificationCompat.BigTextStyle().bigText(textOrEmpty))
|
.setStyle(new NotificationCompat.BigTextStyle().bigText(textOrEmpty))
|
||||||
.setContentText(textOrEmpty);
|
.setContentText(textOrEmpty);
|
||||||
notificationManager.notify(getNotificationId(), notificationBuilder.build());
|
|
||||||
|
if (notificationManager.areNotificationsEnabled()) {
|
||||||
|
notificationManager.notify(getNotificationId(), notificationBuilder.build());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected NotificationCompat.Builder createNotification() {
|
protected NotificationCompat.Builder createNotification() {
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user