Merge branch 'master' into sponsorblock

# Conflicts:
#	app/build.gradle
#	app/src/main/java/org/schabi/newpipe/CheckForNewAppVersion.java
#	app/src/main/java/org/schabi/newpipe/DownloaderImpl.java
#	app/src/main/java/org/schabi/newpipe/fragments/detail/VideoDetailFragment.java
#	app/src/main/java/org/schabi/newpipe/player/PlayQueueActivity.java
#	app/src/main/java/org/schabi/newpipe/settings/ContentSettingsFragment.java
#	app/src/main/res/values-so/strings.xml
#	app/src/main/res/values/strings.xml
#	fastlane/metadata/android/de/full_description.txt
This commit is contained in:
polymorphicshade 2021-09-26 11:24:42 -06:00
commit 9a5aab7e2d
356 changed files with 4025 additions and 6864 deletions

View file

@ -58,7 +58,7 @@ Oh no, a bug! It happens. Thanks for reporting an issue with NewPipe. To make it
<!-- Please fill this out when you do not provide a log generate by NewPipe --> <!-- Please fill this section if you did not provide a log generated by NewPipe -->
### Device info ### Device info

View file

@ -1,6 +1,7 @@
name: CI name: CI
on: on:
workflow_dispatch:
pull_request: pull_request:
branches: branches:
- dev - dev
@ -36,20 +37,11 @@ jobs:
uses: actions/setup-java@v2 uses: actions/setup-java@v2
with: with:
java-version: 8 java-version: 8
distribution: "adopt" distribution: "temurin"
cache: 'gradle'
- name: Cache Gradle dependencies
uses: actions/cache@v2
with:
path: ~/.gradle/caches
key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle') }}
restore-keys: ${{ runner.os }}-gradle
- name: Check if kotlin files are formatted correctly
run: ./gradlew runKtlint
- name: Build debug APK and run jvm tests - name: Build debug APK and run jvm tests
run: ./gradlew assembleDebug lintDebug testDebugUnitTest --stacktrace run: ./gradlew assembleDebug lintDebug testDebugUnitTest --stacktrace -DskipFormatKtlint
- name: Upload APK - name: Upload APK
uses: actions/upload-artifact@v2 uses: actions/upload-artifact@v2
@ -71,14 +63,8 @@ jobs:
uses: actions/setup-java@v2 uses: actions/setup-java@v2
with: with:
java-version: 8 java-version: 8
distribution: "adopt" distribution: "temurin"
cache: 'gradle'
- name: Cache Gradle dependencies
uses: actions/cache@v2
with:
path: ~/.gradle/caches
key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle') }}
restore-keys: ${{ runner.os }}-gradle
- name: Run android tests - name: Run android tests
uses: reactivecircus/android-emulator-runner@v2 uses: reactivecircus/android-emulator-runner@v2
@ -99,7 +85,8 @@ jobs:
# uses: actions/setup-java@v2 # uses: actions/setup-java@v2
# with: # with:
# java-version: 11 # Sonar requires JDK 11 # java-version: 11 # Sonar requires JDK 11
# distribution: "adopt" # distribution: "temurin"
# cache: 'gradle'
# - name: Cache SonarCloud packages # - name: Cache SonarCloud packages
# uses: actions/cache@v2 # uses: actions/cache@v2
@ -108,13 +95,6 @@ jobs:
# key: ${{ runner.os }}-sonar # key: ${{ runner.os }}-sonar
# restore-keys: ${{ runner.os }}-sonar # restore-keys: ${{ runner.os }}-sonar
# - name: Cache Gradle packages
# uses: actions/cache@v2
# with:
# path: ~/.gradle/caches
# key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle') }}
# restore-keys: ${{ runner.os }}-gradle
# - name: Build and analyze # - name: Build and analyze
# env: # env:
# GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # Needed to get PR information, if any # GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # Needed to get PR information, if any

20
.github/workflows/no-response.yml vendored Normal file
View file

@ -0,0 +1,20 @@
name: No Response
# Both `issue_comment` and `scheduled` event types are required for this Action
# to work properly.
on:
issue_comment:
types: [created]
schedule:
# Run daily at midnight.
- cron: '0 0 * * *'
jobs:
noResponse:
runs-on: ubuntu-latest
steps:
- uses: lee-dohm/no-response@v0.5.0
with:
token: ${{ github.token }}
daysUntilClose: 14
responseRequiredLabel: waiting-for-author

View file

@ -4,7 +4,7 @@ plugins {
apply plugin: 'com.android.application' apply plugin: 'com.android.application'
apply plugin: 'kotlin-android' apply plugin: 'kotlin-android'
apply plugin: 'kotlin-android-extensions' apply plugin: 'kotlin-parcelize'
apply plugin: 'kotlin-kapt' apply plugin: 'kotlin-kapt'
apply plugin: 'checkstyle' apply plugin: 'checkstyle'
@ -14,11 +14,11 @@ android {
defaultConfig { defaultConfig {
applicationId "org.polymorphicshade.newpipe" applicationId "org.polymorphicshade.newpipe"
resValue "string", "app_name", "NewPipe Sponsorblock" resValue "string", "app_name", "NewPipe SponsorBlock"
minSdkVersion 19 minSdkVersion 19
targetSdkVersion 29 targetSdkVersion 29
versionCode 975 versionCode 976
versionName "0.21.9_r2" versionName "0.21.10"
multiDexEnabled true multiDexEnabled true
@ -84,11 +84,6 @@ android {
jvmTarget = JavaVersion.VERSION_1_8 jvmTarget = JavaVersion.VERSION_1_8
} }
// Required and used only by groupie
androidExtensions {
experimental = true
}
sourceSets { sourceSets {
androidTest.assets.srcDirs += files("$projectDir/schemas".toString()) androidTest.assets.srcDirs += files("$projectDir/schemas".toString())
} }
@ -165,7 +160,10 @@ task formatKtlint(type: JavaExec) {
} }
afterEvaluate { afterEvaluate {
preDebugBuild.dependsOn formatKtlint, runCheckstyle, runKtlint if (!System.properties.containsKey('skipFormatKtlint')) {
preDebugBuild.dependsOn formatKtlint
}
preDebugBuild.dependsOn runCheckstyle, runKtlint
} }
sonarqube { sonarqube {
@ -186,7 +184,7 @@ dependencies {
// name and the commit hash with the commit hash of the (pushed) commit you want to test // name and the commit hash with the commit hash of the (pushed) commit you want to test
// This works thanks to JitPack: https://jitpack.io/ // This works thanks to JitPack: https://jitpack.io/
implementation 'com.github.TeamNewPipe:nanojson:1d9e1aea9049fc9f85e68b43ba39fe7be1c1f751' implementation 'com.github.TeamNewPipe:nanojson:1d9e1aea9049fc9f85e68b43ba39fe7be1c1f751'
implementation 'com.github.TeamNewPipe:NewPipeExtractor:v0.21.9' implementation 'com.github.TeamNewPipe:NewPipeExtractor:v0.21.10'
/** Checkstyle **/ /** Checkstyle **/
checkstyle "com.puppycrawl.tools:checkstyle:${checkstyleVersion}" checkstyle "com.puppycrawl.tools:checkstyle:${checkstyleVersion}"
@ -243,7 +241,8 @@ dependencies {
// Circular ImageView // Circular ImageView
implementation "de.hdodenhof:circleimageview:3.1.0" implementation "de.hdodenhof:circleimageview:3.1.0"
// Image loading // Image loading
implementation "com.nostra13.universalimageloader:universal-image-loader:1.9.5" //noinspection GradleDependency --> 2.8 is the last version, not 2.71828!
implementation "com.squareup.picasso:picasso:2.8"
// Markdown library for Android // Markdown library for Android
implementation "io.noties.markwon:core:${markwonVersion}" implementation "io.noties.markwon:core:${markwonVersion}"
@ -255,6 +254,9 @@ dependencies {
// Crash reporting // Crash reporting
implementation "ch.acra:acra-core:5.7.0" implementation "ch.acra:acra-core:5.7.0"
// Properly restarting
implementation 'com.jakewharton:process-phoenix:2.1.2'
// Reactive extensions for Java VM // Reactive extensions for Java VM
implementation "io.reactivex.rxjava3:rxjava:3.0.7" implementation "io.reactivex.rxjava3:rxjava:3.0.7"
implementation "io.reactivex.rxjava3:rxandroid:3.0.0" implementation "io.reactivex.rxjava3:rxandroid:3.0.0"

View file

@ -0,0 +1,713 @@
{
"formatVersion": 1,
"database": {
"version": 4,
"identityHash": "d8070091972a7011bce18aed62f80b90",
"entities": [
{
"tableName": "subscriptions",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `service_id` INTEGER NOT NULL, `url` TEXT, `name` TEXT, `avatar_url` TEXT, `subscriber_count` INTEGER, `description` TEXT)",
"fields": [
{
"fieldPath": "uid",
"columnName": "uid",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "serviceId",
"columnName": "service_id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "url",
"columnName": "url",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "name",
"columnName": "name",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "avatarUrl",
"columnName": "avatar_url",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "subscriberCount",
"columnName": "subscriber_count",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "description",
"columnName": "description",
"affinity": "TEXT",
"notNull": false
}
],
"primaryKey": {
"columnNames": [
"uid"
],
"autoGenerate": true
},
"indices": [
{
"name": "index_subscriptions_service_id_url",
"unique": true,
"columnNames": [
"service_id",
"url"
],
"createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_subscriptions_service_id_url` ON `${TABLE_NAME}` (`service_id`, `url`)"
}
],
"foreignKeys": []
},
{
"tableName": "search_history",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `creation_date` INTEGER, `service_id` INTEGER NOT NULL, `search` TEXT)",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "creationDate",
"columnName": "creation_date",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "serviceId",
"columnName": "service_id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "search",
"columnName": "search",
"affinity": "TEXT",
"notNull": false
}
],
"primaryKey": {
"columnNames": [
"id"
],
"autoGenerate": true
},
"indices": [
{
"name": "index_search_history_search",
"unique": false,
"columnNames": [
"search"
],
"createSql": "CREATE INDEX IF NOT EXISTS `index_search_history_search` ON `${TABLE_NAME}` (`search`)"
}
],
"foreignKeys": []
},
{
"tableName": "streams",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `service_id` INTEGER NOT NULL, `url` TEXT NOT NULL, `title` TEXT NOT NULL, `stream_type` TEXT NOT NULL, `duration` INTEGER NOT NULL, `uploader` TEXT NOT NULL, `uploader_url` TEXT, `thumbnail_url` TEXT, `view_count` INTEGER, `textual_upload_date` TEXT, `upload_date` INTEGER, `is_upload_date_approximation` INTEGER)",
"fields": [
{
"fieldPath": "uid",
"columnName": "uid",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "serviceId",
"columnName": "service_id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "url",
"columnName": "url",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "title",
"columnName": "title",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "streamType",
"columnName": "stream_type",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "duration",
"columnName": "duration",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "uploader",
"columnName": "uploader",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "uploaderUrl",
"columnName": "uploader_url",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "thumbnailUrl",
"columnName": "thumbnail_url",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "viewCount",
"columnName": "view_count",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "textualUploadDate",
"columnName": "textual_upload_date",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "uploadDate",
"columnName": "upload_date",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "isUploadDateApproximation",
"columnName": "is_upload_date_approximation",
"affinity": "INTEGER",
"notNull": false
}
],
"primaryKey": {
"columnNames": [
"uid"
],
"autoGenerate": true
},
"indices": [
{
"name": "index_streams_service_id_url",
"unique": true,
"columnNames": [
"service_id",
"url"
],
"createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_streams_service_id_url` ON `${TABLE_NAME}` (`service_id`, `url`)"
}
],
"foreignKeys": []
},
{
"tableName": "stream_history",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`stream_id` INTEGER NOT NULL, `access_date` INTEGER NOT NULL, `repeat_count` INTEGER NOT NULL, PRIMARY KEY(`stream_id`, `access_date`), FOREIGN KEY(`stream_id`) REFERENCES `streams`(`uid`) ON UPDATE CASCADE ON DELETE CASCADE )",
"fields": [
{
"fieldPath": "streamUid",
"columnName": "stream_id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "accessDate",
"columnName": "access_date",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "repeatCount",
"columnName": "repeat_count",
"affinity": "INTEGER",
"notNull": true
}
],
"primaryKey": {
"columnNames": [
"stream_id",
"access_date"
],
"autoGenerate": false
},
"indices": [
{
"name": "index_stream_history_stream_id",
"unique": false,
"columnNames": [
"stream_id"
],
"createSql": "CREATE INDEX IF NOT EXISTS `index_stream_history_stream_id` ON `${TABLE_NAME}` (`stream_id`)"
}
],
"foreignKeys": [
{
"table": "streams",
"onDelete": "CASCADE",
"onUpdate": "CASCADE",
"columns": [
"stream_id"
],
"referencedColumns": [
"uid"
]
}
]
},
{
"tableName": "stream_state",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`stream_id` INTEGER NOT NULL, `progress_time` INTEGER NOT NULL, PRIMARY KEY(`stream_id`), FOREIGN KEY(`stream_id`) REFERENCES `streams`(`uid`) ON UPDATE CASCADE ON DELETE CASCADE )",
"fields": [
{
"fieldPath": "streamUid",
"columnName": "stream_id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "progressMillis",
"columnName": "progress_time",
"affinity": "INTEGER",
"notNull": true
}
],
"primaryKey": {
"columnNames": [
"stream_id"
],
"autoGenerate": false
},
"indices": [],
"foreignKeys": [
{
"table": "streams",
"onDelete": "CASCADE",
"onUpdate": "CASCADE",
"columns": [
"stream_id"
],
"referencedColumns": [
"uid"
]
}
]
},
{
"tableName": "playlists",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT, `thumbnail_url` TEXT)",
"fields": [
{
"fieldPath": "uid",
"columnName": "uid",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "name",
"columnName": "name",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "thumbnailUrl",
"columnName": "thumbnail_url",
"affinity": "TEXT",
"notNull": false
}
],
"primaryKey": {
"columnNames": [
"uid"
],
"autoGenerate": true
},
"indices": [
{
"name": "index_playlists_name",
"unique": false,
"columnNames": [
"name"
],
"createSql": "CREATE INDEX IF NOT EXISTS `index_playlists_name` ON `${TABLE_NAME}` (`name`)"
}
],
"foreignKeys": []
},
{
"tableName": "playlist_stream_join",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`playlist_id` INTEGER NOT NULL, `stream_id` INTEGER NOT NULL, `join_index` INTEGER NOT NULL, PRIMARY KEY(`playlist_id`, `join_index`), FOREIGN KEY(`playlist_id`) REFERENCES `playlists`(`uid`) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED, FOREIGN KEY(`stream_id`) REFERENCES `streams`(`uid`) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED)",
"fields": [
{
"fieldPath": "playlistUid",
"columnName": "playlist_id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "streamUid",
"columnName": "stream_id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "index",
"columnName": "join_index",
"affinity": "INTEGER",
"notNull": true
}
],
"primaryKey": {
"columnNames": [
"playlist_id",
"join_index"
],
"autoGenerate": false
},
"indices": [
{
"name": "index_playlist_stream_join_playlist_id_join_index",
"unique": true,
"columnNames": [
"playlist_id",
"join_index"
],
"createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_playlist_stream_join_playlist_id_join_index` ON `${TABLE_NAME}` (`playlist_id`, `join_index`)"
},
{
"name": "index_playlist_stream_join_stream_id",
"unique": false,
"columnNames": [
"stream_id"
],
"createSql": "CREATE INDEX IF NOT EXISTS `index_playlist_stream_join_stream_id` ON `${TABLE_NAME}` (`stream_id`)"
}
],
"foreignKeys": [
{
"table": "playlists",
"onDelete": "CASCADE",
"onUpdate": "CASCADE",
"columns": [
"playlist_id"
],
"referencedColumns": [
"uid"
]
},
{
"table": "streams",
"onDelete": "CASCADE",
"onUpdate": "CASCADE",
"columns": [
"stream_id"
],
"referencedColumns": [
"uid"
]
}
]
},
{
"tableName": "remote_playlists",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `service_id` INTEGER NOT NULL, `name` TEXT, `url` TEXT, `thumbnail_url` TEXT, `uploader` TEXT, `stream_count` INTEGER)",
"fields": [
{
"fieldPath": "uid",
"columnName": "uid",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "serviceId",
"columnName": "service_id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "name",
"columnName": "name",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "url",
"columnName": "url",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "thumbnailUrl",
"columnName": "thumbnail_url",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "uploader",
"columnName": "uploader",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "streamCount",
"columnName": "stream_count",
"affinity": "INTEGER",
"notNull": false
}
],
"primaryKey": {
"columnNames": [
"uid"
],
"autoGenerate": true
},
"indices": [
{
"name": "index_remote_playlists_name",
"unique": false,
"columnNames": [
"name"
],
"createSql": "CREATE INDEX IF NOT EXISTS `index_remote_playlists_name` ON `${TABLE_NAME}` (`name`)"
},
{
"name": "index_remote_playlists_service_id_url",
"unique": true,
"columnNames": [
"service_id",
"url"
],
"createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_remote_playlists_service_id_url` ON `${TABLE_NAME}` (`service_id`, `url`)"
}
],
"foreignKeys": []
},
{
"tableName": "feed",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`stream_id` INTEGER NOT NULL, `subscription_id` INTEGER NOT NULL, PRIMARY KEY(`stream_id`, `subscription_id`), FOREIGN KEY(`stream_id`) REFERENCES `streams`(`uid`) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED, FOREIGN KEY(`subscription_id`) REFERENCES `subscriptions`(`uid`) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED)",
"fields": [
{
"fieldPath": "streamId",
"columnName": "stream_id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "subscriptionId",
"columnName": "subscription_id",
"affinity": "INTEGER",
"notNull": true
}
],
"primaryKey": {
"columnNames": [
"stream_id",
"subscription_id"
],
"autoGenerate": false
},
"indices": [
{
"name": "index_feed_subscription_id",
"unique": false,
"columnNames": [
"subscription_id"
],
"createSql": "CREATE INDEX IF NOT EXISTS `index_feed_subscription_id` ON `${TABLE_NAME}` (`subscription_id`)"
}
],
"foreignKeys": [
{
"table": "streams",
"onDelete": "CASCADE",
"onUpdate": "CASCADE",
"columns": [
"stream_id"
],
"referencedColumns": [
"uid"
]
},
{
"table": "subscriptions",
"onDelete": "CASCADE",
"onUpdate": "CASCADE",
"columns": [
"subscription_id"
],
"referencedColumns": [
"uid"
]
}
]
},
{
"tableName": "feed_group",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL, `icon_id` INTEGER NOT NULL, `sort_order` INTEGER NOT NULL)",
"fields": [
{
"fieldPath": "uid",
"columnName": "uid",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "name",
"columnName": "name",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "icon",
"columnName": "icon_id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "sortOrder",
"columnName": "sort_order",
"affinity": "INTEGER",
"notNull": true
}
],
"primaryKey": {
"columnNames": [
"uid"
],
"autoGenerate": true
},
"indices": [
{
"name": "index_feed_group_sort_order",
"unique": false,
"columnNames": [
"sort_order"
],
"createSql": "CREATE INDEX IF NOT EXISTS `index_feed_group_sort_order` ON `${TABLE_NAME}` (`sort_order`)"
}
],
"foreignKeys": []
},
{
"tableName": "feed_group_subscription_join",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`group_id` INTEGER NOT NULL, `subscription_id` INTEGER NOT NULL, PRIMARY KEY(`group_id`, `subscription_id`), FOREIGN KEY(`group_id`) REFERENCES `feed_group`(`uid`) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED, FOREIGN KEY(`subscription_id`) REFERENCES `subscriptions`(`uid`) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED)",
"fields": [
{
"fieldPath": "feedGroupId",
"columnName": "group_id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "subscriptionId",
"columnName": "subscription_id",
"affinity": "INTEGER",
"notNull": true
}
],
"primaryKey": {
"columnNames": [
"group_id",
"subscription_id"
],
"autoGenerate": false
},
"indices": [
{
"name": "index_feed_group_subscription_join_subscription_id",
"unique": false,
"columnNames": [
"subscription_id"
],
"createSql": "CREATE INDEX IF NOT EXISTS `index_feed_group_subscription_join_subscription_id` ON `${TABLE_NAME}` (`subscription_id`)"
}
],
"foreignKeys": [
{
"table": "feed_group",
"onDelete": "CASCADE",
"onUpdate": "CASCADE",
"columns": [
"group_id"
],
"referencedColumns": [
"uid"
]
},
{
"table": "subscriptions",
"onDelete": "CASCADE",
"onUpdate": "CASCADE",
"columns": [
"subscription_id"
],
"referencedColumns": [
"uid"
]
}
]
},
{
"tableName": "feed_last_updated",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`subscription_id` INTEGER NOT NULL, `last_updated` INTEGER, PRIMARY KEY(`subscription_id`), FOREIGN KEY(`subscription_id`) REFERENCES `subscriptions`(`uid`) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED)",
"fields": [
{
"fieldPath": "subscriptionId",
"columnName": "subscription_id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "lastUpdated",
"columnName": "last_updated",
"affinity": "INTEGER",
"notNull": false
}
],
"primaryKey": {
"columnNames": [
"subscription_id"
],
"autoGenerate": false
},
"indices": [],
"foreignKeys": [
{
"table": "subscriptions",
"onDelete": "CASCADE",
"onUpdate": "CASCADE",
"columns": [
"subscription_id"
],
"referencedColumns": [
"uid"
]
}
]
}
],
"views": [],
"setupQueries": [
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'd8070091972a7011bce18aed62f80b90')"
]
}
}

View file

@ -1,134 +0,0 @@
package org.schabi.newpipe.database
import android.content.ContentValues
import android.database.sqlite.SQLiteDatabase
import androidx.room.Room
import androidx.room.testing.MigrationTestHelper
import androidx.sqlite.db.framework.FrameworkSQLiteOpenHelperFactory
import androidx.test.core.app.ApplicationProvider
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.platform.app.InstrumentationRegistry
import org.junit.Assert.assertEquals
import org.junit.Assert.assertNull
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.schabi.newpipe.extractor.stream.StreamType
@RunWith(AndroidJUnit4::class)
class AppDatabaseTest {
companion object {
private const val DEFAULT_SERVICE_ID = 0
private const val DEFAULT_URL = "https://www.youtube.com/watch?v=cDphUib5iG4"
private const val DEFAULT_TITLE = "Test Title"
private val DEFAULT_TYPE = StreamType.VIDEO_STREAM
private const val DEFAULT_DURATION = 480L
private const val DEFAULT_UPLOADER_NAME = "Uploader Test"
private const val DEFAULT_THUMBNAIL = "https://example.com/example.jpg"
private const val DEFAULT_SECOND_SERVICE_ID = 0
private const val DEFAULT_SECOND_URL = "https://www.youtube.com/watch?v=ncQU6iBn5Fc"
}
@get:Rule
val testHelper = MigrationTestHelper(
InstrumentationRegistry.getInstrumentation(),
AppDatabase::class.java.canonicalName, FrameworkSQLiteOpenHelperFactory()
)
@Test
fun migrateDatabaseFrom2to3() {
val databaseInV2 = testHelper.createDatabase(AppDatabase.DATABASE_NAME, Migrations.DB_VER_2)
databaseInV2.run {
insert(
"streams", SQLiteDatabase.CONFLICT_FAIL,
ContentValues().apply {
// put("uid", null)
put("service_id", DEFAULT_SERVICE_ID)
put("url", DEFAULT_URL)
put("title", DEFAULT_TITLE)
put("stream_type", DEFAULT_TYPE.name)
put("duration", DEFAULT_DURATION)
put("uploader", DEFAULT_UPLOADER_NAME)
put("thumbnail_url", DEFAULT_THUMBNAIL)
}
)
insert(
"streams", SQLiteDatabase.CONFLICT_FAIL,
ContentValues().apply {
// put("uid", null)
put("service_id", DEFAULT_SECOND_SERVICE_ID)
put("url", DEFAULT_SECOND_URL)
// put("title", null)
// put("stream_type", null)
// put("duration", null)
// put("uploader", null)
// put("thumbnail_url", null)
}
)
insert(
"streams", SQLiteDatabase.CONFLICT_FAIL,
ContentValues().apply {
// put("uid", null)
put("service_id", DEFAULT_SERVICE_ID)
// put("url", null)
// put("title", null)
// put("stream_type", null)
// put("duration", null)
// put("uploader", null)
// put("thumbnail_url", null)
}
)
close()
}
testHelper.runMigrationsAndValidate(
AppDatabase.DATABASE_NAME, Migrations.DB_VER_3,
true, Migrations.MIGRATION_2_3
)
val migratedDatabaseV3 = getMigratedDatabase()
val listFromDB = migratedDatabaseV3.streamDAO().all.blockingFirst()
// Only expect 2, the one with the null url will be ignored
assertEquals(2, listFromDB.size)
val streamFromMigratedDatabase = listFromDB[0]
assertEquals(DEFAULT_SERVICE_ID, streamFromMigratedDatabase.serviceId)
assertEquals(DEFAULT_URL, streamFromMigratedDatabase.url)
assertEquals(DEFAULT_TITLE, streamFromMigratedDatabase.title)
assertEquals(DEFAULT_TYPE, streamFromMigratedDatabase.streamType)
assertEquals(DEFAULT_DURATION, streamFromMigratedDatabase.duration)
assertEquals(DEFAULT_UPLOADER_NAME, streamFromMigratedDatabase.uploader)
assertEquals(DEFAULT_THUMBNAIL, streamFromMigratedDatabase.thumbnailUrl)
assertNull(streamFromMigratedDatabase.viewCount)
assertNull(streamFromMigratedDatabase.textualUploadDate)
assertNull(streamFromMigratedDatabase.uploadDate)
assertNull(streamFromMigratedDatabase.isUploadDateApproximation)
val secondStreamFromMigratedDatabase = listFromDB[1]
assertEquals(DEFAULT_SECOND_SERVICE_ID, secondStreamFromMigratedDatabase.serviceId)
assertEquals(DEFAULT_SECOND_URL, secondStreamFromMigratedDatabase.url)
assertEquals("", secondStreamFromMigratedDatabase.title)
// Should fallback to VIDEO_STREAM
assertEquals(StreamType.VIDEO_STREAM, secondStreamFromMigratedDatabase.streamType)
assertEquals(0, secondStreamFromMigratedDatabase.duration)
assertEquals("", secondStreamFromMigratedDatabase.uploader)
assertEquals("", secondStreamFromMigratedDatabase.thumbnailUrl)
assertNull(secondStreamFromMigratedDatabase.viewCount)
assertNull(secondStreamFromMigratedDatabase.textualUploadDate)
assertNull(secondStreamFromMigratedDatabase.uploadDate)
assertNull(secondStreamFromMigratedDatabase.isUploadDateApproximation)
}
private fun getMigratedDatabase(): AppDatabase {
val database: AppDatabase = Room.databaseBuilder(
ApplicationProvider.getApplicationContext(),
AppDatabase::class.java, AppDatabase.DATABASE_NAME
)
.build()
testHelper.closeWhenFinished(database)
return database
}
}

View file

@ -45,7 +45,8 @@ class LocalPlaylistManagerTest {
fun createPlaylist() { fun createPlaylist() {
val stream = StreamEntity( val stream = StreamEntity(
serviceId = 1, url = "https://newpipe.net/", title = "title", serviceId = 1, url = "https://newpipe.net/", title = "title",
streamType = StreamType.VIDEO_STREAM, duration = 1, uploader = "uploader" streamType = StreamType.VIDEO_STREAM, duration = 1, uploader = "uploader",
uploaderUrl = "https://newpipe.net/"
) )
val result = manager.createPlaylist("name", listOf(stream)) val result = manager.createPlaylist("name", listOf(stream))
@ -69,12 +70,14 @@ class LocalPlaylistManagerTest {
fun createPlaylist_nonExistentStreamsAreUpserted() { fun createPlaylist_nonExistentStreamsAreUpserted() {
val stream = StreamEntity( val stream = StreamEntity(
serviceId = 1, url = "https://newpipe.net/", title = "title", serviceId = 1, url = "https://newpipe.net/", title = "title",
streamType = StreamType.VIDEO_STREAM, duration = 1, uploader = "uploader" streamType = StreamType.VIDEO_STREAM, duration = 1, uploader = "uploader",
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, url = "https://newpipe.net/2", title = "title2",
streamType = StreamType.VIDEO_STREAM, duration = 1, uploader = "uploader" streamType = StreamType.VIDEO_STREAM, duration = 1, uploader = "uploader",
uploaderUrl = "https://newpipe.net/"
) )
val result = manager.createPlaylist("name", listOf(stream, upserted)) val result = manager.createPlaylist("name", listOf(stream, upserted))

View file

@ -5,6 +5,7 @@ import android.os.Bundle;
import androidx.preference.Preference; import androidx.preference.Preference;
import org.schabi.newpipe.R; import org.schabi.newpipe.R;
import org.schabi.newpipe.util.PicassoHelper;
import leakcanary.LeakCanary; import leakcanary.LeakCanary;
@ -15,10 +16,13 @@ public class DebugSettingsFragment extends BasePreferenceFragment {
final Preference showMemoryLeaksPreference final Preference showMemoryLeaksPreference
= findPreference(getString(R.string.show_memory_leaks_key)); = findPreference(getString(R.string.show_memory_leaks_key));
final Preference showImageIndicatorsPreference
= findPreference(getString(R.string.show_image_indicators_key));
final Preference crashTheAppPreference final Preference crashTheAppPreference
= findPreference(getString(R.string.crash_the_app_key)); = findPreference(getString(R.string.crash_the_app_key));
assert showMemoryLeaksPreference != null; assert showMemoryLeaksPreference != null;
assert showImageIndicatorsPreference != null;
assert crashTheAppPreference != null; assert crashTheAppPreference != null;
showMemoryLeaksPreference.setOnPreferenceClickListener(preference -> { showMemoryLeaksPreference.setOnPreferenceClickListener(preference -> {
@ -26,6 +30,11 @@ public class DebugSettingsFragment extends BasePreferenceFragment {
return true; return true;
}); });
showImageIndicatorsPreference.setOnPreferenceChangeListener((preference, newValue) -> {
PicassoHelper.setIndicatorsEnabled((Boolean) newValue);
return true;
});
crashTheAppPreference.setOnPreferenceClickListener(preference -> { crashTheAppPreference.setOnPreferenceClickListener(preference -> {
throw new RuntimeException(); throw new RuntimeException();
}); });

View file

@ -11,9 +11,7 @@ import androidx.core.app.NotificationManagerCompat;
import androidx.multidex.MultiDexApplication; import androidx.multidex.MultiDexApplication;
import androidx.preference.PreferenceManager; import androidx.preference.PreferenceManager;
import com.nostra13.universalimageloader.cache.memory.impl.LRULimitedMemoryCache; import com.jakewharton.processphoenix.ProcessPhoenix;
import com.nostra13.universalimageloader.core.ImageLoader;
import com.nostra13.universalimageloader.core.ImageLoaderConfiguration;
import org.acra.ACRA; import org.acra.ACRA;
import org.acra.config.ACRAConfigurationException; import org.acra.config.ACRAConfigurationException;
@ -28,6 +26,7 @@ import org.schabi.newpipe.extractor.downloader.Downloader;
import org.schabi.newpipe.ktx.ExceptionUtils; import org.schabi.newpipe.ktx.ExceptionUtils;
import org.schabi.newpipe.settings.NewPipeSettings; import org.schabi.newpipe.settings.NewPipeSettings;
import org.schabi.newpipe.util.Localization; import org.schabi.newpipe.util.Localization;
import org.schabi.newpipe.util.PicassoHelper;
import org.schabi.newpipe.util.ServiceHelper; import org.schabi.newpipe.util.ServiceHelper;
import org.schabi.newpipe.util.StateSaver; import org.schabi.newpipe.util.StateSaver;
@ -65,9 +64,9 @@ import io.reactivex.rxjava3.plugins.RxJavaPlugins;
*/ */
public class App extends MultiDexApplication { public class App extends MultiDexApplication {
protected static final String TAG = App.class.toString();
private static App app;
public static final String PACKAGE_NAME = BuildConfig.APPLICATION_ID; public static final String PACKAGE_NAME = BuildConfig.APPLICATION_ID;
private static final String TAG = App.class.toString();
private static App app;
@Nullable @Nullable
private Disposable disposable = null; private Disposable disposable = null;
@ -89,6 +88,12 @@ public class App extends MultiDexApplication {
app = this; app = this;
if (ProcessPhoenix.isPhoenixProcess(this)) {
Log.i(TAG, "This is a phoenix process! "
+ "Aborting initialization of App[onCreate]");
return;
}
// Initialize settings first because others inits can use its values // Initialize settings first because others inits can use its values
NewPipeSettings.initSettings(this); NewPipeSettings.initSettings(this);
@ -103,7 +108,12 @@ public class App extends MultiDexApplication {
ServiceHelper.initServices(this); ServiceHelper.initServices(this);
// Initialize image loader // Initialize image loader
ImageLoader.getInstance().init(getImageLoaderConfigurations(10, 50)); final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this);
PicassoHelper.init(this);
PicassoHelper.setShouldLoadImages(
prefs.getBoolean(getString(R.string.download_thumbnail_key), true));
PicassoHelper.setIndicatorsEnabled(MainActivity.DEBUG
&& prefs.getBoolean(getString(R.string.show_image_indicators_key), false));
configureRxJavaErrorHandler(); configureRxJavaErrorHandler();
@ -117,6 +127,7 @@ public class App extends MultiDexApplication {
disposable.dispose(); disposable.dispose();
} }
super.onTerminate(); super.onTerminate();
PicassoHelper.terminate();
} }
protected Downloader getDownloader() { protected Downloader getDownloader() {
@ -201,15 +212,6 @@ public class App extends MultiDexApplication {
}); });
} }
private ImageLoaderConfiguration getImageLoaderConfigurations(final int memoryCacheSizeMb,
final int diskCacheSizeMb) {
return new ImageLoaderConfiguration.Builder(this)
.memoryCache(new LRULimitedMemoryCache(memoryCacheSizeMb * 1024 * 1024))
.diskCacheSize(diskCacheSizeMb * 1024 * 1024)
.imageDownloader(new ImageDownloader(getApplicationContext()))
.build();
}
/** /**
* Called in {@link #attachBaseContext(Context)} after calling the {@code super} method. * 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. * Should be overridden if MultiDex is enabled, since it has to be initialized before ACRA.

View file

@ -10,16 +10,13 @@ import androidx.appcompat.app.AppCompatActivity;
import androidx.fragment.app.Fragment; import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentManager; import androidx.fragment.app.FragmentManager;
import com.nostra13.universalimageloader.core.ImageLoader;
import icepick.Icepick; import icepick.Icepick;
import icepick.State; import icepick.State;
import leakcanary.AppWatcher; import leakcanary.AppWatcher;
public abstract class BaseFragment extends Fragment { public abstract class BaseFragment extends Fragment {
public static final ImageLoader IMAGE_LOADER = ImageLoader.getInstance();
protected final String TAG = getClass().getSimpleName() + "@" + Integer.toHexString(hashCode()); protected final String TAG = getClass().getSimpleName() + "@" + Integer.toHexString(hashCode());
protected final boolean DEBUG = MainActivity.DEBUG; protected static final boolean DEBUG = MainActivity.DEBUG;
protected AppCompatActivity activity; protected AppCompatActivity activity;
//These values are used for controlling fragments when they are part of the frontpage //These values are used for controlling fragments when they are part of the frontpage
@State @State

View file

@ -17,7 +17,6 @@ import org.schabi.newpipe.util.InfoCache;
import org.schabi.newpipe.util.TLSSocketFactoryCompat; import org.schabi.newpipe.util.TLSSocketFactoryCompat;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream;
import java.security.KeyManagementException; import java.security.KeyManagementException;
import java.security.KeyStore; import java.security.KeyStore;
import java.security.KeyStoreException; import java.security.KeyStoreException;
@ -200,47 +199,6 @@ public final class DownloaderImpl extends Downloader {
} }
} }
public InputStream stream(final String siteUrl) throws IOException {
try {
final okhttp3.Request.Builder requestBuilder = new okhttp3.Request.Builder()
.method("GET", null).url(siteUrl)
.addHeader("User-Agent", USER_AGENT);
final String cookies = getCookies(siteUrl);
if (!cookies.isEmpty()) {
requestBuilder.addHeader("Cookie", cookies);
}
final okhttp3.Request request = requestBuilder.build();
OkHttpClient tmpClient = client;
final okhttp3.Response response;
if (customTimeout != null) {
tmpClient = new OkHttpClient.Builder()
.readTimeout(customTimeout, TimeUnit.SECONDS)
.build();
}
response = tmpClient.newCall(request).execute();
final ResponseBody body = response.body();
if (response.code() == 429) {
throw new ReCaptchaException("reCaptcha Challenge requested", siteUrl);
}
if (body == null) {
response.close();
return null;
}
return body.byteStream();
} catch (final ReCaptchaException e) {
throw new IOException(e.getMessage(), e.getCause());
}
}
@Override @Override
public Response execute(@NonNull final Request request) public Response execute(@NonNull final Request request)
throws IOException, ReCaptchaException { throws IOException, ReCaptchaException {

View file

@ -1,48 +0,0 @@
package org.schabi.newpipe;
import android.annotation.SuppressLint;
import android.content.Context;
import android.content.SharedPreferences;
import android.content.res.Resources;
import androidx.preference.PreferenceManager;
import com.nostra13.universalimageloader.core.download.BaseImageDownloader;
import org.schabi.newpipe.extractor.NewPipe;
import java.io.IOException;
import java.io.InputStream;
public class ImageDownloader extends BaseImageDownloader {
private final Resources resources;
private final SharedPreferences preferences;
private final String downloadThumbnailKey;
public ImageDownloader(final Context context) {
super(context);
this.resources = context.getResources();
this.preferences = PreferenceManager.getDefaultSharedPreferences(context);
this.downloadThumbnailKey = context.getString(R.string.download_thumbnail_key);
}
private boolean isDownloadingThumbnail() {
return preferences.getBoolean(downloadThumbnailKey, true);
}
@SuppressLint("ResourceType")
@Override
public InputStream getStream(final String imageUri, final Object extra) throws IOException {
if (isDownloadingThumbnail()) {
return super.getStream(imageUri, extra);
} else {
return resources.openRawResource(R.drawable.dummy_thumbnail_dark);
}
}
protected InputStream getStreamFromNetwork(final String imageUri, final Object extra)
throws IOException {
final DownloaderImpl downloader = (DownloaderImpl) NewPipe.getDownloader();
return downloader.stream(imageUri);
}
}

View file

@ -11,6 +11,7 @@ import org.schabi.newpipe.database.AppDatabase;
import static org.schabi.newpipe.database.AppDatabase.DATABASE_NAME; import static org.schabi.newpipe.database.AppDatabase.DATABASE_NAME;
import static org.schabi.newpipe.database.Migrations.MIGRATION_1_2; import static org.schabi.newpipe.database.Migrations.MIGRATION_1_2;
import static org.schabi.newpipe.database.Migrations.MIGRATION_2_3; import static org.schabi.newpipe.database.Migrations.MIGRATION_2_3;
import static org.schabi.newpipe.database.Migrations.MIGRATION_3_4;
public final class NewPipeDatabase { public final class NewPipeDatabase {
private static volatile AppDatabase databaseInstance; private static volatile AppDatabase databaseInstance;
@ -22,7 +23,7 @@ public final class NewPipeDatabase {
private static AppDatabase getDatabase(final Context context) { private static AppDatabase getDatabase(final Context context) {
return Room return Room
.databaseBuilder(context.getApplicationContext(), AppDatabase.class, DATABASE_NAME) .databaseBuilder(context.getApplicationContext(), AppDatabase.class, DATABASE_NAME)
.addMigrations(MIGRATION_1_2, MIGRATION_2_3) .addMigrations(MIGRATION_1_2, MIGRATION_2_3, MIGRATION_3_4)
.build(); .build();
} }

View file

@ -0,0 +1,70 @@
package org.schabi.newpipe;
import static org.schabi.newpipe.util.external_communication.ShareUtils.shareText;
import android.content.Context;
import android.view.ContextThemeWrapper;
import android.view.View;
import android.widget.PopupMenu;
import androidx.fragment.app.FragmentManager;
import org.schabi.newpipe.local.dialog.PlaylistAppendDialog;
import org.schabi.newpipe.local.dialog.PlaylistCreationDialog;
import org.schabi.newpipe.player.playqueue.PlayQueue;
import org.schabi.newpipe.player.playqueue.PlayQueueItem;
import org.schabi.newpipe.util.NavigationHelper;
import java.util.Collections;
public final class QueueItemMenuUtil {
public static void openPopupMenu(final PlayQueue playQueue,
final PlayQueueItem item,
final View view,
final boolean hideDetails,
final FragmentManager fragmentManager,
final Context context) {
final ContextThemeWrapper themeWrapper =
new ContextThemeWrapper(context, R.style.DarkPopupMenu);
final PopupMenu popupMenu = new PopupMenu(themeWrapper, view);
popupMenu.inflate(R.menu.menu_play_queue_item);
if (hideDetails) {
popupMenu.getMenu().findItem(R.id.menu_item_details).setVisible(false);
}
popupMenu.setOnMenuItemClickListener(menuItem -> {
switch (menuItem.getItemId()) {
case R.id.menu_item_remove:
final int index = playQueue.indexOf(item);
playQueue.remove(index);
return true;
case R.id.menu_item_details:
// playQueue is null since we don't want any queue change
NavigationHelper.openVideoDetail(context, item.getServiceId(),
item.getUrl(), item.getTitle(), null,
false);
return true;
case R.id.menu_item_append_playlist:
final PlaylistAppendDialog d = PlaylistAppendDialog.fromPlayQueueItems(
Collections.singletonList(item)
);
PlaylistAppendDialog.onPlaylistFound(context,
() -> d.show(fragmentManager, "QueueItemMenuUtil@append_playlist"),
() -> PlaylistCreationDialog.newInstance(d)
.show(fragmentManager, "QueueItemMenuUtil@append_playlist"));
return true;
case R.id.menu_item_share:
shareText(context, item.getTitle(), item.getUrl(),
item.getThumbnailUrl());
return true;
}
return false;
});
popupMenu.show();
}
private QueueItemMenuUtil() { }
}

View file

@ -162,10 +162,18 @@ class AboutActivity : AppCompatActivity() {
"OkHttp", "2019", "Square, Inc.", "OkHttp", "2019", "Square, Inc.",
"https://square.github.io/okhttp/", StandardLicenses.APACHE2 "https://square.github.io/okhttp/", StandardLicenses.APACHE2
), ),
SoftwareComponent(
"Picasso", "2013", "Square, Inc.",
"https://square.github.io/picasso/", StandardLicenses.APACHE2
),
SoftwareComponent( SoftwareComponent(
"PrettyTime", "2012 - 2020", "Lincoln Baxter, III", "PrettyTime", "2012 - 2020", "Lincoln Baxter, III",
"https://github.com/ocpsoft/prettytime", StandardLicenses.APACHE2 "https://github.com/ocpsoft/prettytime", StandardLicenses.APACHE2
), ),
SoftwareComponent(
"ProcessPhoenix", "2015", "Jake Wharton",
"https://github.com/JakeWharton/ProcessPhoenix", StandardLicenses.APACHE2
),
SoftwareComponent( SoftwareComponent(
"RxAndroid", "2015", "The RxAndroid authors", "RxAndroid", "2015", "The RxAndroid authors",
"https://github.com/ReactiveX/RxAndroid", StandardLicenses.APACHE2 "https://github.com/ReactiveX/RxAndroid", StandardLicenses.APACHE2
@ -177,11 +185,6 @@ class AboutActivity : AppCompatActivity() {
SoftwareComponent( SoftwareComponent(
"RxJava", "2016 - 2020", "RxJava Contributors", "RxJava", "2016 - 2020", "RxJava Contributors",
"https://github.com/ReactiveX/RxJava", StandardLicenses.APACHE2 "https://github.com/ReactiveX/RxJava", StandardLicenses.APACHE2
),
SoftwareComponent(
"Universal Image Loader", "2011 - 2015", "Sergey Tarasevich",
"https://github.com/nostra13/Android-Universal-Image-Loader",
StandardLicenses.APACHE2
) )
) )
private const val POS_ABOUT = 0 private const val POS_ABOUT = 0

View file

@ -1,7 +1,7 @@
package org.schabi.newpipe.about package org.schabi.newpipe.about
import android.os.Parcelable import android.os.Parcelable
import kotlinx.android.parcel.Parcelize import kotlinx.parcelize.Parcelize
import java.io.Serializable import java.io.Serializable
/** /**

View file

@ -108,7 +108,7 @@ object LicenseFragmentHelper {
alert.setView(webView) alert.setView(webView)
Localization.assureCorrectAppLanguage(context) Localization.assureCorrectAppLanguage(context)
alert.setNegativeButton( alert.setNegativeButton(
context.getString(R.string.finish) context.getString(R.string.ok)
) { dialog, _ -> dialog.dismiss() } ) { dialog, _ -> dialog.dismiss() }
alert.show() alert.show()
} }

View file

@ -1,7 +1,7 @@
package org.schabi.newpipe.about package org.schabi.newpipe.about
import android.os.Parcelable import android.os.Parcelable
import kotlinx.android.parcel.Parcelize import kotlinx.parcelize.Parcelize
@Parcelize @Parcelize
class SoftwareComponent class SoftwareComponent

View file

@ -27,7 +27,7 @@ import org.schabi.newpipe.database.stream.model.StreamStateEntity;
import org.schabi.newpipe.database.subscription.SubscriptionDAO; import org.schabi.newpipe.database.subscription.SubscriptionDAO;
import org.schabi.newpipe.database.subscription.SubscriptionEntity; import org.schabi.newpipe.database.subscription.SubscriptionEntity;
import static org.schabi.newpipe.database.Migrations.DB_VER_3; import static org.schabi.newpipe.database.Migrations.DB_VER_4;
@TypeConverters({Converters.class}) @TypeConverters({Converters.class})
@Database( @Database(
@ -38,7 +38,7 @@ import static org.schabi.newpipe.database.Migrations.DB_VER_3;
FeedEntity.class, FeedGroupEntity.class, FeedGroupSubscriptionEntity.class, FeedEntity.class, FeedGroupEntity.class, FeedGroupSubscriptionEntity.class,
FeedLastUpdatedEntity.class FeedLastUpdatedEntity.class
}, },
version = DB_VER_3 version = DB_VER_4
) )
public abstract class AppDatabase extends RoomDatabase { public abstract class AppDatabase extends RoomDatabase {
public static final String DATABASE_NAME = "newpipe.db"; public static final String DATABASE_NAME = "newpipe.db";

View file

@ -9,9 +9,19 @@ import androidx.sqlite.db.SupportSQLiteDatabase;
import org.schabi.newpipe.MainActivity; import org.schabi.newpipe.MainActivity;
public final class Migrations { public final class Migrations {
/////////////////////////////////////////////////////////////////////////////
// Test new migrations manually by importing a database from daily usage //
// and checking if the migration works (Use the Database Inspector //
// https://developer.android.com/studio/inspect/database). //
// If you add a migration point it out in the pull request, so that //
// others remember to test it themselves. //
/////////////////////////////////////////////////////////////////////////////
public static final int DB_VER_1 = 1; public static final int DB_VER_1 = 1;
public static final int DB_VER_2 = 2; public static final int DB_VER_2 = 2;
public static final int DB_VER_3 = 3; public static final int DB_VER_3 = 3;
public static final int DB_VER_4 = 4;
private static final String TAG = Migrations.class.getName(); private static final String TAG = Migrations.class.getName();
public static final boolean DEBUG = MainActivity.DEBUG; public static final boolean DEBUG = MainActivity.DEBUG;
@ -160,5 +170,14 @@ public final class Migrations {
} }
}; };
public static final Migration MIGRATION_3_4 = new Migration(DB_VER_3, DB_VER_4) {
@Override
public void migrate(@NonNull final SupportSQLiteDatabase database) {
database.execSQL(
"ALTER TABLE streams ADD COLUMN uploader_url TEXT"
);
}
};
private Migrations() { } private Migrations() { }
} }

View file

@ -27,6 +27,7 @@ data class PlaylistStreamEntry(
val item = StreamInfoItem(streamEntity.serviceId, streamEntity.url, streamEntity.title, streamEntity.streamType) val item = StreamInfoItem(streamEntity.serviceId, streamEntity.url, streamEntity.title, streamEntity.streamType)
item.duration = streamEntity.duration item.duration = streamEntity.duration
item.uploaderName = streamEntity.uploader item.uploaderName = streamEntity.uploader
item.uploaderUrl = streamEntity.uploaderUrl
item.thumbnailUrl = streamEntity.thumbnailUrl item.thumbnailUrl = streamEntity.thumbnailUrl
return item return item

View file

@ -29,6 +29,7 @@ class StreamStatisticsEntry(
val item = StreamInfoItem(streamEntity.serviceId, streamEntity.url, streamEntity.title, streamEntity.streamType) val item = StreamInfoItem(streamEntity.serviceId, streamEntity.url, streamEntity.title, streamEntity.streamType)
item.duration = streamEntity.duration item.duration = streamEntity.duration
item.uploaderName = streamEntity.uploader item.uploaderName = streamEntity.uploader
item.uploaderUrl = streamEntity.uploaderUrl
item.thumbnailUrl = streamEntity.thumbnailUrl item.thumbnailUrl = streamEntity.thumbnailUrl
return item return item

View file

@ -6,6 +6,7 @@ import androidx.room.Insert
import androidx.room.OnConflictStrategy import androidx.room.OnConflictStrategy
import androidx.room.Query import androidx.room.Query
import androidx.room.Transaction import androidx.room.Transaction
import io.reactivex.rxjava3.core.Completable
import io.reactivex.rxjava3.core.Flowable import io.reactivex.rxjava3.core.Flowable
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
@ -29,6 +30,9 @@ abstract class StreamDAO : BasicDAO<StreamEntity> {
@Query("SELECT * FROM streams WHERE url = :url AND service_id = :serviceId") @Query("SELECT * FROM streams WHERE url = :url AND service_id = :serviceId")
abstract fun getStream(serviceId: Long, url: String): Flowable<List<StreamEntity>> abstract fun getStream(serviceId: Long, url: String): Flowable<List<StreamEntity>>
@Query("UPDATE streams SET uploader_url = :uploaderUrl WHERE url = :url AND service_id = :serviceId")
abstract fun setUploaderUrl(serviceId: Long, url: String, uploaderUrl: String): Completable
@Insert(onConflict = OnConflictStrategy.IGNORE) @Insert(onConflict = OnConflictStrategy.IGNORE)
internal abstract fun silentInsertInternal(stream: StreamEntity): Long internal abstract fun silentInsertInternal(stream: StreamEntity): Long

View file

@ -45,6 +45,9 @@ data class StreamEntity(
@ColumnInfo(name = STREAM_UPLOADER) @ColumnInfo(name = STREAM_UPLOADER)
var uploader: String, var uploader: String,
@ColumnInfo(name = STREAM_UPLOADER_URL)
var uploaderUrl: String? = null,
@ColumnInfo(name = STREAM_THUMBNAIL_URL) @ColumnInfo(name = STREAM_THUMBNAIL_URL)
var thumbnailUrl: String? = null, var thumbnailUrl: String? = null,
@ -64,7 +67,7 @@ data class StreamEntity(
constructor(item: StreamInfoItem) : this( constructor(item: StreamInfoItem) : this(
serviceId = item.serviceId, url = item.url, title = item.name, serviceId = item.serviceId, url = item.url, title = item.name,
streamType = item.streamType, duration = item.duration, uploader = item.uploaderName, streamType = item.streamType, duration = item.duration, uploader = item.uploaderName,
thumbnailUrl = item.thumbnailUrl, viewCount = item.viewCount, uploaderUrl = item.uploaderUrl, thumbnailUrl = item.thumbnailUrl, viewCount = item.viewCount,
textualUploadDate = item.textualUploadDate, uploadDate = item.uploadDate?.offsetDateTime(), textualUploadDate = item.textualUploadDate, uploadDate = item.uploadDate?.offsetDateTime(),
isUploadDateApproximation = item.uploadDate?.isApproximation isUploadDateApproximation = item.uploadDate?.isApproximation
) )
@ -73,7 +76,7 @@ data class StreamEntity(
constructor(info: StreamInfo) : this( constructor(info: StreamInfo) : this(
serviceId = info.serviceId, url = info.url, title = info.name, serviceId = info.serviceId, url = info.url, title = info.name,
streamType = info.streamType, duration = info.duration, uploader = info.uploaderName, streamType = info.streamType, duration = info.duration, uploader = info.uploaderName,
thumbnailUrl = info.thumbnailUrl, viewCount = info.viewCount, uploaderUrl = info.uploaderUrl, thumbnailUrl = info.thumbnailUrl, viewCount = info.viewCount,
textualUploadDate = info.textualUploadDate, uploadDate = info.uploadDate?.offsetDateTime(), textualUploadDate = info.textualUploadDate, uploadDate = info.uploadDate?.offsetDateTime(),
isUploadDateApproximation = info.uploadDate?.isApproximation isUploadDateApproximation = info.uploadDate?.isApproximation
) )
@ -82,13 +85,14 @@ data class StreamEntity(
constructor(item: PlayQueueItem) : this( constructor(item: PlayQueueItem) : this(
serviceId = item.serviceId, url = item.url, title = item.title, serviceId = item.serviceId, url = item.url, title = item.title,
streamType = item.streamType, duration = item.duration, uploader = item.uploader, streamType = item.streamType, duration = item.duration, uploader = item.uploader,
thumbnailUrl = item.thumbnailUrl uploaderUrl = item.uploaderUrl, thumbnailUrl = item.thumbnailUrl
) )
fun toStreamInfoItem(): StreamInfoItem { fun toStreamInfoItem(): StreamInfoItem {
val item = StreamInfoItem(serviceId, url, title, streamType) val item = StreamInfoItem(serviceId, url, title, streamType)
item.duration = duration item.duration = duration
item.uploaderName = uploader item.uploaderName = uploader
item.uploaderUrl = uploaderUrl
item.thumbnailUrl = thumbnailUrl item.thumbnailUrl = thumbnailUrl
if (viewCount != null) item.viewCount = viewCount as Long if (viewCount != null) item.viewCount = viewCount as Long
@ -109,6 +113,7 @@ data class StreamEntity(
const val STREAM_TYPE = "stream_type" const val STREAM_TYPE = "stream_type"
const val STREAM_DURATION = "duration" const val STREAM_DURATION = "duration"
const val STREAM_UPLOADER = "uploader" const val STREAM_UPLOADER = "uploader"
const val STREAM_UPLOADER_URL = "uploader_url"
const val STREAM_THUMBNAIL_URL = "thumbnail_url" const val STREAM_THUMBNAIL_URL = "thumbnail_url"
const val STREAM_VIEWS = "view_count" const val STREAM_VIEWS = "view_count"

View file

@ -688,7 +688,7 @@ public class DownloadDialog extends DialogFragment
new AlertDialog.Builder(context) new AlertDialog.Builder(context)
.setTitle(R.string.general_error) .setTitle(R.string.general_error)
.setMessage(msg) .setMessage(msg)
.setNegativeButton(getString(R.string.finish), null) .setNegativeButton(getString(R.string.ok), null)
.create() .create()
.show(); .show();
} }
@ -871,7 +871,7 @@ public class DownloadDialog extends DialogFragment
final AlertDialog.Builder askDialog = new AlertDialog.Builder(context) final AlertDialog.Builder askDialog = new AlertDialog.Builder(context)
.setTitle(R.string.download_dialog_title) .setTitle(R.string.download_dialog_title)
.setMessage(msgBody) .setMessage(msgBody)
.setNegativeButton(android.R.string.cancel, null); .setNegativeButton(R.string.cancel, null);
final StoredFileHelper finalStorage = storage; final StoredFileHelper finalStorage = storage;

View file

@ -2,7 +2,7 @@ package org.schabi.newpipe.error
import android.os.Parcelable import android.os.Parcelable
import androidx.annotation.StringRes import androidx.annotation.StringRes
import kotlinx.android.parcel.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
import org.schabi.newpipe.extractor.NewPipe import org.schabi.newpipe.extractor.NewPipe

View file

@ -6,6 +6,8 @@ import android.util.Log
import android.view.View import android.view.View
import android.widget.Button import android.widget.Button
import android.widget.TextView import android.widget.TextView
import androidx.annotation.Nullable
import androidx.annotation.StringRes
import androidx.core.view.isVisible import androidx.core.view.isVisible
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import com.jakewharton.rxbinding4.view.clicks import com.jakewharton.rxbinding4.view.clicks
@ -37,22 +39,39 @@ class ErrorPanelHelper(
onRetry: Runnable onRetry: Runnable
) { ) {
private val context: Context = rootView.context!! private val context: Context = rootView.context!!
private val errorPanelRoot: View = rootView.findViewById(R.id.error_panel) private val errorPanelRoot: View = rootView.findViewById(R.id.error_panel)
private val errorTextView: TextView = errorPanelRoot.findViewById(R.id.error_message_view)
private val errorServiceInfoTextView: TextView = errorPanelRoot.findViewById(R.id.error_message_service_info_view) // the only element that is visible by default
private val errorServiceExplenationTextView: TextView = errorPanelRoot.findViewById(R.id.error_message_service_explenation_view) private val errorTextView: TextView =
private val errorButtonAction: Button = errorPanelRoot.findViewById(R.id.error_button_action) errorPanelRoot.findViewById(R.id.error_message_view)
private val errorButtonRetry: Button = errorPanelRoot.findViewById(R.id.error_button_retry) private val errorServiceInfoTextView: TextView =
errorPanelRoot.findViewById(R.id.error_message_service_info_view)
private val errorServiceExplanationTextView: TextView =
errorPanelRoot.findViewById(R.id.error_message_service_explanation_view)
private val errorActionButton: Button =
errorPanelRoot.findViewById(R.id.error_action_button)
private val errorRetryButton: Button =
errorPanelRoot.findViewById(R.id.error_retry_button)
private var errorDisposable: Disposable? = null private var errorDisposable: Disposable? = null
init { init {
errorDisposable = errorButtonRetry.clicks() errorDisposable = errorRetryButton.clicks()
.debounce(300, TimeUnit.MILLISECONDS) .debounce(300, TimeUnit.MILLISECONDS)
.observeOn(AndroidSchedulers.mainThread()) .observeOn(AndroidSchedulers.mainThread())
.subscribe { onRetry.run() } .subscribe { onRetry.run() }
} }
private fun ensureDefaultVisibility() {
errorTextView.isVisible = true
errorServiceInfoTextView.isVisible = false
errorServiceExplanationTextView.isVisible = false
errorActionButton.isVisible = false
errorRetryButton.isVisible = false
}
fun showError(errorInfo: ErrorInfo) { fun showError(errorInfo: ErrorInfo) {
if (errorInfo.throwable != null && errorInfo.throwable!!.isInterruptedCaused) { if (errorInfo.throwable != null && errorInfo.throwable!!.isInterruptedCaused) {
@ -62,10 +81,14 @@ class ErrorPanelHelper(
return return
} }
errorButtonAction.isVisible = true ensureDefaultVisibility()
if (errorInfo.throwable is ReCaptchaException) { if (errorInfo.throwable is ReCaptchaException) {
errorButtonAction.setText(R.string.recaptcha_solve) errorTextView.setText(R.string.recaptcha_request_toast)
errorButtonAction.setOnClickListener {
showAndSetErrorButtonAction(
R.string.recaptcha_solve
) {
// Starting ReCaptcha Challenge Activity // Starting ReCaptcha Challenge Activity
val intent = Intent(context, ReCaptchaActivity::class.java) val intent = Intent(context, ReCaptchaActivity::class.java)
intent.putExtra( intent.putExtra(
@ -73,78 +96,70 @@ class ErrorPanelHelper(
(errorInfo.throwable as ReCaptchaException).url (errorInfo.throwable as ReCaptchaException).url
) )
fragment.startActivityForResult(intent, ReCaptchaActivity.RECAPTCHA_REQUEST) fragment.startActivityForResult(intent, ReCaptchaActivity.RECAPTCHA_REQUEST)
errorButtonAction.setOnClickListener(null) errorActionButton.setOnClickListener(null)
} }
errorTextView.setText(R.string.recaptcha_request_toast)
// additional info is only provided by AccountTerminatedException errorRetryButton.isVisible = true
errorServiceInfoTextView.isVisible = false
errorServiceExplenationTextView.isVisible = false
errorButtonRetry.isVisible = true
} else if (errorInfo.throwable is AccountTerminatedException) { } else if (errorInfo.throwable is AccountTerminatedException) {
errorButtonRetry.isVisible = false
errorButtonAction.isVisible = false
errorTextView.setText(R.string.account_terminated) errorTextView.setText(R.string.account_terminated)
if (!isNullOrEmpty((errorInfo.throwable as AccountTerminatedException).message)) { if (!isNullOrEmpty((errorInfo.throwable as AccountTerminatedException).message)) {
errorServiceInfoTextView.setText( errorServiceInfoTextView.text = context.resources.getString(
context.resources.getString( R.string.service_provides_reason,
R.string.service_provides_reason, NewPipe.getNameOfService(ServiceHelper.getSelectedServiceId(context))
NewPipe.getNameOfService(ServiceHelper.getSelectedServiceId(context))
)
)
errorServiceExplenationTextView.setText(
(errorInfo.throwable as AccountTerminatedException).message
) )
errorServiceInfoTextView.isVisible = true errorServiceInfoTextView.isVisible = true
errorServiceExplenationTextView.isVisible = true
} else { errorServiceExplanationTextView.text =
errorServiceInfoTextView.isVisible = false (errorInfo.throwable as AccountTerminatedException).message
errorServiceExplenationTextView.isVisible = false errorServiceExplanationTextView.isVisible = true
} }
} else { } else {
errorButtonAction.setText(R.string.error_snackbar_action) showAndSetErrorButtonAction(
errorButtonAction.setOnClickListener { R.string.error_snackbar_action
) {
ErrorActivity.reportError(context, errorInfo) ErrorActivity.reportError(context, errorInfo)
} }
// additional info is only provided by AccountTerminatedException errorTextView.setText(getExceptionDescription(errorInfo.throwable))
errorServiceInfoTextView.isVisible = false
errorServiceExplenationTextView.isVisible = false
// hide retry button by default, then show only if not unavailable/unsupported content if (errorInfo.throwable !is ContentNotAvailableException &&
errorButtonRetry.isVisible = false errorInfo.throwable !is ContentNotSupportedException
errorTextView.setText( ) {
when (errorInfo.throwable) { // show retry button only for content which is not unavailable or unsupported
is AgeRestrictedContentException -> R.string.restricted_video_no_stream errorRetryButton.isVisible = true
is GeographicRestrictionException -> R.string.georestricted_content }
is PaidContentException -> R.string.paid_content
is PrivateContentException -> R.string.private_content
is SoundCloudGoPlusContentException -> R.string.soundcloud_go_plus_content
is YoutubeMusicPremiumContentException -> R.string.youtube_music_premium_content
is ContentNotAvailableException -> R.string.content_not_available
is ContentNotSupportedException -> R.string.content_not_supported
else -> {
// show retry button only for content which is not unavailable or unsupported
errorButtonRetry.isVisible = true
if (errorInfo.throwable != null && errorInfo.throwable!!.isNetworkRelated) {
R.string.network_error
} else {
R.string.error_snackbar_message
}
}
}
)
} }
errorPanelRoot.animate(true, 300)
setRootVisible()
}
/**
* Shows the errorButtonAction, sets a text into it and sets the click listener.
*/
private fun showAndSetErrorButtonAction(
@StringRes resid: Int,
@Nullable listener: View.OnClickListener
) {
errorActionButton.isVisible = true
errorActionButton.setText(resid)
errorActionButton.setOnClickListener(listener)
} }
fun showTextError(errorString: String) { fun showTextError(errorString: String) {
errorButtonAction.isVisible = false ensureDefaultVisibility()
errorButtonRetry.isVisible = false
errorTextView.text = errorString errorTextView.text = errorString
setRootVisible()
}
private fun setRootVisible() {
errorPanelRoot.animate(true, 300)
} }
fun hide() { fun hide() {
errorButtonAction.setOnClickListener(null) errorActionButton.setOnClickListener(null)
errorPanelRoot.animate(false, 150) errorPanelRoot.animate(false, 150)
} }
@ -153,13 +168,35 @@ class ErrorPanelHelper(
} }
fun dispose() { fun dispose() {
errorButtonAction.setOnClickListener(null) errorActionButton.setOnClickListener(null)
errorButtonRetry.setOnClickListener(null) errorRetryButton.setOnClickListener(null)
errorDisposable?.dispose() errorDisposable?.dispose()
} }
companion object { companion object {
val TAG: String = ErrorPanelHelper::class.simpleName!! val TAG: String = ErrorPanelHelper::class.simpleName!!
val DEBUG: Boolean = MainActivity.DEBUG val DEBUG: Boolean = MainActivity.DEBUG
@StringRes
public fun getExceptionDescription(throwable: Throwable?): Int {
return when (throwable) {
is AgeRestrictedContentException -> R.string.restricted_video_no_stream
is GeographicRestrictionException -> R.string.georestricted_content
is PaidContentException -> R.string.paid_content
is PrivateContentException -> R.string.private_content
is SoundCloudGoPlusContentException -> R.string.soundcloud_go_plus_content
is YoutubeMusicPremiumContentException -> R.string.youtube_music_premium_content
is ContentNotAvailableException -> R.string.content_not_available
is ContentNotSupportedException -> R.string.content_not_supported
else -> {
// show retry button only for content which is not unavailable or unsupported
if (throwable != null && throwable.isNetworkRelated) {
R.string.network_error
} else {
R.string.error_snackbar_message
}
}
}
}
} }
} }

View file

@ -1,5 +1,6 @@
package org.schabi.newpipe.error; package org.schabi.newpipe.error;
import android.annotation.SuppressLint;
import android.content.Intent; import android.content.Intent;
import android.content.SharedPreferences; import android.content.SharedPreferences;
import android.os.Build; import android.os.Build;
@ -66,6 +67,7 @@ public class ReCaptchaActivity extends AppCompatActivity {
private ActivityRecaptchaBinding recaptchaBinding; private ActivityRecaptchaBinding recaptchaBinding;
private String foundCookies = ""; private String foundCookies = "";
@SuppressLint("SetJavaScriptEnabled")
@Override @Override
protected void onCreate(final Bundle savedInstanceState) { protected void onCreate(final Bundle savedInstanceState) {
ThemeHelper.setTheme(this); ThemeHelper.setTheme(this);

View file

@ -48,9 +48,7 @@ import com.google.android.exoplayer2.PlaybackParameters;
import com.google.android.material.appbar.AppBarLayout; import com.google.android.material.appbar.AppBarLayout;
import com.google.android.material.bottomsheet.BottomSheetBehavior; import com.google.android.material.bottomsheet.BottomSheetBehavior;
import com.google.android.material.tabs.TabLayout; import com.google.android.material.tabs.TabLayout;
import com.nostra13.universalimageloader.core.assist.FailReason; import com.squareup.picasso.Callback;
import com.nostra13.universalimageloader.core.listener.ImageLoadingListener;
import com.nostra13.universalimageloader.core.listener.SimpleImageLoadingListener;
import org.schabi.newpipe.App; import org.schabi.newpipe.App;
import org.schabi.newpipe.R; import org.schabi.newpipe.R;
@ -90,15 +88,15 @@ import org.schabi.newpipe.player.playqueue.SinglePlayQueue;
import org.schabi.newpipe.util.Constants; import org.schabi.newpipe.util.Constants;
import org.schabi.newpipe.util.DeviceUtils; import org.schabi.newpipe.util.DeviceUtils;
import org.schabi.newpipe.util.ExtractorHelper; import org.schabi.newpipe.util.ExtractorHelper;
import org.schabi.newpipe.util.ImageDisplayConstants;
import org.schabi.newpipe.util.VideoSegment; import org.schabi.newpipe.util.VideoSegment;
import org.schabi.newpipe.util.external_communication.KoreUtils; import org.schabi.newpipe.util.external_communication.KoreUtils;
import org.schabi.newpipe.util.ListHelper; import org.schabi.newpipe.util.ListHelper;
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.PermissionHelper; import org.schabi.newpipe.util.PermissionHelper;
import org.schabi.newpipe.util.external_communication.ShareUtils; import org.schabi.newpipe.util.PicassoHelper;
import org.schabi.newpipe.util.SponsorBlockUtils; import org.schabi.newpipe.util.SponsorBlockUtils;
import org.schabi.newpipe.util.external_communication.ShareUtils;
import org.schabi.newpipe.util.ThemeHelper; import org.schabi.newpipe.util.ThemeHelper;
import java.util.ArrayList; import java.util.ArrayList;
@ -154,6 +152,8 @@ 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,7 +206,7 @@ public final class VideoDetailFragment
@Nullable @Nullable
private MainPlayer playerService; private MainPlayer playerService;
private Player player; private Player player;
private PlayerHolder playerHolder = PlayerHolder.getInstance(); private final PlayerHolder playerHolder = PlayerHolder.getInstance();
/*////////////////////////////////////////////////////////////////////////// /*//////////////////////////////////////////////////////////////////////////
// Service management // Service management
@ -225,7 +225,7 @@ public final class VideoDetailFragment
return; return;
} }
if (isLandscape()) { if (DeviceUtils.isLandscape(requireContext())) {
// If the video is playing but orientation changed // If the video is playing but orientation changed
// let's make the video in fullscreen again // let's make the video in fullscreen again
checkLandscape(); checkLandscape();
@ -246,7 +246,7 @@ public final class VideoDetailFragment
&& isAutoplayEnabled() && isAutoplayEnabled()
&& player.getParentActivity() == null)) { && player.getParentActivity() == null)) {
autoPlayEnabled = true; // forcefully start playing autoPlayEnabled = true; // forcefully start playing
openVideoPlayer(); openVideoPlayerAutoFullscreen();
} }
} }
@ -431,7 +431,7 @@ public final class VideoDetailFragment
showRelatedItems = sharedPreferences.getBoolean(key, true); showRelatedItems = sharedPreferences.getBoolean(key, true);
tabSettingsChanged = true; tabSettingsChanged = true;
} else if (key.equals(getString(R.string.show_description_key))) { } else if (key.equals(getString(R.string.show_description_key))) {
showComments = sharedPreferences.getBoolean(key, true); showDescription = sharedPreferences.getBoolean(key, true);
tabSettingsChanged = true; tabSettingsChanged = true;
} }
} }
@ -507,7 +507,7 @@ public final class VideoDetailFragment
break; break;
case R.id.detail_thumbnail_root_layout: case R.id.detail_thumbnail_root_layout:
autoPlayEnabled = true; // forcefully start playing autoPlayEnabled = true; // forcefully start playing
openVideoPlayer(); openVideoPlayerAutoFullscreen();
break; break;
case R.id.detail_title_root_layout: case R.id.detail_title_root_layout:
toggleTitleAndSecondaryControls(); toggleTitleAndSecondaryControls();
@ -524,7 +524,7 @@ public final class VideoDetailFragment
showSystemUi(); showSystemUi();
} else { } else {
autoPlayEnabled = true; // forcefully start playing autoPlayEnabled = true; // forcefully start playing
openVideoPlayer(); openVideoPlayer(false);
} }
setOverlayPlayPauseImage(isPlayerAvailable() && player.isPlaying()); setOverlayPlayPauseImage(isPlayerAvailable() && player.isPlaying());
@ -694,33 +694,24 @@ public final class VideoDetailFragment
} }
private void initThumbnailViews(@NonNull final StreamInfo info) { private void initThumbnailViews(@NonNull final StreamInfo info) {
binding.detailThumbnailImageView.setImageResource(R.drawable.dummy_thumbnail_dark); PicassoHelper.loadThumbnail(info.getThumbnailUrl()).tag(PICASSO_VIDEO_DETAILS_TAG)
.into(binding.detailThumbnailImageView, new Callback() {
@Override
public void onSuccess() {
// nothing to do, the image was loaded correctly into the thumbnail
}
if (!isEmpty(info.getThumbnailUrl())) { @Override
final ImageLoadingListener onFailListener = new SimpleImageLoadingListener() { public void onError(final Exception e) {
@Override showSnackBarError(new ErrorInfo(e, UserAction.LOAD_IMAGE,
public void onLoadingFailed(final String imageUri, final View view, info.getThumbnailUrl(), info));
final FailReason failReason) { }
showSnackBarError(new ErrorInfo(failReason.getCause(), UserAction.LOAD_IMAGE, });
imageUri, info));
}
};
IMAGE_LOADER.displayImage(info.getThumbnailUrl(), binding.detailThumbnailImageView, PicassoHelper.loadAvatar(info.getSubChannelAvatarUrl()).tag(PICASSO_VIDEO_DETAILS_TAG)
ImageDisplayConstants.DISPLAY_THUMBNAIL_OPTIONS, onFailListener); .into(binding.detailSubChannelThumbnailView);
} PicassoHelper.loadAvatar(info.getUploaderAvatarUrl()).tag(PICASSO_VIDEO_DETAILS_TAG)
.into(binding.detailUploaderThumbnailView);
if (!isEmpty(info.getSubChannelAvatarUrl())) {
IMAGE_LOADER.displayImage(info.getSubChannelAvatarUrl(),
binding.detailSubChannelThumbnailView,
ImageDisplayConstants.DISPLAY_AVATAR_OPTIONS);
}
if (!isEmpty(info.getUploaderAvatarUrl())) {
IMAGE_LOADER.displayImage(info.getUploaderAvatarUrl(),
binding.detailUploaderThumbnailView,
ImageDisplayConstants.DISPLAY_AVATAR_OPTIONS);
}
} }
/*////////////////////////////////////////////////////////////////////////// /*//////////////////////////////////////////////////////////////////////////
@ -759,27 +750,26 @@ public final class VideoDetailFragment
&& player.getPlayQueue() != null && player.getPlayQueue() != null
&& player.videoPlayerSelected() && player.videoPlayerSelected()
&& player.getPlayQueue().previous()) { && player.getPlayQueue().previous()) {
return true; return true; // no code here, as previous() was used in the if
} }
// That means that we are on the start of the stack, // That means that we are on the start of the stack,
// return false to let the MainActivity handle the onBack
if (stack.size() <= 1) { if (stack.size() <= 1) {
restoreDefaultOrientation(); restoreDefaultOrientation();
return false; // let MainActivity handle the onBack (e.g. to minimize the mini player)
return false;
} }
// Remove top // Remove top
stack.pop(); stack.pop();
// Get stack item from the new top // Get stack item from the new top
assert stack.peek() != null; setupFromHistoryItem(Objects.requireNonNull(stack.peek()));
setupFromHistoryItem(stack.peek());
return true; return true;
} }
private void setupFromHistoryItem(final StackItem item) { private void setupFromHistoryItem(final StackItem item) {
setAutoPlay(false); setAutoPlay(false);
hideMainPlayer(); hideMainPlayerOnLoadingNewStream();
setInitialData(item.getServiceId(), item.getUrl(), setInitialData(item.getServiceId(), item.getUrl(),
item.getTitle() == null ? "" : item.getTitle(), item.getPlayQueue()); item.getTitle() == null ? "" : item.getTitle(), item.getPlayQueue());
@ -899,7 +889,7 @@ public final class VideoDetailFragment
.observeOn(AndroidSchedulers.mainThread()) .observeOn(AndroidSchedulers.mainThread())
.subscribe(result -> { .subscribe(result -> {
isLoading.set(false); isLoading.set(false);
hideMainPlayer(); hideMainPlayerOnLoadingNewStream();
if (result.getAgeLimit() != NO_AGE_LIMIT && !prefs.getBoolean( if (result.getAgeLimit() != NO_AGE_LIMIT && !prefs.getBoolean(
getString(R.string.show_age_restricted_content), false)) { getString(R.string.show_age_restricted_content), false)) {
hideAgeRestrictedContent(); hideAgeRestrictedContent();
@ -914,8 +904,9 @@ public final class VideoDetailFragment
stack.push(new StackItem(serviceId, url, title, playQueue)); stack.push(new StackItem(serviceId, url, title, playQueue));
} }
} }
if (isAutoplayEnabled()) { if (isAutoplayEnabled()) {
openVideoPlayer(); openVideoPlayerAutoFullscreen();
} }
} }
}, throwable -> showError(new ErrorInfo(throwable, UserAction.REQUESTED_STREAM, }, throwable -> showError(new ErrorInfo(throwable, UserAction.REQUESTED_STREAM,
@ -1120,7 +1111,29 @@ public final class VideoDetailFragment
} }
} }
public void openVideoPlayer() { /**
* Opens the video player, in fullscreen if needed. In order to open fullscreen, the activity
* is toggled to landscape orientation (which will then cause fullscreen mode).
*
* @param directlyFullscreenIfApplicable whether to open fullscreen if we are not already
* in landscape and screen orientation is locked
*/
public void openVideoPlayer(final boolean directlyFullscreenIfApplicable) {
if (directlyFullscreenIfApplicable
&& !DeviceUtils.isLandscape(requireContext())
&& PlayerHelper.globalScreenOrientationLocked(requireContext())) {
// Make sure the bottom sheet turns out expanded. When this code kicks in the bottom
// sheet could not have fully expanded yet, and thus be in the STATE_SETTLING state.
// When the activity is rotated, and its state is saved and then restored, the bottom
// sheet would forget what it was doing, since even if STATE_SETTLING is restored, it
// doesn't tell which state it was settling to, and thus the bottom sheet settles to
// STATE_COLLAPSED. This can be solved by manually setting the state that will be
// restored (i.e. bottomSheetState) to STATE_EXPANDED.
bottomSheetState = BottomSheetBehavior.STATE_EXPANDED;
// toggle landscape in order to open directly in fullscreen
onScreenRotationButtonClicked();
}
if (PreferenceManager.getDefaultSharedPreferences(activity) if (PreferenceManager.getDefaultSharedPreferences(activity)
.getBoolean(this.getString(R.string.use_external_video_player_key), false)) { .getBoolean(this.getString(R.string.use_external_video_player_key), false)) {
showExternalPlaybackDialog(); showExternalPlaybackDialog();
@ -1129,6 +1142,18 @@ public final class VideoDetailFragment
} }
} }
/**
* If the option to start directly fullscreen is enabled, calls
* {@link #openVideoPlayer(boolean)} with {@code directlyFullscreenIfApplicable = true}, so that
* if the user is not already in landscape and he has screen orientation locked the activity
* rotates and fullscreen starts. Otherwise, if the option to start directly fullscreen is
* disabled, calls {@link #openVideoPlayer(boolean)} with {@code directlyFullscreenIfApplicable
* = false}, hence preventing it from going directly fullscreen.
*/
public void openVideoPlayerAutoFullscreen() {
openVideoPlayer(PlayerHelper.isStartMainPlayerFullscreenEnabled(requireContext()));
}
private void openNormalBackgroundPlayer(final boolean append) { private void openNormalBackgroundPlayer(final boolean append) {
// See UI changes while remote playQueue changes // See UI changes while remote playQueue changes
if (!isPlayerAvailable()) { if (!isPlayerAvailable()) {
@ -1162,12 +1187,19 @@ public final class VideoDetailFragment
} }
addVideoPlayerView(); addVideoPlayerView();
final Intent playerIntent = NavigationHelper final Intent playerIntent = NavigationHelper.getPlayerIntent(requireContext(),
.getPlayerIntent(requireContext(), MainPlayer.class, queue, true, autoPlayEnabled); MainPlayer.class, queue, true, autoPlayEnabled);
ContextCompat.startForegroundService(activity, playerIntent); ContextCompat.startForegroundService(activity, playerIntent);
} }
private void hideMainPlayer() { /**
* When the video detail fragment is already showing details for a video and the user opens a
* new one, the video detail fragment changes all of its old data to the new stream, so if there
* is a video player currently open it should be hidden. This method does exactly that. If
* autoplay is enabled, the underlying player is not stopped completely, since it is going to
* be reused in a few milliseconds and the flickering would be annoying.
*/
private void hideMainPlayerOnLoadingNewStream() {
if (!isPlayerServiceAvailable() if (!isPlayerServiceAvailable()
|| playerService.getView() == null || playerService.getView() == null
|| !player.videoPlayerSelected()) { || !player.videoPlayerSelected()) {
@ -1175,8 +1207,12 @@ public final class VideoDetailFragment
} }
removeVideoPlayerView(); removeVideoPlayerView();
playerService.stop(isAutoplayEnabled()); if (isAutoplayEnabled()) {
playerService.getView().setVisibility(View.GONE); playerService.stopForImmediateReusing();
playerService.getView().setVisibility(View.GONE);
} else {
playerHolder.stopService();
}
} }
private PlayQueue setupPlayQueueForIntent(final boolean append) { private PlayQueue setupPlayQueueForIntent(final boolean append) {
@ -1269,7 +1305,7 @@ public final class VideoDetailFragment
final DisplayMetrics metrics = getResources().getDisplayMetrics(); final DisplayMetrics metrics = getResources().getDisplayMetrics();
if (getView() != null) { if (getView() != null) {
final int height = (isInMultiWindow() final int height = (DeviceUtils.isInMultiWindow(activity)
? requireView() ? requireView()
: activity.getWindow().getDecorView()).getHeight(); : activity.getWindow().getDecorView()).getHeight();
setHeightThumbnail(height, metrics); setHeightThumbnail(height, metrics);
@ -1292,7 +1328,7 @@ public final class VideoDetailFragment
requireView().getViewTreeObserver().removeOnPreDrawListener(preDrawListener); requireView().getViewTreeObserver().removeOnPreDrawListener(preDrawListener);
if (isPlayerAvailable() && player.isFullscreen()) { if (isPlayerAvailable() && player.isFullscreen()) {
final int height = (isInMultiWindow() final int height = (DeviceUtils.isInMultiWindow(activity)
? requireView() ? requireView()
: activity.getWindow().getDecorView()).getHeight(); : activity.getWindow().getDecorView()).getHeight();
// Height is zero when the view is not yet displayed like after orientation change // Height is zero when the view is not yet displayed like after orientation change
@ -1403,17 +1439,15 @@ public final class VideoDetailFragment
//////////////////////////////////////////////////////////////////////////*/ //////////////////////////////////////////////////////////////////////////*/
private void restoreDefaultOrientation() { private void restoreDefaultOrientation() {
if (!isPlayerAvailable() || !player.videoPlayerSelected() || activity == null) { if (isPlayerAvailable() && player.videoPlayerSelected()) {
return; toggleFullscreenIfInFullscreenMode();
} }
toggleFullscreenIfInFullscreenMode();
// This will show systemUI and pause the player. // This will show systemUI and pause the player.
// User can tap on Play button and video will be in fullscreen mode again // User can tap on Play button and video will be in fullscreen mode again
// Note for tablet: trying to avoid orientation changes since it's not easy // Note for tablet: trying to avoid orientation changes since it's not easy
// to physically rotate the tablet every time // to physically rotate the tablet every time
if (!DeviceUtils.isTablet(activity)) { if (activity != null && !DeviceUtils.isTablet(activity)) {
activity.setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED); activity.setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED);
} }
} }
@ -1454,8 +1488,7 @@ public final class VideoDetailFragment
} }
} }
IMAGE_LOADER.cancelDisplayTask(binding.detailThumbnailImageView); PicassoHelper.cancelTag(PICASSO_VIDEO_DETAILS_TAG);
IMAGE_LOADER.cancelDisplayTask(binding.detailSubChannelThumbnailView);
binding.detailThumbnailImageView.setImageBitmap(null); binding.detailThumbnailImageView.setImageBitmap(null);
binding.detailSubChannelThumbnailView.setImageBitmap(null); binding.detailSubChannelThumbnailView.setImageBitmap(null);
} }
@ -1845,7 +1878,7 @@ public final class VideoDetailFragment
|| error.type == ExoPlaybackException.TYPE_UNEXPECTED) { || error.type == ExoPlaybackException.TYPE_UNEXPECTED) {
// Properly exit from fullscreen // Properly exit from fullscreen
toggleFullscreenIfInFullscreenMode(); toggleFullscreenIfInFullscreenMode();
hideMainPlayer(); hideMainPlayerOnLoadingNewStream();
} }
} }
@ -1901,13 +1934,14 @@ public final class VideoDetailFragment
// from landscape to portrait every time. // from landscape to portrait every time.
// Just turn on fullscreen mode in landscape orientation // Just turn on fullscreen mode in landscape orientation
// or portrait & unlocked global orientation // or portrait & unlocked global orientation
final boolean isLandscape = DeviceUtils.isLandscape(requireContext());
if (DeviceUtils.isTablet(activity) if (DeviceUtils.isTablet(activity)
&& (!globalScreenOrientationLocked(activity) || isLandscape())) { && (!globalScreenOrientationLocked(activity) || isLandscape)) {
player.toggleFullscreen(); player.toggleFullscreen();
return; return;
} }
final int newOrientation = isLandscape() final int newOrientation = isLandscape
? ActivityInfo.SCREEN_ORIENTATION_PORTRAIT ? ActivityInfo.SCREEN_ORIENTATION_PORTRAIT
: ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE; : ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE;
@ -1979,15 +2013,17 @@ public final class VideoDetailFragment
| View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
| View.SYSTEM_UI_FLAG_HIDE_NAVIGATION | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION
| View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY; | View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY;
// In multiWindow mode status bar is not transparent for devices with cutout // In multiWindow mode status bar is not transparent for devices with cutout
// if I include this flag. So without it is better in this case // if I include this flag. So without it is better in this case
if (!isInMultiWindow()) { final boolean isInMultiWindow = DeviceUtils.isInMultiWindow(activity);
if (!isInMultiWindow) {
visibility |= View.SYSTEM_UI_FLAG_FULLSCREEN; visibility |= View.SYSTEM_UI_FLAG_FULLSCREEN;
} }
activity.getWindow().getDecorView().setSystemUiVisibility(visibility); activity.getWindow().getDecorView().setSystemUiVisibility(visibility);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP
&& (isInMultiWindow() || (isPlayerAvailable() && player.isFullscreen()))) { && (isInMultiWindow || (isPlayerAvailable() && player.isFullscreen()))) {
activity.getWindow().setStatusBarColor(Color.TRANSPARENT); activity.getWindow().setStatusBarColor(Color.TRANSPARENT);
activity.getWindow().setNavigationBarColor(Color.TRANSPARENT); activity.getWindow().setNavigationBarColor(Color.TRANSPARENT);
} }
@ -2059,15 +2095,6 @@ public final class VideoDetailFragment
} }
} }
private boolean isLandscape() {
return getResources().getDisplayMetrics().heightPixels < getResources()
.getDisplayMetrics().widthPixels;
}
private boolean isInMultiWindow() {
return Build.VERSION.SDK_INT >= Build.VERSION_CODES.N && activity.isInMultiWindowMode();
}
/* /*
* Means that the player fragment was swiped away via BottomSheetLayout * Means that the player fragment was swiped away via BottomSheetLayout
* and is empty but ready for any new actions. See cleanUp() * and is empty but ready for any new actions. See cleanUp()
@ -2107,8 +2134,8 @@ public final class VideoDetailFragment
private void showClearingQueueConfirmation(final Runnable onAllow) { private void showClearingQueueConfirmation(final Runnable onAllow) {
new AlertDialog.Builder(activity) new AlertDialog.Builder(activity)
.setTitle(R.string.clear_queue_confirmation_description) .setTitle(R.string.clear_queue_confirmation_description)
.setNegativeButton(android.R.string.cancel, null) .setNegativeButton(R.string.cancel, null)
.setPositiveButton(android.R.string.yes, (dialog, which) -> { .setPositiveButton(R.string.ok, (dialog, which) -> {
onAllow.run(); onAllow.run();
dialog.dismiss(); dialog.dismiss();
}).show(); }).show();
@ -2123,7 +2150,7 @@ public final class VideoDetailFragment
resolutions[i] = sortedVideoStreams.get(i).getResolution(); resolutions[i] = sortedVideoStreams.get(i).getResolution();
} }
final AlertDialog.Builder builder = new AlertDialog.Builder(activity) final AlertDialog.Builder builder = new AlertDialog.Builder(activity)
.setNegativeButton(android.R.string.cancel, null) .setNegativeButton(R.string.cancel, null)
.setNeutralButton(R.string.open_in_browser, (dialog, i) -> .setNeutralButton(R.string.open_in_browser, (dialog, i) ->
ShareUtils.openUrlInBrowser(requireActivity(), url) ShareUtils.openUrlInBrowser(requireActivity(), url)
); );
@ -2250,7 +2277,7 @@ public final class VideoDetailFragment
setOverlayElementsClickable(false); setOverlayElementsClickable(false);
hideSystemUiIfNeeded(); hideSystemUiIfNeeded();
// Conditions when the player should be expanded to fullscreen // Conditions when the player should be expanded to fullscreen
if (isLandscape() if (DeviceUtils.isLandscape(requireContext())
&& isPlayerAvailable() && isPlayerAvailable()
&& player.isPlaying() && player.isPlaying()
&& !player.isFullscreen() && !player.isFullscreen()
@ -2305,10 +2332,8 @@ 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.setImageResource(R.drawable.dummy_thumbnail_dark); binding.overlayThumbnail.setImageResource(R.drawable.dummy_thumbnail_dark);
if (!isEmpty(thumbnailUrl)) { PicassoHelper.loadThumbnail(thumbnailUrl).tag(PICASSO_VIDEO_DETAILS_TAG)
IMAGE_LOADER.displayImage(thumbnailUrl, binding.overlayThumbnail, .into(binding.overlayThumbnail);
ImageDisplayConstants.DISPLAY_THUMBNAIL_OPTIONS, null);
}
} }
private void setOverlayPlayPauseImage(final boolean playerIsPlaying) { private void setOverlayPlayPauseImage(final boolean playerIsPlaying) {

View file

@ -40,10 +40,10 @@ import org.schabi.newpipe.local.subscription.SubscriptionManager;
import org.schabi.newpipe.player.playqueue.ChannelPlayQueue; import org.schabi.newpipe.player.playqueue.ChannelPlayQueue;
import org.schabi.newpipe.player.playqueue.PlayQueue; import org.schabi.newpipe.player.playqueue.PlayQueue;
import org.schabi.newpipe.util.ExtractorHelper; import org.schabi.newpipe.util.ExtractorHelper;
import org.schabi.newpipe.util.ImageDisplayConstants;
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.PicassoHelper;
import org.schabi.newpipe.util.ThemeHelper; import org.schabi.newpipe.util.ThemeHelper;
import java.util.ArrayList; import java.util.ArrayList;
@ -66,7 +66,10 @@ import static org.schabi.newpipe.ktx.ViewUtils.animateBackgroundColor;
public class ChannelFragment extends BaseListInfoFragment<ChannelInfo> public class ChannelFragment extends BaseListInfoFragment<ChannelInfo>
implements View.OnClickListener { implements View.OnClickListener {
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";
private final CompositeDisposable disposables = new CompositeDisposable(); private final CompositeDisposable disposables = new CompositeDisposable();
private Disposable subscribeButtonMonitor; private Disposable subscribeButtonMonitor;
@ -421,10 +424,7 @@ public class ChannelFragment extends BaseListInfoFragment<ChannelInfo>
@Override @Override
public void showLoading() { public void showLoading() {
super.showLoading(); super.showLoading();
PicassoHelper.cancelTag(PICASSO_CHANNEL_TAG);
IMAGE_LOADER.cancelDisplayTask(headerBinding.channelBannerImage);
IMAGE_LOADER.cancelDisplayTask(headerBinding.channelAvatarView);
IMAGE_LOADER.cancelDisplayTask(headerBinding.subChannelAvatarView);
animate(headerBinding.channelSubscribeButton, false, 100); animate(headerBinding.channelSubscribeButton, false, 100);
} }
@ -433,13 +433,12 @@ public class ChannelFragment extends BaseListInfoFragment<ChannelInfo>
super.handleResult(result); super.handleResult(result);
headerBinding.getRoot().setVisibility(View.VISIBLE); headerBinding.getRoot().setVisibility(View.VISIBLE);
IMAGE_LOADER.displayImage(result.getBannerUrl(), headerBinding.channelBannerImage, PicassoHelper.loadBanner(result.getBannerUrl()).tag(PICASSO_CHANNEL_TAG)
ImageDisplayConstants.DISPLAY_BANNER_OPTIONS); .into(headerBinding.channelBannerImage);
IMAGE_LOADER.displayImage(result.getAvatarUrl(), headerBinding.channelAvatarView, PicassoHelper.loadAvatar(result.getAvatarUrl()).tag(PICASSO_CHANNEL_TAG)
ImageDisplayConstants.DISPLAY_AVATAR_OPTIONS); .into(headerBinding.channelAvatarView);
IMAGE_LOADER.displayImage(result.getParentChannelAvatarUrl(), PicassoHelper.loadAvatar(result.getParentChannelAvatarUrl()).tag(PICASSO_CHANNEL_TAG)
headerBinding.subChannelAvatarView, .into(headerBinding.subChannelAvatarView);
ImageDisplayConstants.DISPLAY_AVATAR_OPTIONS);
headerBinding.channelSubscriberView.setVisibility(View.VISIBLE); headerBinding.channelSubscriberView.setVisibility(View.VISIBLE);
if (result.getSubscriberCount() >= 0) { if (result.getSubscriberCount() >= 0) {

View file

@ -41,7 +41,7 @@ import org.schabi.newpipe.player.helper.PlayerHolder;
import org.schabi.newpipe.player.playqueue.PlayQueue; import org.schabi.newpipe.player.playqueue.PlayQueue;
import org.schabi.newpipe.player.playqueue.PlaylistPlayQueue; import org.schabi.newpipe.player.playqueue.PlaylistPlayQueue;
import org.schabi.newpipe.util.ExtractorHelper; import org.schabi.newpipe.util.ExtractorHelper;
import org.schabi.newpipe.util.ImageDisplayConstants; import org.schabi.newpipe.util.PicassoHelper;
import org.schabi.newpipe.util.external_communication.KoreUtils; import org.schabi.newpipe.util.external_communication.KoreUtils;
import org.schabi.newpipe.util.Localization; import org.schabi.newpipe.util.Localization;
import org.schabi.newpipe.util.NavigationHelper; import org.schabi.newpipe.util.NavigationHelper;
@ -64,12 +64,16 @@ import static org.schabi.newpipe.ktx.ViewUtils.animate;
import static org.schabi.newpipe.ktx.ViewUtils.animateHideRecyclerViewAllowingScrolling; import static org.schabi.newpipe.ktx.ViewUtils.animateHideRecyclerViewAllowingScrolling;
public class PlaylistFragment extends BaseListInfoFragment<PlaylistInfo> { public class PlaylistFragment extends BaseListInfoFragment<PlaylistInfo> {
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;
private RemotePlaylistManager remotePlaylistManager; private RemotePlaylistManager remotePlaylistManager;
private PlaylistRemoteEntity playlistEntity; private PlaylistRemoteEntity playlistEntity;
/*////////////////////////////////////////////////////////////////////////// /*//////////////////////////////////////////////////////////////////////////
// Views // Views
//////////////////////////////////////////////////////////////////////////*/ //////////////////////////////////////////////////////////////////////////*/
@ -274,7 +278,7 @@ public class PlaylistFragment extends BaseListInfoFragment<PlaylistInfo> {
animate(headerBinding.getRoot(), false, 200); animate(headerBinding.getRoot(), false, 200);
animateHideRecyclerViewAllowingScrolling(itemsList); animateHideRecyclerViewAllowingScrolling(itemsList);
IMAGE_LOADER.cancelDisplayTask(headerBinding.uploaderAvatarView); PicassoHelper.cancelTag(PICASSO_PLAYLIST_TAG);
animate(headerBinding.uploaderLayout, false, 200); animate(headerBinding.uploaderLayout, false, 200);
} }
@ -317,8 +321,8 @@ public class PlaylistFragment extends BaseListInfoFragment<PlaylistInfo> {
R.drawable.ic_radio) R.drawable.ic_radio)
); );
} else { } else {
IMAGE_LOADER.displayImage(avatarUrl, headerBinding.uploaderAvatarView, PicassoHelper.loadAvatar(avatarUrl).tag(PICASSO_PLAYLIST_TAG)
ImageDisplayConstants.DISPLAY_AVATAR_OPTIONS); .into(headerBinding.uploaderAvatarView);
} }
headerBinding.playlistStreamCount.setText(Localization headerBinding.playlistStreamCount.setText(Localization

View file

@ -57,6 +57,7 @@ import org.schabi.newpipe.fragments.list.BaseListFragment;
import org.schabi.newpipe.ktx.AnimationType; import org.schabi.newpipe.ktx.AnimationType;
import org.schabi.newpipe.ktx.ExceptionUtils; import org.schabi.newpipe.ktx.ExceptionUtils;
import org.schabi.newpipe.local.history.HistoryRecordManager; import org.schabi.newpipe.local.history.HistoryRecordManager;
import org.schabi.newpipe.settings.NewPipeSettings;
import org.schabi.newpipe.util.Constants; import org.schabi.newpipe.util.Constants;
import org.schabi.newpipe.util.DeviceUtils; import org.schabi.newpipe.util.DeviceUtils;
import org.schabi.newpipe.util.ExtractorHelper; import org.schabi.newpipe.util.ExtractorHelper;
@ -65,16 +66,19 @@ import org.schabi.newpipe.util.ServiceHelper;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap; import java.util.HashMap;
import java.util.HashSet;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Queue; import java.util.Queue;
import java.util.Set;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import icepick.State; import icepick.State;
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.Observable; import io.reactivex.rxjava3.core.Observable;
import io.reactivex.rxjava3.core.Single;
import io.reactivex.rxjava3.disposables.CompositeDisposable; import io.reactivex.rxjava3.disposables.CompositeDisposable;
import io.reactivex.rxjava3.disposables.Disposable; import io.reactivex.rxjava3.disposables.Disposable;
import io.reactivex.rxjava3.schedulers.Schedulers; import io.reactivex.rxjava3.schedulers.Schedulers;
@ -143,7 +147,8 @@ public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.I
@Nullable private Map<Integer, String> menuItemToFilterName = null; @Nullable private Map<Integer, String> menuItemToFilterName = null;
private StreamingService service; private StreamingService service;
private Page nextPage; private Page nextPage;
private boolean isSuggestionsEnabled = true; private boolean showLocalSuggestions = true;
private boolean showRemoteSuggestions = true;
private Disposable searchDisposable; private Disposable searchDisposable;
private Disposable suggestionDisposable; private Disposable suggestionDisposable;
@ -194,26 +199,14 @@ public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.I
public void onAttach(@NonNull final Context context) { public void onAttach(@NonNull final Context context) {
super.onAttach(context); super.onAttach(context);
final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(activity);
showLocalSuggestions = NewPipeSettings.showLocalSearchSuggestions(activity, prefs);
showRemoteSuggestions = NewPipeSettings.showRemoteSearchSuggestions(activity, prefs);
suggestionListAdapter = new SuggestionListAdapter(activity); suggestionListAdapter = new SuggestionListAdapter(activity);
final SharedPreferences preferences
= PreferenceManager.getDefaultSharedPreferences(activity);
final boolean isSearchHistoryEnabled = preferences
.getBoolean(getString(R.string.enable_search_history_key), true);
suggestionListAdapter.setShowSuggestionHistory(isSearchHistoryEnabled);
historyRecordManager = new HistoryRecordManager(context); historyRecordManager = new HistoryRecordManager(context);
} }
@Override
public void onCreate(final Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
final SharedPreferences preferences
= PreferenceManager.getDefaultSharedPreferences(activity);
isSuggestionsEnabled = preferences
.getBoolean(getString(R.string.show_search_suggestions_key), true);
}
@Override @Override
public View onCreateView(final LayoutInflater inflater, @Nullable final ViewGroup container, public View onCreateView(final LayoutInflater inflater, @Nullable final ViewGroup container,
@Nullable final Bundle savedInstanceState) { @Nullable final Bundle savedInstanceState) {
@ -222,6 +215,7 @@ public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.I
@Override @Override
public void onViewCreated(@NonNull final View rootView, final Bundle savedInstanceState) { public void onViewCreated(@NonNull final View rootView, final Bundle savedInstanceState) {
searchBinding = FragmentSearchBinding.bind(rootView);
super.onViewCreated(rootView, savedInstanceState); super.onViewCreated(rootView, savedInstanceState);
showSearchOnStart(); showSearchOnStart();
initSearchListeners(); initSearchListeners();
@ -348,7 +342,6 @@ public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.I
@Override @Override
protected void initViews(final View rootView, final Bundle savedInstanceState) { protected void initViews(final View rootView, final Bundle savedInstanceState) {
super.initViews(rootView, savedInstanceState); super.initViews(rootView, savedInstanceState);
searchBinding = FragmentSearchBinding.bind(rootView);
searchBinding.suggestionsList.setAdapter(suggestionListAdapter); searchBinding.suggestionsList.setAdapter(suggestionListAdapter);
new ItemTouchHelper(new ItemTouchHelper.Callback() { new ItemTouchHelper(new ItemTouchHelper.Callback() {
@ -554,7 +547,7 @@ public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.I
if (DEBUG) { if (DEBUG) {
Log.d(TAG, "onClick() called with: v = [" + v + "]"); Log.d(TAG, "onClick() called with: v = [" + v + "]");
} }
if (isSuggestionsEnabled && !isErrorPanelVisible()) { if ((showLocalSuggestions || showRemoteSuggestions) && !isErrorPanelVisible()) {
showSuggestionsPanel(); showSuggestionsPanel();
} }
if (DeviceUtils.isTv(getContext())) { if (DeviceUtils.isTv(getContext())) {
@ -567,7 +560,8 @@ public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.I
Log.d(TAG, "onFocusChange() called with: " Log.d(TAG, "onFocusChange() called with: "
+ "v = [" + v + "], hasFocus = [" + hasFocus + "]"); + "v = [" + v + "], hasFocus = [" + hasFocus + "]");
} }
if (isSuggestionsEnabled && hasFocus && !isErrorPanelVisible()) { if ((showLocalSuggestions || showRemoteSuggestions)
&& hasFocus && !isErrorPanelVisible()) {
showSuggestionsPanel(); showSuggestionsPanel();
} }
}); });
@ -743,6 +737,34 @@ public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.I
return false; return false;
} }
private Observable<List<SuggestionItem>> getLocalSuggestionsObservable(
final String query, final int similarQueryLimit) {
return historyRecordManager
.getRelatedSearches(query, similarQueryLimit, 25)
.toObservable()
.map(searchHistoryEntries -> {
final Set<SuggestionItem> result = new HashSet<>(); // remove duplicates
for (final SearchHistoryEntry entry : searchHistoryEntries) {
result.add(new SuggestionItem(true, entry.getSearch()));
}
return new ArrayList<>(result);
});
}
private Observable<List<SuggestionItem>> getRemoteSuggestionsObservable(final String query) {
return ExtractorHelper
.suggestionsFor(serviceId, query)
.toObservable()
.map(strings -> {
final List<SuggestionItem> result = new ArrayList<>();
for (final String entry : strings) {
result.add(new SuggestionItem(false, entry));
}
return result;
});
}
private void initSuggestionObserver() { private void initSuggestionObserver() {
if (DEBUG) { if (DEBUG) {
Log.d(TAG, "initSuggestionObserver() called"); Log.d(TAG, "initSuggestionObserver() called");
@ -753,73 +775,53 @@ public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.I
suggestionDisposable = suggestionPublisher suggestionDisposable = suggestionPublisher
.debounce(SUGGESTIONS_DEBOUNCE, TimeUnit.MILLISECONDS) .debounce(SUGGESTIONS_DEBOUNCE, TimeUnit.MILLISECONDS)
.startWithItem(searchString != null .startWithItem(searchString == null ? "" : searchString)
? searchString
: "")
.filter(ss -> isSuggestionsEnabled)
.switchMap(query -> { .switchMap(query -> {
final Flowable<List<SearchHistoryEntry>> flowable = historyRecordManager // Only show remote suggestions if they are enabled in settings and
.getRelatedSearches(query, 3, 25); // the query length is at least THRESHOLD_NETWORK_SUGGESTION
final Observable<List<SuggestionItem>> local = flowable.toObservable() final boolean shallShowRemoteSuggestionsNow = showRemoteSuggestions
.map(searchHistoryEntries -> { && query.length() >= THRESHOLD_NETWORK_SUGGESTION;
final List<SuggestionItem> result = new ArrayList<>();
for (final SearchHistoryEntry entry : searchHistoryEntries) {
result.add(new SuggestionItem(true, entry.getSearch()));
}
return result;
});
if (query.length() < THRESHOLD_NETWORK_SUGGESTION) { if (showLocalSuggestions && shallShowRemoteSuggestionsNow) {
// Only pass through if the query length return Observable.zip(
// is equal or greater than THRESHOLD_NETWORK_SUGGESTION getLocalSuggestionsObservable(query, 3),
return local.materialize(); getRemoteSuggestionsObservable(query),
(local, remote) -> {
remote.removeIf(remoteItem -> local.stream().anyMatch(
localItem -> localItem.equals(remoteItem)));
local.addAll(remote);
return local;
})
.materialize();
} else if (showLocalSuggestions) {
return getLocalSuggestionsObservable(query, 25)
.materialize();
} else if (shallShowRemoteSuggestionsNow) {
return getRemoteSuggestionsObservable(query)
.materialize();
} else {
return Single.fromCallable(Collections::<SuggestionItem>emptyList)
.toObservable()
.materialize();
} }
final Observable<List<SuggestionItem>> network = ExtractorHelper
.suggestionsFor(serviceId, query)
.onErrorReturn(throwable -> {
if (!ExceptionUtils.isNetworkRelated(throwable)) {
showSnackBarError(new ErrorInfo(throwable,
UserAction.GET_SUGGESTIONS, searchString, serviceId));
}
return new ArrayList<>();
})
.toObservable()
.map(strings -> {
final List<SuggestionItem> result = new ArrayList<>();
for (final String entry : strings) {
result.add(new SuggestionItem(false, entry));
}
return result;
});
return Observable.zip(local, network, (localResult, networkResult) -> {
final List<SuggestionItem> result = new ArrayList<>();
if (localResult.size() > 0) {
result.addAll(localResult);
}
// Remove duplicates
networkResult.removeIf(networkItem ->
localResult.stream().anyMatch(localItem ->
localItem.query.equals(networkItem.query)));
if (networkResult.size() > 0) {
result.addAll(networkResult);
}
return result;
}).materialize();
}) })
.subscribeOn(Schedulers.io()) .subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread()) .observeOn(AndroidSchedulers.mainThread())
.subscribe(listNotification -> { .subscribe(
if (listNotification.isOnNext()) { listNotification -> {
handleSuggestions(listNotification.getValue()); if (listNotification.isOnNext()) {
} else if (listNotification.isOnError()) { if (listNotification.getValue() != null) {
showError(new ErrorInfo(listNotification.getError(), handleSuggestions(listNotification.getValue());
UserAction.GET_SUGGESTIONS, searchString, serviceId)); }
} } else if (listNotification.isOnError()
}); && listNotification.getError() != null
&& !ExceptionUtils.isInterruptedCaused(
listNotification.getError())) {
showSnackBarError(new ErrorInfo(listNotification.getError(),
UserAction.GET_SUGGESTIONS, searchString, serviceId));
}
}, throwable -> showSnackBarError(new ErrorInfo(
throwable, UserAction.GET_SUGGESTIONS, searchString, serviceId)));
} }
@Override @Override

View file

@ -1,5 +1,7 @@
package org.schabi.newpipe.fragments.list.search; package org.schabi.newpipe.fragments.list.search;
import androidx.annotation.NonNull;
public class SuggestionItem { public class SuggestionItem {
final boolean fromHistory; final boolean fromHistory;
public final String query; public final String query;
@ -9,6 +11,20 @@ public class SuggestionItem {
this.query = query; this.query = query;
} }
@Override
public boolean equals(final Object o) {
if (o instanceof SuggestionItem) {
return query.equals(((SuggestionItem) o).query);
}
return false;
}
@Override
public int hashCode() {
return query.hashCode();
}
@NonNull
@Override @Override
public String toString() { public String toString() {
return "[" + fromHistory + "" + query + "]"; return "[" + fromHistory + "" + query + "]";

View file

@ -19,7 +19,6 @@ public class SuggestionListAdapter
private final ArrayList<SuggestionItem> items = new ArrayList<>(); private final ArrayList<SuggestionItem> items = new ArrayList<>();
private final Context context; private final Context context;
private OnSuggestionItemSelected listener; private OnSuggestionItemSelected listener;
private boolean showSuggestionHistory = true;
public SuggestionListAdapter(final Context context) { public SuggestionListAdapter(final Context context) {
this.context = context; this.context = context;
@ -27,16 +26,7 @@ public class SuggestionListAdapter
public void setItems(final List<SuggestionItem> items) { public void setItems(final List<SuggestionItem> items) {
this.items.clear(); this.items.clear();
if (showSuggestionHistory) { this.items.addAll(items);
this.items.addAll(items);
} else {
// remove history items if history is disabled
for (final SuggestionItem item : items) {
if (!item.fromHistory) {
this.items.add(item);
}
}
}
notifyDataSetChanged(); notifyDataSetChanged();
} }
@ -44,10 +34,6 @@ public class SuggestionListAdapter
this.listener = listener; this.listener = listener;
} }
public void setShowSuggestionHistory(final boolean v) {
showSuggestionHistory = v;
}
@Override @Override
public SuggestionItemHolder onCreateViewHolder(final ViewGroup parent, final int viewType) { public SuggestionItemHolder onCreateViewHolder(final ViewGroup parent, final int viewType) {
return new SuggestionItemHolder(LayoutInflater.from(context) return new SuggestionItemHolder(LayoutInflater.from(context)

View file

@ -6,8 +6,6 @@ import android.view.ViewGroup;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import com.nostra13.universalimageloader.core.ImageLoader;
import org.schabi.newpipe.extractor.InfoItem; import org.schabi.newpipe.extractor.InfoItem;
import org.schabi.newpipe.extractor.channel.ChannelInfoItem; import org.schabi.newpipe.extractor.channel.ChannelInfoItem;
import org.schabi.newpipe.extractor.comments.CommentsInfoItem; import org.schabi.newpipe.extractor.comments.CommentsInfoItem;
@ -51,7 +49,6 @@ import org.schabi.newpipe.util.OnClickGesture;
public class InfoItemBuilder { public class InfoItemBuilder {
private final Context context; private final Context context;
private final ImageLoader imageLoader = ImageLoader.getInstance();
private OnClickGesture<StreamInfoItem> onStreamSelectedListener; private OnClickGesture<StreamInfoItem> onStreamSelectedListener;
private OnClickGesture<ChannelInfoItem> onChannelSelectedListener; private OnClickGesture<ChannelInfoItem> onChannelSelectedListener;
@ -101,10 +98,6 @@ public class InfoItemBuilder {
return context; return context;
} }
public ImageLoader getImageLoader() {
return imageLoader;
}
public OnClickGesture<StreamInfoItem> getOnStreamSelectedListener() { public OnClickGesture<StreamInfoItem> getOnStreamSelectedListener() {
return onStreamSelectedListener; return onStreamSelectedListener;
} }

View file

@ -3,13 +3,12 @@ package org.schabi.newpipe.info_list
import android.view.View import android.view.View
import android.widget.ImageView import android.widget.ImageView
import android.widget.TextView import android.widget.TextView
import com.nostra13.universalimageloader.core.ImageLoader
import com.xwray.groupie.GroupieViewHolder import com.xwray.groupie.GroupieViewHolder
import com.xwray.groupie.Item import com.xwray.groupie.Item
import org.schabi.newpipe.R import org.schabi.newpipe.R
import org.schabi.newpipe.extractor.stream.StreamSegment import org.schabi.newpipe.extractor.stream.StreamSegment
import org.schabi.newpipe.util.ImageDisplayConstants
import org.schabi.newpipe.util.Localization import org.schabi.newpipe.util.Localization
import org.schabi.newpipe.util.PicassoHelper
class StreamSegmentItem( class StreamSegmentItem(
private val item: StreamSegment, private val item: StreamSegment,
@ -24,10 +23,8 @@ class StreamSegmentItem(
override fun bind(viewHolder: GroupieViewHolder, position: Int) { override fun bind(viewHolder: GroupieViewHolder, position: Int) {
item.previewUrl?.let { item.previewUrl?.let {
ImageLoader.getInstance().displayImage( PicassoHelper.loadThumbnail(it)
it, viewHolder.root.findViewById<ImageView>(R.id.previewImage), .into(viewHolder.root.findViewById<ImageView>(R.id.previewImage))
ImageDisplayConstants.DISPLAY_THUMBNAIL_OPTIONS
)
} }
viewHolder.root.findViewById<TextView>(R.id.textViewTitle).text = item.title viewHolder.root.findViewById<TextView>(R.id.textViewTitle).text = item.title
if (item.channelName == null) { if (item.channelName == null) {

View file

@ -8,7 +8,7 @@ import org.schabi.newpipe.extractor.InfoItem;
import org.schabi.newpipe.extractor.channel.ChannelInfoItem; import org.schabi.newpipe.extractor.channel.ChannelInfoItem;
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.ImageDisplayConstants; import org.schabi.newpipe.util.PicassoHelper;
import org.schabi.newpipe.util.Localization; import org.schabi.newpipe.util.Localization;
import de.hdodenhof.circleimageview.CircleImageView; import de.hdodenhof.circleimageview.CircleImageView;
@ -43,10 +43,7 @@ public class ChannelMiniInfoItemHolder extends InfoItemHolder {
itemTitleView.setText(item.getName()); itemTitleView.setText(item.getName());
itemAdditionalDetailView.setText(getDetailLine(item)); itemAdditionalDetailView.setText(getDetailLine(item));
itemBuilder.getImageLoader() PicassoHelper.loadThumbnail(item.getThumbnailUrl()).into(itemThumbnailView);
.displayImage(item.getThumbnailUrl(),
itemThumbnailView,
ImageDisplayConstants.DISPLAY_THUMBNAIL_OPTIONS);
itemView.setOnClickListener(view -> { itemView.setOnClickListener(view -> {
if (itemBuilder.getOnChannelSelectedListener() != null) { if (itemBuilder.getOnChannelSelectedListener() != null) {

View file

@ -1,17 +1,16 @@
package org.schabi.newpipe.info_list.holder; package org.schabi.newpipe.info_list.holder;
import android.content.SharedPreferences;
import android.text.TextUtils; import android.text.TextUtils;
import android.text.method.LinkMovementMethod; import android.text.method.LinkMovementMethod;
import android.text.style.URLSpan; import android.text.style.URLSpan;
import android.text.util.Linkify; import android.text.util.Linkify;
import android.util.Log;
import android.view.View; import android.view.View;
import android.view.ViewGroup; import android.view.ViewGroup;
import android.widget.RelativeLayout; import android.widget.RelativeLayout;
import android.widget.TextView; import android.widget.TextView;
import androidx.appcompat.app.AppCompatActivity; import androidx.appcompat.app.AppCompatActivity;
import androidx.preference.PreferenceManager;
import org.schabi.newpipe.R; import org.schabi.newpipe.R;
import org.schabi.newpipe.error.ErrorActivity; import org.schabi.newpipe.error.ErrorActivity;
@ -21,30 +20,29 @@ 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.CommentTextOnTouchListener; import org.schabi.newpipe.util.CommentTextOnTouchListener;
import org.schabi.newpipe.util.DeviceUtils; import org.schabi.newpipe.util.DeviceUtils;
import org.schabi.newpipe.util.ImageDisplayConstants;
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.TimestampExtractor;
import org.schabi.newpipe.util.external_communication.ShareUtils; import org.schabi.newpipe.util.external_communication.ShareUtils;
import org.schabi.newpipe.util.PicassoHelper;
import java.util.regex.Matcher; import java.util.regex.Matcher;
import java.util.regex.Pattern;
import de.hdodenhof.circleimageview.CircleImageView; import de.hdodenhof.circleimageview.CircleImageView;
public class CommentsMiniInfoItemHolder extends InfoItemHolder { public class CommentsMiniInfoItemHolder extends InfoItemHolder {
private static final String TAG = "CommentsMiniIIHolder";
private static final int COMMENT_DEFAULT_LINES = 2; private static final int COMMENT_DEFAULT_LINES = 2;
private static final int COMMENT_EXPANDED_LINES = 1000; private static final int COMMENT_EXPANDED_LINES = 1000;
private static final Pattern PATTERN = Pattern.compile("(\\d+:)?(\\d+)?:(\\d+)");
private final String downloadThumbnailKey;
private final int commentHorizontalPadding; private final int commentHorizontalPadding;
private final int commentVerticalPadding; private final int commentVerticalPadding;
private SharedPreferences preferences = null;
private final RelativeLayout itemRoot; private final RelativeLayout itemRoot;
public final CircleImageView itemThumbnailView; public final CircleImageView itemThumbnailView;
private final TextView itemContentView; private final TextView itemContentView;
private final TextView itemLikesCountView; private final TextView itemLikesCountView;
private final TextView itemDislikesCountView;
private final TextView itemPublishedTime; private final TextView itemPublishedTime;
private String commentText; private String commentText;
@ -53,20 +51,21 @@ public class CommentsMiniInfoItemHolder extends InfoItemHolder {
private final Linkify.TransformFilter timestampLink = new Linkify.TransformFilter() { private final Linkify.TransformFilter timestampLink = new Linkify.TransformFilter() {
@Override @Override
public String transformUrl(final Matcher match, final String url) { public String transformUrl(final Matcher match, final String url) {
int timestamp = 0; try {
final String hours = match.group(1); final TimestampExtractor.TimestampMatchDTO timestampMatchDTO =
final String minutes = match.group(2); TimestampExtractor.getTimestampFromMatcher(match, commentText);
final String seconds = match.group(3);
if (hours != null) { if (timestampMatchDTO == null) {
timestamp += (Integer.parseInt(hours.replace(":", "")) * 3600); return url;
}
return streamUrl + url.replace(
match.group(0),
"#timestamp=" + timestampMatchDTO.seconds());
} catch (final Exception ex) {
Log.e(TAG, "Unable to process url='" + url + "' as timestampLink", ex);
return url;
} }
if (minutes != null) {
timestamp += (Integer.parseInt(minutes.replace(":", "")) * 60);
}
if (seconds != null) {
timestamp += (Integer.parseInt(seconds));
}
return streamUrl + url.replace(match.group(0), "#timestamp=" + timestamp);
} }
}; };
@ -77,13 +76,9 @@ public class CommentsMiniInfoItemHolder extends InfoItemHolder {
itemRoot = itemView.findViewById(R.id.itemRoot); itemRoot = itemView.findViewById(R.id.itemRoot);
itemThumbnailView = itemView.findViewById(R.id.itemThumbnailView); itemThumbnailView = itemView.findViewById(R.id.itemThumbnailView);
itemLikesCountView = itemView.findViewById(R.id.detail_thumbs_up_count_view); itemLikesCountView = itemView.findViewById(R.id.detail_thumbs_up_count_view);
itemDislikesCountView = itemView.findViewById(R.id.detail_thumbs_down_count_view);
itemPublishedTime = itemView.findViewById(R.id.itemPublishedTime); itemPublishedTime = itemView.findViewById(R.id.itemPublishedTime);
itemContentView = itemView.findViewById(R.id.itemCommentContentView); itemContentView = itemView.findViewById(R.id.itemCommentContentView);
downloadThumbnailKey = infoItemBuilder.getContext().
getString(R.string.download_thumbnail_key);
commentHorizontalPadding = (int) infoItemBuilder.getContext() commentHorizontalPadding = (int) infoItemBuilder.getContext()
.getResources().getDimension(R.dimen.comments_horizontal_padding); .getResources().getDimension(R.dimen.comments_horizontal_padding);
commentVerticalPadding = (int) infoItemBuilder.getContext() commentVerticalPadding = (int) infoItemBuilder.getContext()
@ -103,14 +98,8 @@ public class CommentsMiniInfoItemHolder extends InfoItemHolder {
} }
final CommentsInfoItem item = (CommentsInfoItem) infoItem; final CommentsInfoItem item = (CommentsInfoItem) infoItem;
preferences = PreferenceManager.getDefaultSharedPreferences(itemBuilder.getContext()); PicassoHelper.loadAvatar(item.getUploaderAvatarUrl()).into(itemThumbnailView);
if (PicassoHelper.getShouldLoadImages()) {
itemBuilder.getImageLoader()
.displayImage(item.getUploaderAvatarUrl(),
itemThumbnailView,
ImageDisplayConstants.DISPLAY_THUMBNAIL_OPTIONS);
if (preferences.getBoolean(downloadThumbnailKey, true)) {
itemThumbnailView.setVisibility(View.VISIBLE); itemThumbnailView.setVisibility(View.VISIBLE);
itemRoot.setPadding(commentVerticalPadding, commentVerticalPadding, itemRoot.setPadding(commentVerticalPadding, commentVerticalPadding,
commentVerticalPadding, commentVerticalPadding); commentVerticalPadding, commentVerticalPadding);
@ -254,7 +243,14 @@ public class CommentsMiniInfoItemHolder extends InfoItemHolder {
} }
private void linkify() { private void linkify() {
Linkify.addLinks(itemContentView, Linkify.WEB_URLS); Linkify.addLinks(
Linkify.addLinks(itemContentView, PATTERN, null, null, timestampLink); itemContentView,
Linkify.WEB_URLS);
Linkify.addLinks(
itemContentView,
TimestampExtractor.TIMESTAMPS_PATTERN,
null,
null,
timestampLink);
} }
} }

View file

@ -9,7 +9,7 @@ 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.ImageDisplayConstants; import org.schabi.newpipe.util.PicassoHelper;
import org.schabi.newpipe.util.Localization; import org.schabi.newpipe.util.Localization;
public class PlaylistMiniInfoItemHolder extends InfoItemHolder { public class PlaylistMiniInfoItemHolder extends InfoItemHolder {
@ -46,9 +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());
itemBuilder.getImageLoader() PicassoHelper.loadPlaylistThumbnail(item.getThumbnailUrl()).into(itemThumbnailView);
.displayImage(item.getThumbnailUrl(), itemThumbnailView,
ImageDisplayConstants.DISPLAY_THUMBNAIL_OPTIONS);
itemView.setOnClickListener(view -> { itemView.setOnClickListener(view -> {
if (itemBuilder.getOnPlaylistSelectedListener() != null) { if (itemBuilder.getOnPlaylistSelectedListener() != null) {

View file

@ -15,7 +15,7 @@ import org.schabi.newpipe.extractor.stream.StreamType;
import org.schabi.newpipe.info_list.InfoItemBuilder; import org.schabi.newpipe.info_list.InfoItemBuilder;
import org.schabi.newpipe.ktx.ViewUtils; 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.ImageDisplayConstants; import org.schabi.newpipe.util.PicassoHelper;
import org.schabi.newpipe.util.Localization; import org.schabi.newpipe.util.Localization;
import org.schabi.newpipe.views.AnimatedProgressBar; import org.schabi.newpipe.views.AnimatedProgressBar;
@ -83,10 +83,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
itemBuilder.getImageLoader() PicassoHelper.loadThumbnail(item.getThumbnailUrl()).into(itemThumbnailView);
.displayImage(item.getThumbnailUrl(),
itemThumbnailView,
ImageDisplayConstants.DISPLAY_THUMBNAIL_OPTIONS);
itemView.setOnClickListener(view -> { itemView.setOnClickListener(view -> {
if (itemBuilder.getOnStreamSelectedListener() != null) { if (itemBuilder.getOnStreamSelectedListener() != null) {

View file

@ -1,10 +1,6 @@
package org.schabi.newpipe.local; package org.schabi.newpipe.local;
import android.content.Context; import android.content.Context;
import android.widget.ImageView;
import com.nostra13.universalimageloader.core.DisplayImageOptions;
import com.nostra13.universalimageloader.core.ImageLoader;
import org.schabi.newpipe.database.LocalItem; import org.schabi.newpipe.database.LocalItem;
import org.schabi.newpipe.util.OnClickGesture; import org.schabi.newpipe.util.OnClickGesture;
@ -31,7 +27,6 @@ import org.schabi.newpipe.util.OnClickGesture;
public class LocalItemBuilder { public class LocalItemBuilder {
private final Context context; private final Context context;
private final ImageLoader imageLoader = ImageLoader.getInstance();
private OnClickGesture<LocalItem> onSelectedListener; private OnClickGesture<LocalItem> onSelectedListener;
@ -43,11 +38,6 @@ public class LocalItemBuilder {
return context; return context;
} }
public void displayImage(final String url, final ImageView view,
final DisplayImageOptions options) {
imageLoader.displayImage(url, view, options);
}
public OnClickGesture<LocalItem> getOnItemSelectedListener() { public OnClickGesture<LocalItem> getOnItemSelectedListener() {
return onSelectedListener; return onSelectedListener;
} }

View file

@ -206,7 +206,7 @@ class FeedFragment : BaseStateFragment<FeedState>() {
putBoolean(getString(R.string.feed_use_dedicated_fetch_method_key), !usingDedicatedMethod) putBoolean(getString(R.string.feed_use_dedicated_fetch_method_key), !usingDedicatedMethod)
} }
} }
.setPositiveButton(resources.getString(R.string.finish), null) .setPositiveButton(resources.getString(R.string.ok), null)
.create() .create()
.show() .show()
return true return true
@ -362,6 +362,7 @@ class FeedFragment : BaseStateFragment<FeedState>() {
StreamDialogEntry.mark_as_watched StreamDialogEntry.mark_as_watched
) )
} }
entries.add(StreamDialogEntry.show_channel_details)
StreamDialogEntry.setEnabledEntries(entries) StreamDialogEntry.setEnabledEntries(entries)
InfoItemDialog(activity, item, StreamDialogEntry.getCommands(context)) { _, which -> InfoItemDialog(activity, item, StreamDialogEntry.getCommands(context)) { _, which ->

View file

@ -5,7 +5,6 @@ import android.text.TextUtils
import android.view.View import android.view.View
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.preference.PreferenceManager import androidx.preference.PreferenceManager
import com.nostra13.universalimageloader.core.ImageLoader
import com.xwray.groupie.viewbinding.BindableItem import com.xwray.groupie.viewbinding.BindableItem
import org.schabi.newpipe.MainActivity import org.schabi.newpipe.MainActivity
import org.schabi.newpipe.R import org.schabi.newpipe.R
@ -16,8 +15,8 @@ import org.schabi.newpipe.extractor.stream.StreamType.AUDIO_LIVE_STREAM
import org.schabi.newpipe.extractor.stream.StreamType.AUDIO_STREAM import org.schabi.newpipe.extractor.stream.StreamType.AUDIO_STREAM
import org.schabi.newpipe.extractor.stream.StreamType.LIVE_STREAM import org.schabi.newpipe.extractor.stream.StreamType.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.ImageDisplayConstants
import org.schabi.newpipe.util.Localization import org.schabi.newpipe.util.Localization
import org.schabi.newpipe.util.PicassoHelper
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
data class StreamItem( data class StreamItem(
@ -93,10 +92,7 @@ data class StreamItem(
viewBinding.itemProgressView.visibility = View.GONE viewBinding.itemProgressView.visibility = View.GONE
} }
ImageLoader.getInstance().displayImage( PicassoHelper.loadThumbnail(stream.thumbnailUrl).into(viewBinding.itemThumbnailView)
stream.thumbnailUrl, viewBinding.itemThumbnailView,
ImageDisplayConstants.DISPLAY_THUMBNAIL_OPTIONS
)
if (itemVersion != ItemVersion.MINI) { if (itemVersion != ItemVersion.MINI) {
viewBinding.itemAdditionalDetails.text = viewBinding.itemAdditionalDetails.text =

View file

@ -300,6 +300,12 @@ class FeedLoadService : Service() {
.subscribeOn(Schedulers.io()) .subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread()) .observeOn(AndroidSchedulers.mainThread())
.subscribe { _, throwable -> .subscribe { _, throwable ->
// There seems to be a bug in the kotlin plugin as it tells you when
// building that this can't be null:
// "Condition 'throwable != null' is always 'true'"
// However it can indeed be null
// The suppression may be removed in further versions
@Suppress("SENSELESS_COMPARISON")
if (throwable != null) { if (throwable != null) {
Log.e(TAG, "Error while storing result", throwable) Log.e(TAG, "Error while storing result", throwable)
handleError(throwable) handleError(throwable)

View file

@ -53,8 +53,6 @@ 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;
import static org.schabi.newpipe.extractor.utils.Utils.isNullOrEmpty;
public class StatisticsPlaylistFragment public class StatisticsPlaylistFragment
extends BaseLocalListFragment<List<StreamStatisticsEntry>, Void> { extends BaseLocalListFragment<List<StreamStatisticsEntry>, Void> {
private final CompositeDisposable disposables = new CompositeDisposable(); private final CompositeDisposable disposables = new CompositeDisposable();
@ -363,10 +361,7 @@ public class StatisticsPlaylistFragment
if (KoreUtils.shouldShowPlayWithKodi(context, infoItem.getServiceId())) { if (KoreUtils.shouldShowPlayWithKodi(context, infoItem.getServiceId())) {
entries.add(StreamDialogEntry.play_with_kodi); entries.add(StreamDialogEntry.play_with_kodi);
} }
entries.add(StreamDialogEntry.show_channel_details);
if (!isNullOrEmpty(infoItem.getUploaderUrl())) {
entries.add(StreamDialogEntry.show_channel_details);
}
StreamDialogEntry.setEnabledEntries(entries); StreamDialogEntry.setEnabledEntries(entries);

View file

@ -7,7 +7,7 @@ import org.schabi.newpipe.database.LocalItem;
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.ImageDisplayConstants; import org.schabi.newpipe.util.PicassoHelper;
import org.schabi.newpipe.util.Localization; import org.schabi.newpipe.util.Localization;
import java.time.format.DateTimeFormatter; import java.time.format.DateTimeFormatter;
@ -36,8 +36,7 @@ public class LocalPlaylistItemHolder extends PlaylistItemHolder {
itemStreamCountView.getContext(), item.streamCount)); itemStreamCountView.getContext(), item.streamCount));
itemUploaderView.setVisibility(View.INVISIBLE); itemUploaderView.setVisibility(View.INVISIBLE);
itemBuilder.displayImage(item.thumbnailUrl, itemThumbnailView, PicassoHelper.loadPlaylistThumbnail(item.thumbnailUrl).into(itemThumbnailView);
ImageDisplayConstants.DISPLAY_PLAYLIST_OPTIONS);
super.updateFromItem(localItem, historyRecordManager, dateTimeFormatter); super.updateFromItem(localItem, historyRecordManager, dateTimeFormatter);
} }

View file

@ -15,7 +15,7 @@ import org.schabi.newpipe.extractor.NewPipe;
import org.schabi.newpipe.ktx.ViewUtils; import org.schabi.newpipe.ktx.ViewUtils;
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.ImageDisplayConstants; import org.schabi.newpipe.util.PicassoHelper;
import org.schabi.newpipe.util.Localization; import org.schabi.newpipe.util.Localization;
import org.schabi.newpipe.views.AnimatedProgressBar; import org.schabi.newpipe.views.AnimatedProgressBar;
@ -81,8 +81,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
itemBuilder.displayImage(item.getStreamEntity().getThumbnailUrl(), itemThumbnailView, PicassoHelper.loadThumbnail(item.getStreamEntity().getThumbnailUrl())
ImageDisplayConstants.DISPLAY_THUMBNAIL_OPTIONS); .into(itemThumbnailView);
itemView.setOnClickListener(view -> { itemView.setOnClickListener(view -> {
if (itemBuilder.getOnItemSelectedListener() != null) { if (itemBuilder.getOnItemSelectedListener() != null) {

View file

@ -15,7 +15,7 @@ import org.schabi.newpipe.extractor.NewPipe;
import org.schabi.newpipe.ktx.ViewUtils; import org.schabi.newpipe.ktx.ViewUtils;
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.ImageDisplayConstants; import org.schabi.newpipe.util.PicassoHelper;
import org.schabi.newpipe.util.Localization; import org.schabi.newpipe.util.Localization;
import org.schabi.newpipe.views.AnimatedProgressBar; import org.schabi.newpipe.views.AnimatedProgressBar;
@ -114,8 +114,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
itemBuilder.displayImage(item.getStreamEntity().getThumbnailUrl(), itemThumbnailView, PicassoHelper.loadThumbnail(item.getStreamEntity().getThumbnailUrl())
ImageDisplayConstants.DISPLAY_THUMBNAIL_OPTIONS); .into(itemThumbnailView);
itemView.setOnClickListener(view -> { itemView.setOnClickListener(view -> {
if (itemBuilder.getOnItemSelectedListener() != null) { if (itemBuilder.getOnItemSelectedListener() != null) {

View file

@ -8,7 +8,7 @@ import org.schabi.newpipe.database.playlist.model.PlaylistRemoteEntity;
import org.schabi.newpipe.extractor.NewPipe; import org.schabi.newpipe.extractor.NewPipe;
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.ImageDisplayConstants; import org.schabi.newpipe.util.PicassoHelper;
import org.schabi.newpipe.util.Localization; import org.schabi.newpipe.util.Localization;
import java.time.format.DateTimeFormatter; import java.time.format.DateTimeFormatter;
@ -44,9 +44,7 @@ public class RemotePlaylistItemHolder extends PlaylistItemHolder {
itemUploaderView.setText(NewPipe.getNameOfService(item.getServiceId())); itemUploaderView.setText(NewPipe.getNameOfService(item.getServiceId()));
} }
PicassoHelper.loadPlaylistThumbnail(item.getThumbnailUrl()).into(itemThumbnailView);
itemBuilder.displayImage(item.getThumbnailUrl(), itemThumbnailView,
ImageDisplayConstants.DISPLAY_PLAYLIST_OPTIONS);
super.updateFromItem(localItem, historyRecordManager, dateTimeFormatter); super.updateFromItem(localItem, historyRecordManager, dateTimeFormatter);
} }

View file

@ -67,7 +67,6 @@ import io.reactivex.rxjava3.disposables.Disposable;
import io.reactivex.rxjava3.schedulers.Schedulers; import io.reactivex.rxjava3.schedulers.Schedulers;
import io.reactivex.rxjava3.subjects.PublishSubject; import io.reactivex.rxjava3.subjects.PublishSubject;
import static org.schabi.newpipe.extractor.utils.Utils.isNullOrEmpty;
import static org.schabi.newpipe.ktx.ViewUtils.animate; import static org.schabi.newpipe.ktx.ViewUtils.animate;
import static org.schabi.newpipe.util.ThemeHelper.shouldUseGridLayout; import static org.schabi.newpipe.util.ThemeHelper.shouldUseGridLayout;
@ -778,10 +777,7 @@ public class LocalPlaylistFragment extends BaseLocalListFragment<List<PlaylistSt
if (KoreUtils.shouldShowPlayWithKodi(context, infoItem.getServiceId())) { if (KoreUtils.shouldShowPlayWithKodi(context, infoItem.getServiceId())) {
entries.add(StreamDialogEntry.play_with_kodi); entries.add(StreamDialogEntry.play_with_kodi);
} }
entries.add(StreamDialogEntry.show_channel_details);
if (!isNullOrEmpty(infoItem.getUploaderUrl())) {
entries.add(StreamDialogEntry.show_channel_details);
}
StreamDialogEntry.setEnabledEntries(entries); StreamDialogEntry.setEnabledEntries(entries);

View file

@ -40,7 +40,7 @@ public class ImportConfirmationDialog extends DialogFragment {
.setMessage(R.string.import_network_expensive_warning) .setMessage(R.string.import_network_expensive_warning)
.setCancelable(true) .setCancelable(true)
.setNegativeButton(R.string.cancel, null) .setNegativeButton(R.string.cancel, null)
.setPositiveButton(R.string.finish, (dialogInterface, i) -> { .setPositiveButton(R.string.ok, (dialogInterface, i) -> {
if (resultServiceIntent != null && getContext() != null) { if (resultServiceIntent != null && getContext() != null) {
getContext().startService(resultServiceIntent); getContext().startService(resultServiceIntent);
} }

View file

@ -179,7 +179,7 @@ class SubscriptionFragment : BaseStateFragment<SubscriptionState>() {
} }
private fun onImportPreviousSelected() { private fun onImportPreviousSelected() {
requestImportLauncher.launch(StoredFileHelper.getPicker(activity)) requestImportLauncher.launch(StoredFileHelper.getPicker(activity, JSON_MIME_TYPE))
} }
private fun onExportSelected() { private fun onExportSelected() {
@ -187,7 +187,7 @@ class SubscriptionFragment : BaseStateFragment<SubscriptionState>() {
val exportName = "newpipe_subscriptions_$date.json" val exportName = "newpipe_subscriptions_$date.json"
requestExportLauncher.launch( requestExportLauncher.launch(
StoredFileHelper.getNewPicker(activity, exportName, "application/json", null) StoredFileHelper.getNewPicker(activity, exportName, JSON_MIME_TYPE, null)
) )
} }
@ -195,7 +195,7 @@ class SubscriptionFragment : BaseStateFragment<SubscriptionState>() {
FeedGroupReorderDialog().show(parentFragmentManager, null) FeedGroupReorderDialog().show(parentFragmentManager, null)
} }
fun requestExportResult(result: ActivityResult) { private fun requestExportResult(result: ActivityResult) {
if (result.data != null && result.resultCode == Activity.RESULT_OK) { if (result.data != null && result.resultCode == Activity.RESULT_OK) {
activity.startService( activity.startService(
Intent(activity, SubscriptionsExportService::class.java) Intent(activity, SubscriptionsExportService::class.java)
@ -204,7 +204,7 @@ class SubscriptionFragment : BaseStateFragment<SubscriptionState>() {
} }
} }
fun requestImportResult(result: ActivityResult) { private fun requestImportResult(result: ActivityResult) {
if (result.data != null && result.resultCode == Activity.RESULT_OK) { if (result.data != null && result.resultCode == Activity.RESULT_OK) {
ImportConfirmationDialog.show( ImportConfirmationDialog.show(
this, this,
@ -407,4 +407,8 @@ class SubscriptionFragment : BaseStateFragment<SubscriptionState>() {
super.hideLoading() super.hideLoading()
binding.itemsList.animate(true, 200) binding.itemsList.animate(true, 200)
} }
companion object {
const val JSON_MIME_TYPE = "application/json"
}
} }

View file

@ -177,7 +177,8 @@ public class SubscriptionsImportFragment extends BaseFragment {
} }
public void onImportFile() { public void onImportFile() {
requestImportFileLauncher.launch(StoredFileHelper.getPicker(activity)); // leave */* mime type to support all services with different mime types and file extensions
requestImportFileLauncher.launch(StoredFileHelper.getPicker(activity, "*/*"));
} }
private void requestImportFileResult(final ActivityResult result) { private void requestImportFileResult(final ActivityResult result) {

View file

@ -143,21 +143,15 @@ class FeedGroupDialog : DialogFragment(), BackPressable {
).get(FeedGroupDialogViewModel::class.java) ).get(FeedGroupDialogViewModel::class.java)
viewModel.groupLiveData.observe(viewLifecycleOwner, Observer(::handleGroup)) viewModel.groupLiveData.observe(viewLifecycleOwner, Observer(::handleGroup))
viewModel.subscriptionsLiveData.observe( viewModel.subscriptionsLiveData.observe(viewLifecycleOwner) {
viewLifecycleOwner, setupSubscriptionPicker(it.first, it.second)
Observer { }
setupSubscriptionPicker(it.first, it.second) viewModel.dialogEventLiveData.observe(viewLifecycleOwner) {
when (it) {
ProcessingEvent -> disableInput()
SuccessEvent -> dismiss()
} }
) }
viewModel.dialogEventLiveData.observe(
viewLifecycleOwner,
Observer {
when (it) {
ProcessingEvent -> disableInput()
SuccessEvent -> dismiss()
}
}
)
subscriptionGroupAdapter = GroupAdapter<GroupieViewHolder>().apply { subscriptionGroupAdapter = GroupAdapter<GroupieViewHolder>().apply {
add(subscriptionMainSection) add(subscriptionMainSection)
@ -437,7 +431,7 @@ class FeedGroupDialog : DialogFragment(), BackPressable {
feedGroupCreateBinding.confirmButton.setText( feedGroupCreateBinding.confirmButton.setText(
when { when {
currentScreen == InitialScreen && groupId == NO_GROUP_SELECTED -> R.string.create currentScreen == InitialScreen && groupId == NO_GROUP_SELECTED -> R.string.create
else -> android.R.string.ok else -> R.string.ok
} }
) )

View file

@ -3,14 +3,13 @@ package org.schabi.newpipe.local.subscription.item
import android.content.Context import android.content.Context
import android.widget.ImageView import android.widget.ImageView
import android.widget.TextView import android.widget.TextView
import com.nostra13.universalimageloader.core.ImageLoader
import com.xwray.groupie.GroupieViewHolder import com.xwray.groupie.GroupieViewHolder
import com.xwray.groupie.Item import com.xwray.groupie.Item
import org.schabi.newpipe.R import org.schabi.newpipe.R
import org.schabi.newpipe.extractor.channel.ChannelInfoItem import org.schabi.newpipe.extractor.channel.ChannelInfoItem
import org.schabi.newpipe.util.ImageDisplayConstants
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.PicassoHelper
class ChannelItem( class ChannelItem(
private val infoItem: ChannelInfoItem, private val infoItem: ChannelInfoItem,
@ -40,10 +39,7 @@ class ChannelItem(
itemChannelDescriptionView.text = infoItem.description itemChannelDescriptionView.text = infoItem.description
} }
ImageLoader.getInstance().displayImage( PicassoHelper.loadThumbnail(infoItem.thumbnailUrl).into(itemThumbnailView)
infoItem.thumbnailUrl, itemThumbnailView,
ImageDisplayConstants.DISPLAY_THUMBNAIL_OPTIONS
)
gesturesListener?.run { gesturesListener?.run {
viewHolder.root.setOnClickListener { selected(infoItem) } viewHolder.root.setOnClickListener { selected(infoItem) }

View file

@ -1,23 +0,0 @@
package org.schabi.newpipe.local.subscription.item
import android.view.View
import android.view.View.OnClickListener
import com.xwray.groupie.viewbinding.BindableItem
import org.schabi.newpipe.R
import org.schabi.newpipe.databinding.HeaderItemBinding
class HeaderItem(
val title: String,
private val onClickListener: (() -> Unit)? = null
) : BindableItem<HeaderItemBinding>() {
override fun getLayout(): Int = R.layout.header_item
override fun bind(viewBinding: HeaderItemBinding, position: Int) {
viewBinding.headerTitle.text = title
val listener = onClickListener?.let { OnClickListener { onClickListener.invoke() } }
viewBinding.root.setOnClickListener(listener)
}
override fun initializeViewBinding(view: View) = HeaderItemBinding.bind(view)
}

View file

@ -3,7 +3,6 @@ package org.schabi.newpipe.local.subscription.item
import android.view.View import android.view.View
import androidx.core.view.isGone import androidx.core.view.isGone
import androidx.core.view.isVisible import androidx.core.view.isVisible
import com.nostra13.universalimageloader.core.ImageLoader
import com.xwray.groupie.viewbinding.BindableItem import com.xwray.groupie.viewbinding.BindableItem
import com.xwray.groupie.viewbinding.GroupieViewHolder import com.xwray.groupie.viewbinding.GroupieViewHolder
import org.schabi.newpipe.R import org.schabi.newpipe.R
@ -11,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.ImageDisplayConstants import org.schabi.newpipe.util.PicassoHelper
data class PickerSubscriptionItem( data class PickerSubscriptionItem(
val subscriptionEntity: SubscriptionEntity, val subscriptionEntity: SubscriptionEntity,
@ -22,11 +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) {
ImageLoader.getInstance().displayImage( PicassoHelper.loadAvatar(subscriptionEntity.avatarUrl).into(viewBinding.thumbnailView)
subscriptionEntity.avatarUrl,
viewBinding.thumbnailView, ImageDisplayConstants.DISPLAY_AVATAR_OPTIONS
)
viewBinding.titleView.text = subscriptionEntity.name viewBinding.titleView.text = subscriptionEntity.name
viewBinding.selectedHighlight.isVisible = isSelected viewBinding.selectedHighlight.isVisible = isSelected
} }

View file

@ -19,6 +19,9 @@
package org.schabi.newpipe.local.subscription.services; package org.schabi.newpipe.local.subscription.services;
import static org.schabi.newpipe.MainActivity.DEBUG;
import static org.schabi.newpipe.streams.io.StoredFileHelper.DEFAULT_MIME;
import android.content.Intent; import android.content.Intent;
import android.net.Uri; import android.net.Uri;
import android.text.TextUtils; import android.text.TextUtils;
@ -46,6 +49,7 @@ import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.Objects;
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;
@ -54,9 +58,6 @@ import io.reactivex.rxjava3.functions.Consumer;
import io.reactivex.rxjava3.functions.Function; import io.reactivex.rxjava3.functions.Function;
import io.reactivex.rxjava3.schedulers.Schedulers; import io.reactivex.rxjava3.schedulers.Schedulers;
import static org.schabi.newpipe.MainActivity.DEBUG;
import static org.schabi.newpipe.streams.io.StoredFileHelper.DEFAULT_MIME;
public class SubscriptionsImportService extends BaseImportExportService { public class SubscriptionsImportService extends BaseImportExportService {
public static final int CHANNEL_URL_MODE = 0; public static final int CHANNEL_URL_MODE = 0;
public static final int INPUT_STREAM_MODE = 1; public static final int INPUT_STREAM_MODE = 1;
@ -89,6 +90,8 @@ public class SubscriptionsImportService extends BaseImportExportService {
private String channelUrl; private String channelUrl;
@Nullable @Nullable
private InputStream inputStream; private InputStream inputStream;
@Nullable
private String inputStreamType;
@Override @Override
public int onStartCommand(final Intent intent, final int flags, final int startId) { public int onStartCommand(final Intent intent, final int flags, final int startId) {
@ -111,8 +114,20 @@ public class SubscriptionsImportService extends BaseImportExportService {
} }
try { try {
inputStream = new SharpInputStream( final StoredFileHelper fileHelper = new StoredFileHelper(this, uri, DEFAULT_MIME);
new StoredFileHelper(this, uri, DEFAULT_MIME).getStream()); inputStream = new SharpInputStream(fileHelper.getStream());
inputStreamType = fileHelper.getType();
if (inputStreamType == null || inputStreamType.equals(DEFAULT_MIME)) {
// mime type could not be determined, just take file extension
final String name = fileHelper.getName();
final int pointIndex = name.lastIndexOf('.');
if (pointIndex == -1 || pointIndex >= name.length() - 1) {
inputStreamType = DEFAULT_MIME; // no extension, will fail in the extractor
} else {
inputStreamType = name.substring(pointIndex + 1);
}
}
} catch (final IOException e) { } catch (final IOException e) {
handleError(e); handleError(e);
return START_NOT_STICKY; return START_NOT_STICKY;
@ -248,9 +263,9 @@ public class SubscriptionsImportService extends BaseImportExportService {
final Throwable error = notification.getError(); final Throwable error = notification.getError();
final Throwable cause = error.getCause(); final Throwable cause = error.getCause();
if (error instanceof IOException) { if (error instanceof IOException) {
throw (IOException) error; throw error;
} else if (cause instanceof IOException) { } else if (cause instanceof IOException) {
throw (IOException) cause; throw cause;
} else if (ExceptionUtils.isNetworkRelated(error)) { } else if (ExceptionUtils.isNetworkRelated(error)) {
throw new IOException(error); throw new IOException(error);
} }
@ -280,9 +295,12 @@ public class SubscriptionsImportService extends BaseImportExportService {
} }
private Flowable<List<SubscriptionItem>> importFromInputStream() { private Flowable<List<SubscriptionItem>> importFromInputStream() {
Objects.requireNonNull(inputStream);
Objects.requireNonNull(inputStreamType);
return Flowable.fromCallable(() -> NewPipe.getService(currentServiceId) return Flowable.fromCallable(() -> NewPipe.getService(currentServiceId)
.getSubscriptionExtractor() .getSubscriptionExtractor()
.fromInputStream(inputStream)); .fromInputStream(inputStream, inputStreamType));
} }
private Flowable<List<SubscriptionItem>> importFromPreviousExport() { private Flowable<List<SubscriptionItem>> importFromPreviousExport() {

View file

@ -24,7 +24,6 @@ import android.content.Context;
import android.content.Intent; import android.content.Intent;
import android.os.Binder; import android.os.Binder;
import android.os.IBinder; import android.os.IBinder;
import android.util.DisplayMetrics;
import android.util.Log; import android.util.Log;
import android.view.LayoutInflater; import android.view.LayoutInflater;
import android.view.View; import android.view.View;
@ -36,6 +35,7 @@ import androidx.core.content.ContextCompat;
import org.schabi.newpipe.App; import org.schabi.newpipe.App;
import org.schabi.newpipe.databinding.PlayerBinding; import org.schabi.newpipe.databinding.PlayerBinding;
import org.schabi.newpipe.util.DeviceUtils;
import org.schabi.newpipe.util.ThemeHelper; import org.schabi.newpipe.util.ThemeHelper;
import static org.schabi.newpipe.util.Localization.assureCorrectAppLanguage; import static org.schabi.newpipe.util.Localization.assureCorrectAppLanguage;
@ -133,32 +133,29 @@ public final class MainPlayer extends Service {
return START_NOT_STICKY; return START_NOT_STICKY;
} }
public void stop(final boolean autoplayEnabled) { public void stopForImmediateReusing() {
if (DEBUG) { if (DEBUG) {
Log.d(TAG, "stop() called"); Log.d(TAG, "stopForImmediateReusing() called");
} }
if (!player.exoPlayerIsNull()) { if (!player.exoPlayerIsNull()) {
player.saveWasPlaying(); player.saveWasPlaying();
// Releases wifi & cpu, disables keepScreenOn, etc. // Releases wifi & cpu, disables keepScreenOn, etc.
if (!autoplayEnabled) {
player.pause();
}
// We can't just pause the player here because it will make transition // We can't just pause the player here because it will make transition
// from one stream to a new stream not smooth // from one stream to a new stream not smooth
player.smoothStopPlayer(); player.smoothStopPlayer();
player.setRecovery(); player.setRecovery();
// Android TV will handle back button in case controls will be visible // Android TV will handle back button in case controls will be visible
// (one more additional unneeded click while the player is hidden) // (one more additional unneeded click while the player is hidden)
player.hideControls(0, 0); player.hideControls(0, 0);
player.closeItemsList(); player.closeItemsList();
// Notification shows information about old stream but if a user selects // Notification shows information about old stream but if a user selects
// a stream from backStack it's not actual anymore // a stream from backStack it's not actual anymore
// So we should hide the notification at all. // So we should hide the notification at all.
// When autoplay enabled such notification flashing is annoying so skip this case // When autoplay enabled such notification flashing is annoying so skip this case
if (!autoplayEnabled) {
NotificationUtil.getInstance().cancelNotificationAndStopForeground(this);
}
} }
} }
@ -222,11 +219,8 @@ public final class MainPlayer extends Service {
boolean isLandscape() { boolean isLandscape() {
// DisplayMetrics from activity context knows about MultiWindow feature // DisplayMetrics from activity context knows about MultiWindow feature
// while DisplayMetrics from app context doesn't // while DisplayMetrics from app context doesn't
final DisplayMetrics metrics = (player != null return DeviceUtils.isLandscape(player != null && player.getParentActivity() != null
&& player.getParentActivity() != null ? player.getParentActivity() : this);
? player.getParentActivity().getResources()
: getResources()).getDisplayMetrics();
return metrics.heightPixels < metrics.widthPixels;
} }
@Nullable @Nullable

View file

@ -1,9 +1,5 @@
package org.schabi.newpipe.player; package org.schabi.newpipe.player;
import static org.schabi.newpipe.player.helper.PlayerHelper.formatSpeed;
import static org.schabi.newpipe.util.Localization.assureCorrectAppLanguage;
import static org.schabi.newpipe.util.external_communication.ShareUtils.shareText;
import android.content.ComponentName; import android.content.ComponentName;
import android.content.Context; import android.content.Context;
import android.content.Intent; import android.content.Intent;
@ -17,7 +13,6 @@ import android.view.Menu;
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.PopupMenu;
import android.widget.SeekBar; import android.widget.SeekBar;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
@ -49,9 +44,12 @@ import org.schabi.newpipe.util.PermissionHelper;
import org.schabi.newpipe.util.ServiceHelper; import org.schabi.newpipe.util.ServiceHelper;
import org.schabi.newpipe.util.ThemeHelper; import org.schabi.newpipe.util.ThemeHelper;
import java.util.Collections;
import java.util.List; import java.util.List;
import static org.schabi.newpipe.QueueItemMenuUtil.openPopupMenu;
import static org.schabi.newpipe.player.helper.PlayerHelper.formatSpeed;
import static org.schabi.newpipe.util.Localization.assureCorrectAppLanguage;
import static org.schabi.newpipe.util.SponsorBlockUtils.markSegments; import static org.schabi.newpipe.util.SponsorBlockUtils.markSegments;
public final class PlayQueueActivity extends AppCompatActivity public final class PlayQueueActivity extends AppCompatActivity
@ -60,7 +58,6 @@ public final class PlayQueueActivity extends AppCompatActivity
private static final String TAG = PlayQueueActivity.class.getSimpleName(); private static final String TAG = PlayQueueActivity.class.getSimpleName();
private static final int RECYCLER_ITEM_POPUP_MENU_GROUP_ID = 47;
private static final int SMOOTH_SCROLL_MAXIMUM_DISTANCE = 80; private static final int SMOOTH_SCROLL_MAXIMUM_DISTANCE = 80;
protected Player player; protected Player player;
@ -289,49 +286,6 @@ public final class PlayQueueActivity extends AppCompatActivity
queueControlBinding.controlShuffle.setOnClickListener(this); queueControlBinding.controlShuffle.setOnClickListener(this);
} }
private void buildItemPopupMenu(final PlayQueueItem item, final View view) {
final PopupMenu popupMenu = new PopupMenu(this, view);
final MenuItem remove = popupMenu.getMenu().add(RECYCLER_ITEM_POPUP_MENU_GROUP_ID, 0,
Menu.NONE, R.string.play_queue_remove);
remove.setOnMenuItemClickListener(menuItem -> {
if (player == null) {
return false;
}
final int index = player.getPlayQueue().indexOf(item);
if (index != -1) {
player.getPlayQueue().remove(index);
}
return true;
});
final MenuItem detail = popupMenu.getMenu().add(RECYCLER_ITEM_POPUP_MENU_GROUP_ID, 1,
Menu.NONE, R.string.play_queue_stream_detail);
detail.setOnMenuItemClickListener(menuItem -> {
// playQueue is null since we don't want any queue change
NavigationHelper.openVideoDetail(this, item.getServiceId(), item.getUrl(),
item.getTitle(), null, false);
return true;
});
final MenuItem append = popupMenu.getMenu().add(RECYCLER_ITEM_POPUP_MENU_GROUP_ID, 2,
Menu.NONE, R.string.append_playlist);
append.setOnMenuItemClickListener(menuItem -> {
openPlaylistAppendDialog(Collections.singletonList(item));
return true;
});
final MenuItem share = popupMenu.getMenu().add(RECYCLER_ITEM_POPUP_MENU_GROUP_ID, 3,
Menu.NONE, R.string.share);
share.setOnMenuItemClickListener(menuItem -> {
shareText(getApplicationContext(), item.getTitle(), item.getUrl(),
item.getThumbnailUrl());
return true;
});
popupMenu.show();
}
//////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////
// Component Helpers // Component Helpers
//////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////
@ -379,13 +333,9 @@ public final class PlayQueueActivity extends AppCompatActivity
@Override @Override
public void held(final PlayQueueItem item, final View view) { public void held(final PlayQueueItem item, final View view) {
if (player == null) { if (player != null && player.getPlayQueue().indexOf(item) != -1) {
return; openPopupMenu(player.getPlayQueue(), item, view, false,
} getSupportFragmentManager(), PlayQueueActivity.this);
final int index = player.getPlayQueue().indexOf(item);
if (index != -1) {
buildItemPopupMenu(item, view);
} }
} }

View file

@ -18,6 +18,7 @@ import android.graphics.BitmapFactory;
import android.graphics.Color; import android.graphics.Color;
import android.graphics.PorterDuff; import android.graphics.PorterDuff;
import android.graphics.PorterDuffColorFilter; import android.graphics.PorterDuffColorFilter;
import android.graphics.drawable.Drawable;
import android.media.AudioManager; import android.media.AudioManager;
import android.net.Uri; import android.net.Uri;
import android.os.Build; import android.os.Build;
@ -83,9 +84,8 @@ import com.google.android.exoplayer2.upstream.DefaultBandwidthMeter;
import com.google.android.exoplayer2.util.Util; import com.google.android.exoplayer2.util.Util;
import com.google.android.exoplayer2.video.VideoListener; import com.google.android.exoplayer2.video.VideoListener;
import com.google.android.material.floatingactionbutton.FloatingActionButton; import com.google.android.material.floatingactionbutton.FloatingActionButton;
import com.nostra13.universalimageloader.core.ImageLoader; import com.squareup.picasso.Picasso;
import com.nostra13.universalimageloader.core.assist.FailReason; import com.squareup.picasso.Target;
import com.nostra13.universalimageloader.core.listener.ImageLoadingListener;
import org.schabi.newpipe.DownloaderImpl; import org.schabi.newpipe.DownloaderImpl;
import org.schabi.newpipe.MainActivity; import org.schabi.newpipe.MainActivity;
@ -129,11 +129,11 @@ import org.schabi.newpipe.player.resolver.VideoPlaybackResolver;
import org.schabi.newpipe.player.seekbarpreview.SeekbarPreviewThumbnailHelper; import org.schabi.newpipe.player.seekbarpreview.SeekbarPreviewThumbnailHelper;
import org.schabi.newpipe.player.seekbarpreview.SeekbarPreviewThumbnailHolder; import org.schabi.newpipe.player.seekbarpreview.SeekbarPreviewThumbnailHolder;
import org.schabi.newpipe.util.DeviceUtils; import org.schabi.newpipe.util.DeviceUtils;
import org.schabi.newpipe.util.ImageDisplayConstants; import org.schabi.newpipe.util.external_communication.KoreUtils;
import org.schabi.newpipe.util.ListHelper; import org.schabi.newpipe.util.ListHelper;
import org.schabi.newpipe.util.NavigationHelper; import org.schabi.newpipe.util.NavigationHelper;
import org.schabi.newpipe.util.PicassoHelper;
import org.schabi.newpipe.util.SerializedCache; import org.schabi.newpipe.util.SerializedCache;
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.SponsorBlockMode; import org.schabi.newpipe.util.SponsorBlockMode;
import org.schabi.newpipe.util.VideoSegment; import org.schabi.newpipe.util.VideoSegment;
@ -164,7 +164,9 @@ import static com.google.android.exoplayer2.Player.REPEAT_MODE_OFF;
import static com.google.android.exoplayer2.Player.REPEAT_MODE_ONE; import static com.google.android.exoplayer2.Player.REPEAT_MODE_ONE;
import static com.google.android.exoplayer2.Player.RepeatMode; import static com.google.android.exoplayer2.Player.RepeatMode;
import static java.util.concurrent.TimeUnit.MILLISECONDS; import static java.util.concurrent.TimeUnit.MILLISECONDS;
import static org.schabi.newpipe.QueueItemMenuUtil.openPopupMenu;
import static org.schabi.newpipe.extractor.ServiceList.YouTube; import static org.schabi.newpipe.extractor.ServiceList.YouTube;
import static org.schabi.newpipe.extractor.utils.Utils.isNullOrEmpty;
import static org.schabi.newpipe.ktx.ViewUtils.animate; import static org.schabi.newpipe.ktx.ViewUtils.animate;
import static org.schabi.newpipe.ktx.ViewUtils.animateRotation; import static org.schabi.newpipe.ktx.ViewUtils.animateRotation;
import static org.schabi.newpipe.player.MainPlayer.ACTION_CLOSE; import static org.schabi.newpipe.player.MainPlayer.ACTION_CLOSE;
@ -202,7 +204,6 @@ import static org.schabi.newpipe.util.SponsorBlockUtils.markSegments;
public final class Player implements public final class Player implements
EventListener, EventListener,
PlaybackListener, PlaybackListener,
ImageLoadingListener,
VideoListener, VideoListener,
SeekBar.OnSeekBarChangeListener, SeekBar.OnSeekBarChangeListener,
View.OnClickListener, View.OnClickListener,
@ -244,7 +245,7 @@ public final class Player implements
//////////////////////////////////////////////////////////////////////////*/ //////////////////////////////////////////////////////////////////////////*/
public static final int PLAY_PREV_ACTIVATION_LIMIT_MILLIS = 5000; // 5 seconds public static final int PLAY_PREV_ACTIVATION_LIMIT_MILLIS = 5000; // 5 seconds
public static final int PROGRESS_LOOP_INTERVAL_MILLIS = 500; // 500 millis public static final int PROGRESS_LOOP_INTERVAL_MILLIS = 1000; // 1 second
public static final int DEFAULT_CONTROLS_DURATION = 300; // 300 millis public static final int DEFAULT_CONTROLS_DURATION = 300; // 300 millis
public static final int DEFAULT_CONTROLS_HIDE_TIME = 2000; // 2 Seconds public static final int DEFAULT_CONTROLS_HIDE_TIME = 2000; // 2 Seconds
public static final int DPAD_CONTROLS_HIDE_TIME = 7000; // 7 Seconds public static final int DPAD_CONTROLS_HIDE_TIME = 7000; // 7 Seconds
@ -403,7 +404,7 @@ public final class Player implements
/*////////////////////////////////////////////////////////////////////////// /*//////////////////////////////////////////////////////////////////////////
// Constructor // Constructor
//////////////////////////////////////////////////////////////////////////*/ //////////////////////////////////////////////////////////////////////////*/
//region //region Constructor
public Player(@NonNull final MainPlayer service) { public Player(@NonNull final MainPlayer service) {
this.service = service; this.service = service;
@ -450,7 +451,7 @@ public final class Player implements
/*////////////////////////////////////////////////////////////////////////// /*//////////////////////////////////////////////////////////////////////////
// Setup and initialization // Setup and initialization
//////////////////////////////////////////////////////////////////////////*/ //////////////////////////////////////////////////////////////////////////*/
//region //region Setup and initialization
public void setupFromView(@NonNull final PlayerBinding playerBinding) { public void setupFromView(@NonNull final PlayerBinding playerBinding) {
initViews(playerBinding); initViews(playerBinding);
@ -601,7 +602,7 @@ public final class Player implements
/*////////////////////////////////////////////////////////////////////////// /*//////////////////////////////////////////////////////////////////////////
// Playback initialization via intent // Playback initialization via intent
//////////////////////////////////////////////////////////////////////////*/ //////////////////////////////////////////////////////////////////////////*/
//region //region Playback initialization via intent
public void handleIntent(@NonNull final Intent intent) { public void handleIntent(@NonNull final Intent intent) {
// fail fast if no play queue was provided // fail fast if no play queue was provided
@ -629,13 +630,16 @@ public final class Player implements
playQueue.append(newQueue.getStreams()); playQueue.append(newQueue.getStreams());
if ((intent.getBooleanExtra(SELECT_ON_APPEND, false) if ((intent.getBooleanExtra(SELECT_ON_APPEND, false)
|| currentState == STATE_COMPLETED) && newQueue.getStreams().size() > 0) { || currentState == STATE_COMPLETED) && !newQueue.getStreams().isEmpty()) {
playQueue.setIndex(sizeBeforeAppend); playQueue.setIndex(sizeBeforeAppend);
} }
return; return;
} }
// needed for tablets, check the function for a better explanation
directlyOpenFullscreenIfNeeded();
final PlaybackParameters savedParameters = retrievePlaybackParametersFromPrefs(this); final PlaybackParameters savedParameters = retrievePlaybackParametersFromPrefs(this);
final float playbackSpeed = savedParameters.speed; final float playbackSpeed = savedParameters.speed;
final float playbackPitch = savedParameters.pitch; final float playbackPitch = savedParameters.pitch;
@ -687,6 +691,7 @@ public final class Player implements
&& isPlaybackResumeEnabled(this) && isPlaybackResumeEnabled(this)
&& !samePlayQueue && !samePlayQueue
&& !newQueue.isEmpty() && !newQueue.isEmpty()
&& newQueue.getItem() != null
&& newQueue.getItem().getRecoveryPosition() == PlayQueueItem.RECOVERY_UNSET) { && newQueue.getItem().getRecoveryPosition() == PlayQueueItem.RECOVERY_UNSET) {
databaseUpdateDisposable.add(recordManager.loadStreamState(newQueue.getItem()) databaseUpdateDisposable.add(recordManager.loadStreamState(newQueue.getItem())
.observeOn(AndroidSchedulers.mainThread()) .observeOn(AndroidSchedulers.mainThread())
@ -760,6 +765,22 @@ public final class Player implements
NavigationHelper.sendPlayerStartedEvent(context); NavigationHelper.sendPlayerStartedEvent(context);
} }
/**
* Open fullscreen on tablets where the option to have the main player start automatically in
* fullscreen mode is on. Rotating the device to landscape is already done in {@link
* VideoDetailFragment#openVideoPlayer(boolean)} when the thumbnail is clicked, and that's
* enough for phones, but not for tablets since the mini player can be also shown in landscape.
*/
private void directlyOpenFullscreenIfNeeded() {
if (fragmentListener != null
&& PlayerHelper.isStartMainPlayerFullscreenEnabled(service)
&& DeviceUtils.isTablet(service)
&& videoPlayerSelected()
&& PlayerHelper.globalScreenOrientationLocked(service)) {
fragmentListener.onScreenRotationButtonClicked();
}
}
private void initPlayback(@NonNull final PlayQueue queue, private void initPlayback(@NonNull final PlayQueue queue,
@RepeatMode final int repeatMode, @RepeatMode final int repeatMode,
final float playbackSpeed, final float playbackSpeed,
@ -792,7 +813,7 @@ public final class Player implements
/*////////////////////////////////////////////////////////////////////////// /*//////////////////////////////////////////////////////////////////////////
// Destroy and recovery // Destroy and recovery
//////////////////////////////////////////////////////////////////////////*/ //////////////////////////////////////////////////////////////////////////*/
//region //region Destroy and recovery
private void destroyPlayer() { private void destroyPlayer() {
if (DEBUG) { if (DEBUG) {
@ -838,7 +859,7 @@ public final class Player implements
databaseUpdateDisposable.clear(); databaseUpdateDisposable.clear();
progressUpdateDisposable.set(null); progressUpdateDisposable.set(null);
ImageLoader.getInstance().stop(); PicassoHelper.cancelTag(PicassoHelper.PLAYER_THUMBNAIL_TAG); // cancel thumbnail loading
if (binding != null) { if (binding != null) {
binding.endScreen.setImageBitmap(null); binding.endScreen.setImageBitmap(null);
@ -901,7 +922,7 @@ public final class Player implements
/*////////////////////////////////////////////////////////////////////////// /*//////////////////////////////////////////////////////////////////////////
// Player type specific setup // Player type specific setup
//////////////////////////////////////////////////////////////////////////*/ //////////////////////////////////////////////////////////////////////////*/
//region //region Player type specific setup
private void initVideoPlayer() { private void initVideoPlayer() {
// restore last resize mode // restore last resize mode
@ -963,7 +984,7 @@ public final class Player implements
/*////////////////////////////////////////////////////////////////////////// /*//////////////////////////////////////////////////////////////////////////
// Elements visibility and size: popup and main players have different look // Elements visibility and size: popup and main players have different look
//////////////////////////////////////////////////////////////////////////*/ //////////////////////////////////////////////////////////////////////////*/
//region //region Elements visibility and size: popup and main players have different look
/** /**
* This method ensures that popup and main players have different look. * This method ensures that popup and main players have different look.
@ -1083,7 +1104,7 @@ public final class Player implements
/*////////////////////////////////////////////////////////////////////////// /*//////////////////////////////////////////////////////////////////////////
// Broadcast receiver // Broadcast receiver
//////////////////////////////////////////////////////////////////////////*/ //////////////////////////////////////////////////////////////////////////*/
//region //region Broadcast receiver
private void setupBroadcastReceiver() { private void setupBroadcastReceiver() {
if (DEBUG) { if (DEBUG) {
@ -1235,18 +1256,49 @@ public final class Player implements
/*////////////////////////////////////////////////////////////////////////// /*//////////////////////////////////////////////////////////////////////////
// Thumbnail loading // Thumbnail loading
//////////////////////////////////////////////////////////////////////////*/ //////////////////////////////////////////////////////////////////////////*/
//region //region Thumbnail loading
private void initThumbnail(final String url) { private void initThumbnail(final String url) {
if (DEBUG) { if (DEBUG) {
Log.d(TAG, "Thumbnail - initThumbnail() called"); Log.d(TAG, "Thumbnail - initThumbnail() called with url = ["
+ (url == null ? "null" : url) + "]");
} }
if (url == null || url.isEmpty()) { if (isNullOrEmpty(url)) {
return; return;
} }
ImageLoader.getInstance().resume();
ImageLoader.getInstance() // scale down the notification thumbnail for performance
.loadImage(url, ImageDisplayConstants.DISPLAY_THUMBNAIL_OPTIONS, this); PicassoHelper.loadScaledDownThumbnail(context, url).into(new Target() {
@Override
public void onBitmapLoaded(final Bitmap bitmap, final Picasso.LoadedFrom from) {
if (DEBUG) {
Log.d(TAG, "Thumbnail - onLoadingComplete() called with: url = [" + url
+ "], " + "loadedImage = [" + bitmap + " -> " + bitmap.getWidth() + "x"
+ bitmap.getHeight() + "], from = [" + from + "]");
}
currentThumbnail = bitmap;
NotificationUtil.getInstance()
.createNotificationIfNeededAndUpdate(Player.this, false);
// there is a new thumbnail, so changed the end screen thumbnail, too.
updateEndScreenThumbnail();
}
@Override
public void onBitmapFailed(final Exception e, final Drawable errorDrawable) {
Log.e(TAG, "Thumbnail - onBitmapFailed() called with: url = [" + url + "]", e);
currentThumbnail = null;
NotificationUtil.getInstance()
.createNotificationIfNeededAndUpdate(Player.this, false);
}
@Override
public void onPrepareLoad(final Drawable placeHolderDrawable) {
if (DEBUG) {
Log.d(TAG, "Thumbnail - onLoadingStarted() called with: url = [" + url + "]");
}
}
});
} }
/** /**
@ -1320,61 +1372,6 @@ public final class Player implements
return Math.min(currentThumbnail.getHeight(), screenHeight); return Math.min(currentThumbnail.getHeight(), screenHeight);
} }
} }
@Override
public void onLoadingStarted(final String imageUri, final View view) {
if (DEBUG) {
Log.d(TAG, "Thumbnail - onLoadingStarted() called on: "
+ "imageUri = [" + imageUri + "], view = [" + view + "]");
}
}
@Override
public void onLoadingFailed(final String imageUri, final View view,
final FailReason failReason) {
Log.e(TAG, "Thumbnail - onLoadingFailed() called on imageUri = [" + imageUri + "]",
failReason.getCause());
currentThumbnail = null;
NotificationUtil.getInstance().createNotificationIfNeededAndUpdate(this, false);
}
@Override
public void onLoadingComplete(final String imageUri, final View view,
final Bitmap loadedImage) {
// scale down the notification thumbnail for performance
final float notificationThumbnailWidth = Math.min(
context.getResources().getDimension(R.dimen.player_notification_thumbnail_width),
loadedImage.getWidth());
currentThumbnail = Bitmap.createScaledBitmap(
loadedImage,
(int) notificationThumbnailWidth,
(int) (loadedImage.getHeight()
/ (loadedImage.getWidth() / notificationThumbnailWidth)),
true);
if (DEBUG) {
Log.d(TAG, "Thumbnail - onLoadingComplete() called with: "
+ "imageUri = [" + imageUri + "], view = [" + view + "], "
+ "loadedImage = [" + loadedImage + "], "
+ loadedImage.getWidth() + "x" + loadedImage.getHeight()
+ ", scaled notification width = " + notificationThumbnailWidth);
}
NotificationUtil.getInstance().createNotificationIfNeededAndUpdate(this, false);
// there is a new thumbnail, thus the end screen thumbnail needs to be changed, too.
updateEndScreenThumbnail();
}
@Override
public void onLoadingCancelled(final String imageUri, final View view) {
if (DEBUG) {
Log.d(TAG, "Thumbnail - onLoadingCancelled() called with: "
+ "imageUri = [" + imageUri + "], view = [" + view + "]");
}
currentThumbnail = null;
NotificationUtil.getInstance().createNotificationIfNeededAndUpdate(this, false);
}
//endregion //endregion
@ -1382,7 +1379,7 @@ public final class Player implements
/*////////////////////////////////////////////////////////////////////////// /*//////////////////////////////////////////////////////////////////////////
// Popup player utils // Popup player utils
//////////////////////////////////////////////////////////////////////////*/ //////////////////////////////////////////////////////////////////////////*/
//region //region Popup player utils
/** /**
* Check if {@link #popupLayoutParams}' position is within a arbitrary boundary * Check if {@link #popupLayoutParams}' position is within a arbitrary boundary
@ -1557,7 +1554,7 @@ public final class Player implements
/*////////////////////////////////////////////////////////////////////////// /*//////////////////////////////////////////////////////////////////////////
// Playback parameters // Playback parameters
//////////////////////////////////////////////////////////////////////////*/ //////////////////////////////////////////////////////////////////////////*/
//region //region Playback parameters
public float getPlaybackSpeed() { public float getPlaybackSpeed() {
return getPlaybackParameters().speed; return getPlaybackParameters().speed;
@ -1610,7 +1607,7 @@ public final class Player implements
/*////////////////////////////////////////////////////////////////////////// /*//////////////////////////////////////////////////////////////////////////
// Progress loop and updates // Progress loop and updates
//////////////////////////////////////////////////////////////////////////*/ //////////////////////////////////////////////////////////////////////////*/
//region //region Progress loop and updates
private void onUpdateProgress(final int currentProgress, private void onUpdateProgress(final int currentProgress,
final int duration, final int duration,
@ -1620,8 +1617,7 @@ public final class Player implements
} }
if (duration != binding.playbackSeekBar.getMax()) { if (duration != binding.playbackSeekBar.getMax()) {
binding.playbackEndTime.setText(getTimeString(duration)); setVideoDurationToControls(duration);
binding.playbackSeekBar.setMax(duration);
} }
if (currentState != STATE_PAUSED) { if (currentState != STATE_PAUSED) {
if (currentState != STATE_PAUSED_SEEK) { if (currentState != STATE_PAUSED_SEEK) {
@ -1900,7 +1896,7 @@ public final class Player implements
/*////////////////////////////////////////////////////////////////////////// /*//////////////////////////////////////////////////////////////////////////
// Controls showing / hiding // Controls showing / hiding
//////////////////////////////////////////////////////////////////////////*/ //////////////////////////////////////////////////////////////////////////*/
//region //region Controls showing / hiding
public boolean isControlsVisible() { public boolean isControlsVisible() {
return binding != null && binding.playbackControlRoot.getVisibility() == View.VISIBLE; return binding != null && binding.playbackControlRoot.getVisibility() == View.VISIBLE;
@ -2070,7 +2066,7 @@ public final class Player implements
/*////////////////////////////////////////////////////////////////////////// /*//////////////////////////////////////////////////////////////////////////
// Playback states // Playback states
//////////////////////////////////////////////////////////////////////////*/ //////////////////////////////////////////////////////////////////////////*/
//region //region Playback states
@Override // exoplayer listener @Override // exoplayer listener
public void onPlayerStateChanged(final boolean playWhenReady, final int playbackState) { public void onPlayerStateChanged(final boolean playWhenReady, final int playbackState) {
@ -2195,8 +2191,8 @@ public final class Player implements
Log.d(TAG, "onPrepared() called with: playWhenReady = [" + playWhenReady + "]"); Log.d(TAG, "onPrepared() called with: playWhenReady = [" + playWhenReady + "]");
} }
binding.playbackSeekBar.setMax((int) simpleExoPlayer.getDuration()); setVideoDurationToControls((int) simpleExoPlayer.getDuration());
binding.playbackEndTime.setText(getTimeString((int) simpleExoPlayer.getDuration()));
binding.playbackSpeed.setText(formatSpeed(getPlaybackSpeed())); binding.playbackSpeed.setText(formatSpeed(getPlaybackSpeed()));
if (playWhenReady) { if (playWhenReady) {
@ -2393,7 +2389,7 @@ public final class Player implements
/*////////////////////////////////////////////////////////////////////////// /*//////////////////////////////////////////////////////////////////////////
// Repeat and shuffle // Repeat and shuffle
//////////////////////////////////////////////////////////////////////////*/ //////////////////////////////////////////////////////////////////////////*/
//region //region Repeat and shuffle
public void onRepeatClicked() { public void onRepeatClicked() {
if (DEBUG) { if (DEBUG) {
@ -2430,7 +2426,7 @@ public final class Player implements
Log.d(TAG, "ExoPlayer - onRepeatModeChanged() called with: " Log.d(TAG, "ExoPlayer - onRepeatModeChanged() called with: "
+ "repeatMode = [" + repeatMode + "]"); + "repeatMode = [" + repeatMode + "]");
} }
setRepeatModeButton(((AppCompatImageButton) binding.repeatButton), repeatMode); setRepeatModeButton(binding.repeatButton, repeatMode);
onShuffleOrRepeatModeChanged(); onShuffleOrRepeatModeChanged();
} }
@ -2482,7 +2478,7 @@ public final class Player implements
/*////////////////////////////////////////////////////////////////////////// /*//////////////////////////////////////////////////////////////////////////
// Mute / Unmute // Mute / Unmute
//////////////////////////////////////////////////////////////////////////*/ //////////////////////////////////////////////////////////////////////////*/
//region //region Mute / Unmute
public void onMuteUnmuteButtonClicked() { public void onMuteUnmuteButtonClicked() {
if (DEBUG) { if (DEBUG) {
@ -2508,7 +2504,7 @@ public final class Player implements
/*////////////////////////////////////////////////////////////////////////// /*//////////////////////////////////////////////////////////////////////////
// ExoPlayer listeners (that didn't fit in other categories) // ExoPlayer listeners (that didn't fit in other categories)
//////////////////////////////////////////////////////////////////////////*/ //////////////////////////////////////////////////////////////////////////*/
//region //region ExoPlayer listeners (that didn't fit in other categories)
@Override @Override
public void onTimelineChanged(@NonNull final Timeline timeline, final int reason) { public void onTimelineChanged(@NonNull final Timeline timeline, final int reason) {
@ -2596,7 +2592,7 @@ public final class Player implements
/*////////////////////////////////////////////////////////////////////////// /*//////////////////////////////////////////////////////////////////////////
// Errors // Errors
//////////////////////////////////////////////////////////////////////////*/ //////////////////////////////////////////////////////////////////////////*/
//region //region Errors
/** /**
* Process exceptions produced by {@link com.google.android.exoplayer2.ExoPlayer ExoPlayer}. * Process exceptions produced by {@link com.google.android.exoplayer2.ExoPlayer ExoPlayer}.
* <p>There are multiple types of errors:</p> * <p>There are multiple types of errors:</p>
@ -2697,7 +2693,7 @@ public final class Player implements
/*////////////////////////////////////////////////////////////////////////// /*//////////////////////////////////////////////////////////////////////////
// Playback position and seek // Playback position and seek
//////////////////////////////////////////////////////////////////////////*/ //////////////////////////////////////////////////////////////////////////*/
//region //region Playback position and seek
@Override // own playback listener (this is a getter) @Override // own playback listener (this is a getter)
public boolean isApproachingPlaybackEdge(final long timeToEndMillis) { public boolean isApproachingPlaybackEdge(final long timeToEndMillis) {
@ -2840,6 +2836,20 @@ public final class Player implements
simpleExoPlayer.seekToDefaultPosition(); simpleExoPlayer.seekToDefaultPosition();
} }
} }
/**
* Sets the video duration time into all control components (e.g. seekbar).
* @param duration
*/
private void setVideoDurationToControls(final int duration) {
binding.playbackEndTime.setText(getTimeString(duration));
binding.playbackSeekBar.setMax(duration);
// This is important for Android TVs otherwise it would apply the default from
// setMax/Min methods which is (max - min) / 20
binding.playbackSeekBar.setKeyProgressIncrement(
PlayerHelper.retrieveSeekDurationFromPreferences(this));
}
//endregion //endregion
@ -2847,7 +2857,7 @@ public final class Player implements
/*////////////////////////////////////////////////////////////////////////// /*//////////////////////////////////////////////////////////////////////////
// Player actions (play, pause, previous, fast-forward, ...) // Player actions (play, pause, previous, fast-forward, ...)
//////////////////////////////////////////////////////////////////////////*/ //////////////////////////////////////////////////////////////////////////*/
//region //region Player actions (play, pause, previous, fast-forward, ...)
public void play() { public void play() {
if (DEBUG) { if (DEBUG) {
@ -2889,7 +2899,9 @@ public final class Player implements
Log.d(TAG, "onPlayPause() called"); Log.d(TAG, "onPlayPause() called");
} }
if (getPlayWhenReady()) { if (getPlayWhenReady()
// When state is completed (replay button is shown) then (re)play and do not pause
&& currentState != STATE_COMPLETED) {
pause(); pause();
} else { } else {
play(); play();
@ -2955,7 +2967,7 @@ public final class Player implements
/*////////////////////////////////////////////////////////////////////////// /*//////////////////////////////////////////////////////////////////////////
// StreamInfo history: views and progress // StreamInfo history: views and progress
//////////////////////////////////////////////////////////////////////////*/ //////////////////////////////////////////////////////////////////////////*/
//region //region StreamInfo history: views and progress
private void registerStreamViewed() { private void registerStreamViewed() {
if (currentMetadata != null) { if (currentMetadata != null) {
@ -3013,7 +3025,7 @@ public final class Player implements
/*////////////////////////////////////////////////////////////////////////// /*//////////////////////////////////////////////////////////////////////////
// Metadata // Metadata
//////////////////////////////////////////////////////////////////////////*/ //////////////////////////////////////////////////////////////////////////*/
//region //region Metadata
private void onMetadataChanged(@NonNull final MediaSourceTag tag) { private void onMetadataChanged(@NonNull final MediaSourceTag tag) {
final StreamInfo info = tag.getMetadata(); final StreamInfo info = tag.getMetadata();
@ -3137,7 +3149,7 @@ public final class Player implements
/*////////////////////////////////////////////////////////////////////////// /*//////////////////////////////////////////////////////////////////////////
// Play queue, segments and streams // Play queue, segments and streams
//////////////////////////////////////////////////////////////////////////*/ //////////////////////////////////////////////////////////////////////////*/
//region //region Play queue, segments and streams
private void maybeAutoQueueNextStream(@NonNull final MediaSourceTag metadata) { private void maybeAutoQueueNextStream(@NonNull final MediaSourceTag metadata) {
if (playQueue == null || playQueue.getIndex() != playQueue.size() - 1 if (playQueue == null || playQueue.getIndex() != playQueue.size() - 1
@ -3292,7 +3304,7 @@ public final class Player implements
private StreamSegmentAdapter.StreamSegmentListener getStreamSegmentListener() { private StreamSegmentAdapter.StreamSegmentListener getStreamSegmentListener() {
return (item, seconds) -> { return (item, seconds) -> {
segmentAdapter.selectSegment(item); segmentAdapter.selectSegment(item);
seekTo(seconds * 1000); seekTo(seconds * 1000L);
triggerProgressUpdate(); triggerProgressUpdate();
}; };
} }
@ -3302,7 +3314,7 @@ public final class Player implements
final List<StreamSegment> segments = currentMetadata.getMetadata().getStreamSegments(); final List<StreamSegment> segments = currentMetadata.getMetadata().getStreamSegments();
for (int i = 0; i < segments.size(); i++) { for (int i = 0; i < segments.size(); i++) {
if (segments.get(i).getStartTimeSeconds() * 1000 > playbackPosition) { if (segments.get(i).getStartTimeSeconds() * 1000L > playbackPosition) {
break; break;
} }
nearestPosition++; nearestPosition++;
@ -3337,9 +3349,9 @@ public final class Player implements
@Override @Override
public void held(final PlayQueueItem item, final View view) { public void held(final PlayQueueItem item, final View view) {
final int index = playQueue.indexOf(item); if (playQueue.indexOf(item) != -1) {
if (index != -1) { openPopupMenu(playQueue, item, view, true,
playQueue.remove(index); getParentActivity().getSupportFragmentManager(), context);
} }
} }
@ -3453,7 +3465,7 @@ public final class Player implements
/*////////////////////////////////////////////////////////////////////////// /*//////////////////////////////////////////////////////////////////////////
// Popup menus ("popup" means that they pop up, not that they belong to the popup player) // Popup menus ("popup" means that they pop up, not that they belong to the popup player)
//////////////////////////////////////////////////////////////////////////*/ //////////////////////////////////////////////////////////////////////////*/
//region //region Popup menus ("popup" means that they pop up, not that they belong to the popup player)
private void buildQualityMenu() { private void buildQualityMenu() {
if (qualityPopupMenu == null) { if (qualityPopupMenu == null) {
@ -3656,7 +3668,7 @@ public final class Player implements
/*////////////////////////////////////////////////////////////////////////// /*//////////////////////////////////////////////////////////////////////////
// Captions (text tracks) // Captions (text tracks)
//////////////////////////////////////////////////////////////////////////*/ //////////////////////////////////////////////////////////////////////////*/
//region //region Captions (text tracks)
private void setupSubtitleView() { private void setupSubtitleView() {
final float captionScale = PlayerHelper.getCaptionScale(context); final float captionScale = PlayerHelper.getCaptionScale(context);
@ -3735,7 +3747,7 @@ public final class Player implements
/*////////////////////////////////////////////////////////////////////////// /*//////////////////////////////////////////////////////////////////////////
// Click listeners // Click listeners
//////////////////////////////////////////////////////////////////////////*/ //////////////////////////////////////////////////////////////////////////*/
//region //region Click listeners
@Override @Override
public void onClick(final View v) { public void onClick(final View v) {
@ -3952,7 +3964,7 @@ public final class Player implements
/*////////////////////////////////////////////////////////////////////////// /*//////////////////////////////////////////////////////////////////////////
// Video size, resize, orientation, fullscreen // Video size, resize, orientation, fullscreen
//////////////////////////////////////////////////////////////////////////*/ //////////////////////////////////////////////////////////////////////////*/
//region //region Video size, resize, orientation, fullscreen
private void setupScreenRotationButton() { private void setupScreenRotationButton() {
binding.screenRotationButton.setVisibility(videoPlayerSelected() binding.screenRotationButton.setVisibility(videoPlayerSelected()
@ -4007,11 +4019,9 @@ public final class Player implements
if (DEBUG) { if (DEBUG) {
Log.d(TAG, "toggleFullscreen() called"); Log.d(TAG, "toggleFullscreen() called");
} }
if (popupPlayerSelected() || exoPlayerIsNull() || currentMetadata == null if (popupPlayerSelected() || exoPlayerIsNull() || fragmentListener == null) {
|| fragmentListener == null) {
return; return;
} }
//changeState(STATE_BLOCKED); TODO check what this does
isFullscreen = !isFullscreen; isFullscreen = !isFullscreen;
if (!isFullscreen) { if (!isFullscreen) {
@ -4066,7 +4076,7 @@ public final class Player implements
/*////////////////////////////////////////////////////////////////////////// /*//////////////////////////////////////////////////////////////////////////
// Gestures // Gestures
//////////////////////////////////////////////////////////////////////////*/ //////////////////////////////////////////////////////////////////////////*/
//region //region Gestures
@SuppressWarnings("checkstyle:ParameterNumber") @SuppressWarnings("checkstyle:ParameterNumber")
private void onLayoutChange(final View view, final int l, final int t, final int r, final int b, private void onLayoutChange(final View view, final int l, final int t, final int r, final int b,
@ -4130,7 +4140,7 @@ public final class Player implements
/*////////////////////////////////////////////////////////////////////////// /*//////////////////////////////////////////////////////////////////////////
// Activity / fragment binding // Activity / fragment binding
//////////////////////////////////////////////////////////////////////////*/ //////////////////////////////////////////////////////////////////////////*/
//region //region Activity / fragment binding
public void setFragmentListener(final PlayerServiceEventListener listener) { public void setFragmentListener(final PlayerServiceEventListener listener) {
fragmentListener = listener; fragmentListener = listener;
@ -4269,7 +4279,7 @@ public final class Player implements
/*////////////////////////////////////////////////////////////////////////// /*//////////////////////////////////////////////////////////////////////////
// Getters // Getters
//////////////////////////////////////////////////////////////////////////*/ //////////////////////////////////////////////////////////////////////////*/
//region //region Getters
public int getCurrentState() { public int getCurrentState() {
return currentState; return currentState;
@ -4544,6 +4554,7 @@ public final class Player implements
// SurfaceHolderCallback helpers // SurfaceHolderCallback helpers
//////////////////////////////////////////////////////////////////////////*/ //////////////////////////////////////////////////////////////////////////*/
//region SurfaceHolderCallback helpers //region SurfaceHolderCallback helpers
private void setupVideoSurface() { private void setupVideoSurface() {
// make sure there is nothing left over from previous calls // make sure there is nothing left over from previous calls
cleanupVideoSurface(); cleanupVideoSurface();
@ -4571,5 +4582,5 @@ public final class Player implements
} }
} }
} }
//endregion SurfaceHolderCallback helpers //endregion
} }

View file

@ -16,6 +16,7 @@ import androidx.media.AudioManagerCompat;
import com.google.android.exoplayer2.SimpleExoPlayer; import com.google.android.exoplayer2.SimpleExoPlayer;
import com.google.android.exoplayer2.analytics.AnalyticsListener; import com.google.android.exoplayer2.analytics.AnalyticsListener;
import com.google.android.exoplayer2.decoder.DecoderCounters;
public class AudioReactor implements AudioManager.OnAudioFocusChangeListener, AnalyticsListener { public class AudioReactor implements AudioManager.OnAudioFocusChangeListener, AnalyticsListener {
@ -50,6 +51,7 @@ public class AudioReactor implements AudioManager.OnAudioFocusChangeListener, An
public void dispose() { public void dispose() {
abandonAudioFocus(); abandonAudioFocus();
player.removeAnalyticsListener(this); player.removeAnalyticsListener(this);
notifyAudioSessionUpdate(false, player.getAudioSessionId());
} }
/*////////////////////////////////////////////////////////////////////////// /*//////////////////////////////////////////////////////////////////////////
@ -149,11 +151,21 @@ public class AudioReactor implements AudioManager.OnAudioFocusChangeListener, An
@Override @Override
public void onAudioSessionId(final EventTime eventTime, final int audioSessionId) { public void onAudioSessionId(final EventTime eventTime, final int audioSessionId) {
notifyAudioSessionUpdate(true, audioSessionId);
}
@Override
public void onAudioDisabled(final EventTime eventTime, final DecoderCounters counters) {
notifyAudioSessionUpdate(false, player.getAudioSessionId());
}
private void notifyAudioSessionUpdate(final boolean active, final int audioSessionId) {
if (!PlayerHelper.isUsingDSP()) { if (!PlayerHelper.isUsingDSP()) {
return; return;
} }
final Intent intent = new Intent(active
final Intent intent = new Intent(AudioEffect.ACTION_OPEN_AUDIO_EFFECT_CONTROL_SESSION); ? AudioEffect.ACTION_OPEN_AUDIO_EFFECT_CONTROL_SESSION
: AudioEffect.ACTION_CLOSE_AUDIO_EFFECT_CONTROL_SESSION);
intent.putExtra(AudioEffect.EXTRA_AUDIO_SESSION, audioSessionId); intent.putExtra(AudioEffect.EXTRA_AUDIO_SESSION, audioSessionId);
intent.putExtra(AudioEffect.EXTRA_PACKAGE_NAME, context.getPackageName()); intent.putExtra(AudioEffect.EXTRA_PACKAGE_NAME, context.getPackageName());
context.sendBroadcast(intent); context.sendBroadcast(intent);

View file

@ -20,18 +20,16 @@ public class LoadController implements LoadControl {
//////////////////////////////////////////////////////////////////////////*/ //////////////////////////////////////////////////////////////////////////*/
public LoadController() { public LoadController() {
this(PlayerHelper.getPlaybackStartBufferMs(), this(PlayerHelper.getPlaybackStartBufferMs());
PlayerHelper.getPlaybackMinimumBufferMs(),
PlayerHelper.getPlaybackOptimalBufferMs());
} }
private LoadController(final int initialPlaybackBufferMs, private LoadController(final int initialPlaybackBufferMs) {
final int minimumPlaybackBufferMs,
final int optimalPlaybackBufferMs) {
this.initialPlaybackBufferUs = initialPlaybackBufferMs * 1000; this.initialPlaybackBufferUs = initialPlaybackBufferMs * 1000;
final DefaultLoadControl.Builder builder = new DefaultLoadControl.Builder(); final DefaultLoadControl.Builder builder = new DefaultLoadControl.Builder();
builder.setBufferDurationsMs(minimumPlaybackBufferMs, optimalPlaybackBufferMs, builder.setBufferDurationsMs(
DefaultLoadControl.DEFAULT_MIN_BUFFER_MS,
DefaultLoadControl.DEFAULT_MAX_BUFFER_MS,
initialPlaybackBufferMs, initialPlaybackBufferMs,
DefaultLoadControl.DEFAULT_BUFFER_FOR_PLAYBACK_AFTER_REBUFFER_MS); DefaultLoadControl.DEFAULT_BUFFER_FOR_PLAYBACK_AFTER_REBUFFER_MS);
internalLoadControl = builder.build(); internalLoadControl = builder.build();

View file

@ -164,7 +164,7 @@ public class PlaybackParameterDialog extends DialogFragment {
setPlaybackParameters(initialTempo, initialPitch, initialSkipSilence)) setPlaybackParameters(initialTempo, initialPitch, initialSkipSilence))
.setNeutralButton(R.string.playback_reset, (dialogInterface, i) -> .setNeutralButton(R.string.playback_reset, (dialogInterface, i) ->
setPlaybackParameters(DEFAULT_TEMPO, DEFAULT_PITCH, DEFAULT_SKIP_SILENCE)) setPlaybackParameters(DEFAULT_TEMPO, DEFAULT_PITCH, DEFAULT_SKIP_SILENCE))
.setPositiveButton(R.string.finish, (dialogInterface, i) -> .setPositiveButton(R.string.ok, (dialogInterface, i) ->
setCurrentPlaybackParameters()); setCurrentPlaybackParameters());
return dialogBuilder.create(); return dialogBuilder.create();

View file

@ -239,6 +239,11 @@ public final class PlayerHelper {
.getBoolean(context.getString(R.string.brightness_gesture_control_key), true); .getBoolean(context.getString(R.string.brightness_gesture_control_key), true);
} }
public static boolean isStartMainPlayerFullscreenEnabled(@NonNull final Context context) {
return getPreferences(context)
.getBoolean(context.getString(R.string.start_main_player_fullscreen_key), false);
}
public static boolean isAutoQueueEnabled(@NonNull final Context context) { public static boolean isAutoQueueEnabled(@NonNull final Context context) {
return getPreferences(context) return getPreferences(context)
.getBoolean(context.getString(R.string.auto_queue_key), false); .getBoolean(context.getString(R.string.auto_queue_key), false);
@ -307,22 +312,6 @@ public final class PlayerHelper {
return 500; return 500;
} }
/**
* @return the minimum number of milliseconds the player always buffers to
* after starting playback.
*/
public static int getPlaybackMinimumBufferMs() {
return 25000;
}
/**
* @return the maximum/optimal number of milliseconds the player will buffer to once the buffer
* hits the point of {@link #getPlaybackMinimumBufferMs()}.
*/
public static int getPlaybackOptimalBufferMs() {
return 60000;
}
public static TrackSelection.Factory getQualitySelector() { public static TrackSelection.Factory getQualitySelector() {
return new AdaptiveTrackSelection.Factory( return new AdaptiveTrackSelection.Factory(
1000, 1000,

View file

@ -28,6 +28,7 @@ public class PlayQueueItem implements Serializable {
private final String thumbnailUrl; private final String thumbnailUrl;
@NonNull @NonNull
private final String uploader; private final String uploader;
private final String uploaderUrl;
@NonNull @NonNull
private final StreamType streamType; private final StreamType streamType;
@ -40,7 +41,8 @@ public class PlayQueueItem implements Serializable {
PlayQueueItem(@NonNull final StreamInfo info) { PlayQueueItem(@NonNull final StreamInfo info) {
this(info.getName(), info.getUrl(), info.getServiceId(), info.getDuration(), this(info.getName(), info.getUrl(), info.getServiceId(), info.getDuration(),
info.getThumbnailUrl(), info.getUploaderName(), info.getStreamType()); info.getThumbnailUrl(), info.getUploaderName(),
info.getUploaderUrl(), info.getStreamType());
if (info.getStartPosition() > 0) { if (info.getStartPosition() > 0) {
setRecoveryPosition(info.getStartPosition() * 1000); setRecoveryPosition(info.getStartPosition() * 1000);
@ -49,19 +51,21 @@ public class PlayQueueItem implements Serializable {
PlayQueueItem(@NonNull final StreamInfoItem item) { PlayQueueItem(@NonNull final StreamInfoItem item) {
this(item.getName(), item.getUrl(), item.getServiceId(), item.getDuration(), this(item.getName(), item.getUrl(), item.getServiceId(), item.getDuration(),
item.getThumbnailUrl(), item.getUploaderName(), item.getStreamType()); item.getThumbnailUrl(), item.getUploaderName(),
item.getUploaderUrl(), item.getStreamType());
} }
private PlayQueueItem(@Nullable final String name, @Nullable final String url, private PlayQueueItem(@Nullable final String name, @Nullable final String url,
final int serviceId, final long duration, final int serviceId, final long duration,
@Nullable final String thumbnailUrl, @Nullable final String uploader, @Nullable final String thumbnailUrl, @Nullable final String uploader,
@NonNull final StreamType streamType) { final String uploaderUrl, @NonNull final StreamType streamType) {
this.title = name != null ? name : EMPTY_STRING; this.title = name != null ? name : EMPTY_STRING;
this.url = url != null ? url : EMPTY_STRING; this.url = url != null ? url : EMPTY_STRING;
this.serviceId = serviceId; this.serviceId = serviceId;
this.duration = duration; this.duration = duration;
this.thumbnailUrl = thumbnailUrl != null ? thumbnailUrl : EMPTY_STRING; this.thumbnailUrl = thumbnailUrl != null ? thumbnailUrl : EMPTY_STRING;
this.uploader = uploader != null ? uploader : EMPTY_STRING; this.uploader = uploader != null ? uploader : EMPTY_STRING;
this.uploaderUrl = uploaderUrl;
this.streamType = streamType; this.streamType = streamType;
this.recoveryPosition = RECOVERY_UNSET; this.recoveryPosition = RECOVERY_UNSET;
@ -95,6 +99,10 @@ public class PlayQueueItem implements Serializable {
return uploader; return uploader;
} }
public String getUploaderUrl() {
return uploaderUrl;
}
@NonNull @NonNull
public StreamType getStreamType() { public StreamType getStreamType() {
return streamType; return streamType;

View file

@ -5,11 +5,9 @@ import android.text.TextUtils;
import android.view.MotionEvent; import android.view.MotionEvent;
import android.view.View; import android.view.View;
import com.nostra13.universalimageloader.core.ImageLoader;
import org.schabi.newpipe.extractor.NewPipe; import org.schabi.newpipe.extractor.NewPipe;
import org.schabi.newpipe.util.ImageDisplayConstants;
import org.schabi.newpipe.util.Localization; import org.schabi.newpipe.util.Localization;
import org.schabi.newpipe.util.PicassoHelper;
public class PlayQueueItemBuilder { public class PlayQueueItemBuilder {
private static final String TAG = PlayQueueItemBuilder.class.toString(); private static final String TAG = PlayQueueItemBuilder.class.toString();
@ -35,8 +33,7 @@ public class PlayQueueItemBuilder {
holder.itemDurationView.setVisibility(View.GONE); holder.itemDurationView.setVisibility(View.GONE);
} }
ImageLoader.getInstance().displayImage(item.getThumbnailUrl(), holder.itemThumbnailView, PicassoHelper.loadThumbnail(item.getThumbnailUrl()).into(holder.itemThumbnailView);
ImageDisplayConstants.DISPLAY_THUMBNAIL_OPTIONS);
holder.itemRoot.setOnClickListener(view -> { holder.itemRoot.setOnClickListener(view -> {
if (onItemClickListener != null) { if (onItemClickListener != null) {

View file

@ -1,16 +1,18 @@
package org.schabi.newpipe.player.seekbarpreview; package org.schabi.newpipe.player.seekbarpreview;
import static org.schabi.newpipe.player.seekbarpreview.SeekbarPreviewThumbnailHelper.SeekbarPreviewThumbnailType;
import android.content.Context; import android.content.Context;
import android.graphics.Bitmap; import android.graphics.Bitmap;
import android.util.Log; import android.util.Log;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.google.common.base.Stopwatch; import com.google.common.base.Stopwatch;
import com.nostra13.universalimageloader.core.ImageLoader;
import org.schabi.newpipe.extractor.stream.Frameset; import org.schabi.newpipe.extractor.stream.Frameset;
import org.schabi.newpipe.util.ImageDisplayConstants; import org.schabi.newpipe.util.PicassoHelper;
import java.util.Comparator; import java.util.Comparator;
import java.util.HashMap; import java.util.HashMap;
@ -21,11 +23,8 @@ import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ExecutorService; import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors; import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.function.Supplier; import java.util.function.Supplier;
import static org.schabi.newpipe.player.seekbarpreview.SeekbarPreviewThumbnailHelper.SeekbarPreviewThumbnailType;
public class SeekbarPreviewThumbnailHolder { public class SeekbarPreviewThumbnailHolder {
// This has to be <= 23 chars on devices running Android 7 or lower (API <= 25) // This has to be <= 23 chars on devices running Android 7 or lower (API <= 25)
@ -174,6 +173,7 @@ public class SeekbarPreviewThumbnailHolder {
} }
} }
@Nullable
private Bitmap getBitMapFrom(final String url) { private Bitmap getBitMapFrom(final String url) {
if (url == null) { if (url == null) {
Log.w(TAG, "url is null; This should never happen"); Log.w(TAG, "url is null; This should never happen");
@ -182,24 +182,11 @@ public class SeekbarPreviewThumbnailHolder {
final Stopwatch sw = Log.isLoggable(TAG, Log.DEBUG) ? Stopwatch.createStarted() : null; final Stopwatch sw = Log.isLoggable(TAG, Log.DEBUG) ? Stopwatch.createStarted() : null;
try { try {
final SyncImageLoadingListener syncImageLoadingListener =
new SyncImageLoadingListener();
Log.d(TAG, "Downloading bitmap for seekbarPreview from '" + url + "'"); Log.d(TAG, "Downloading bitmap for seekbarPreview from '" + url + "'");
// Ensure that everything is running // Gets the bitmap within the timeout of 15 seconds imposed by default by OkHttpClient
ImageLoader.getInstance().resume();
// Load the image
// Impl-Note:
// Ensure that your are not running on the main-Thread this will otherwise hang // Ensure that your are not running on the main-Thread this will otherwise hang
ImageLoader.getInstance().loadImage( final Bitmap bitmap = PicassoHelper.loadSeekbarThumbnailPreview(url).get();
url,
ImageDisplayConstants.DISPLAY_SEEKBAR_PREVIEW_OPTIONS,
syncImageLoadingListener);
// Get the bitmap within the timeout
final Bitmap bitmap =
syncImageLoadingListener.waitForBitmapOrThrow(30, TimeUnit.SECONDS);
if (sw != null) { if (sw != null) {
Log.d(TAG, Log.d(TAG,

View file

@ -1,87 +0,0 @@
package org.schabi.newpipe.player.seekbarpreview;
import android.graphics.Bitmap;
import android.view.View;
import com.nostra13.universalimageloader.core.assist.FailReason;
import com.nostra13.universalimageloader.core.listener.SimpleImageLoadingListener;
import java.util.concurrent.CancellationException;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
/**
* Listener for synchronously downloading of an image/bitmap.
*/
public class SyncImageLoadingListener extends SimpleImageLoadingListener {
private final CountDownLatch countDownLatch = new CountDownLatch(1);
private Bitmap bitmap;
private boolean cancelled = false;
private FailReason failReason = null;
@SuppressWarnings("checkstyle:HiddenField")
@Override
public void onLoadingFailed(
final String imageUri,
final View view,
final FailReason failReason) {
this.failReason = failReason;
countDownLatch.countDown();
}
@Override
public void onLoadingComplete(
final String imageUri,
final View view,
final Bitmap loadedImage) {
bitmap = loadedImage;
countDownLatch.countDown();
}
@Override
public void onLoadingCancelled(final String imageUri, final View view) {
cancelled = true;
countDownLatch.countDown();
}
public Bitmap waitForBitmapOrThrow(final long timeout, final TimeUnit timeUnit)
throws InterruptedException, TimeoutException {
// Wait for the download to finish
if (!countDownLatch.await(timeout, timeUnit)) {
throw new TimeoutException("Couldn't get the image in time");
}
if (isCancelled()) {
throw new CancellationException("Download of image was cancelled");
}
if (getFailReason() != null) {
throw new RuntimeException("Failed to download image" + getFailReason().getType(),
getFailReason().getCause());
}
if (getBitmap() == null) {
throw new NullPointerException("Bitmap is null");
}
return getBitmap();
}
public Bitmap getBitmap() {
return bitmap;
}
public boolean isCancelled() {
return cancelled;
}
public FailReason getFailReason() {
return failReason;
}
}

View file

@ -17,8 +17,6 @@ import androidx.core.content.ContextCompat;
import androidx.preference.Preference; import androidx.preference.Preference;
import androidx.preference.PreferenceManager; import androidx.preference.PreferenceManager;
import com.nostra13.universalimageloader.core.ImageLoader;
import org.schabi.newpipe.DownloaderImpl; import org.schabi.newpipe.DownloaderImpl;
import org.schabi.newpipe.NewPipeDatabase; import org.schabi.newpipe.NewPipeDatabase;
import org.schabi.newpipe.R; import org.schabi.newpipe.R;
@ -29,6 +27,7 @@ import org.schabi.newpipe.extractor.localization.ContentCountry;
import org.schabi.newpipe.extractor.localization.Localization; import org.schabi.newpipe.extractor.localization.Localization;
import org.schabi.newpipe.streams.io.StoredFileHelper; import org.schabi.newpipe.streams.io.StoredFileHelper;
import org.schabi.newpipe.util.NavigationHelper; import org.schabi.newpipe.util.NavigationHelper;
import org.schabi.newpipe.util.PicassoHelper;
import org.schabi.newpipe.util.ZipHelper; import org.schabi.newpipe.util.ZipHelper;
import java.io.File; import java.io.File;
@ -50,7 +49,6 @@ public class ContentSettingsFragment extends BasePreferenceFragment {
private ContentSettingsManager manager; private ContentSettingsManager manager;
private String importExportDataPathKey; private String importExportDataPathKey;
private String thumbnailLoadToggleKey;
private String youtubeRestrictedModeEnabledKey; private String youtubeRestrictedModeEnabledKey;
private Localization initialSelectedLocalization; private Localization initialSelectedLocalization;
@ -69,7 +67,6 @@ public class ContentSettingsFragment extends BasePreferenceFragment {
manager.deleteSettingsFile(); manager.deleteSettingsFile();
importExportDataPathKey = getString(R.string.import_export_data_path); importExportDataPathKey = getString(R.string.import_export_data_path);
thumbnailLoadToggleKey = getString(R.string.download_thumbnail_key);
youtubeRestrictedModeEnabledKey = getString(R.string.youtube_restricted_mode_enabled); youtubeRestrictedModeEnabledKey = getString(R.string.youtube_restricted_mode_enabled);
addPreferencesFromResource(R.xml.content_settings); addPreferencesFromResource(R.xml.content_settings);
@ -77,7 +74,8 @@ public class ContentSettingsFragment extends BasePreferenceFragment {
final Preference importDataPreference = requirePreference(R.string.import_data); final Preference importDataPreference = requirePreference(R.string.import_data);
importDataPreference.setOnPreferenceClickListener((Preference p) -> { importDataPreference.setOnPreferenceClickListener((Preference p) -> {
requestImportPathLauncher.launch( requestImportPathLauncher.launch(
StoredFileHelper.getPicker(requireContext(), getImportExportDataUri())); StoredFileHelper.getPicker(requireContext(),
ZIP_MIME_TYPE, getImportExportDataUri()));
return true; return true;
}); });
@ -95,8 +93,7 @@ public class ContentSettingsFragment extends BasePreferenceFragment {
.getPreferredLocalization(requireContext()); .getPreferredLocalization(requireContext());
initialSelectedContentCountry = org.schabi.newpipe.util.Localization initialSelectedContentCountry = org.schabi.newpipe.util.Localization
.getPreferredContentCountry(requireContext()); .getPreferredContentCountry(requireContext());
initialLanguage = PreferenceManager initialLanguage = defaultPreferences.getString(getString(R.string.app_language_key), "en");
.getDefaultSharedPreferences(requireContext()).getString("app_language_key", "en");
final Preference clearCookiePref = requirePreference(R.string.clear_cookie_key); final Preference clearCookiePref = requirePreference(R.string.clear_cookie_key);
clearCookiePref.setOnPreferenceClickListener(preference -> { clearCookiePref.setOnPreferenceClickListener(preference -> {
@ -112,30 +109,30 @@ public class ContentSettingsFragment extends BasePreferenceFragment {
if (defaultPreferences.getString(getString(R.string.recaptcha_cookies_key), "").isEmpty()) { if (defaultPreferences.getString(getString(R.string.recaptcha_cookies_key), "").isEmpty()) {
clearCookiePref.setVisible(false); clearCookiePref.setVisible(false);
} }
findPreference(getString(R.string.download_thumbnail_key)).setOnPreferenceChangeListener(
(preference, newValue) -> {
PicassoHelper.setShouldLoadImages((Boolean) newValue);
try {
PicassoHelper.clearCache(preference.getContext());
Toast.makeText(preference.getContext(),
R.string.thumbnail_cache_wipe_complete_notice, Toast.LENGTH_SHORT)
.show();
} catch (final IOException e) {
Log.e(TAG, "Unable to clear Picasso cache", e);
}
return true;
});
} }
@Override @Override
public boolean onPreferenceTreeClick(final Preference preference) { public boolean onPreferenceTreeClick(final Preference preference) {
final String key = preference.getKey(); if (preference.getKey().equals(youtubeRestrictedModeEnabledKey)) {
if (key != null) { final Context context = getContext();
if (key.equals(thumbnailLoadToggleKey)) { if (context != null) {
final ImageLoader imageLoader = ImageLoader.getInstance(); DownloaderImpl.getInstance().updateYoutubeRestrictedModeCookies(context);
imageLoader.stop(); } else {
imageLoader.clearDiskCache(); Log.w(TAG, "onPreferenceTreeClick: null context");
imageLoader.clearMemoryCache();
imageLoader.resume();
Toast.makeText(preference.getContext(),
R.string.thumbnail_cache_wipe_complete_notice,
Toast.LENGTH_SHORT).show();
}
if (key.equals(youtubeRestrictedModeEnabledKey)) {
final Context context = getContext();
if (context != null) {
DownloaderImpl.getInstance().updateYoutubeRestrictedModeCookies(context);
} else {
Log.w(TAG, "onPreferenceTreeClick: null context");
}
} }
} }
@ -150,8 +147,8 @@ public class ContentSettingsFragment extends BasePreferenceFragment {
.getPreferredLocalization(requireContext()); .getPreferredLocalization(requireContext());
final ContentCountry selectedContentCountry = org.schabi.newpipe.util.Localization final ContentCountry selectedContentCountry = org.schabi.newpipe.util.Localization
.getPreferredContentCountry(requireContext()); .getPreferredContentCountry(requireContext());
final String selectedLanguage = PreferenceManager final String selectedLanguage =
.getDefaultSharedPreferences(requireContext()).getString("app_language_key", "en"); defaultPreferences.getString(getString(R.string.app_language_key), "en");
if (!selectedLocalization.equals(initialSelectedLocalization) if (!selectedLocalization.equals(initialSelectedLocalization)
|| !selectedContentCountry.equals(initialSelectedContentCountry) || !selectedContentCountry.equals(initialSelectedContentCountry)
@ -187,7 +184,7 @@ public class ContentSettingsFragment extends BasePreferenceFragment {
new AlertDialog.Builder(requireActivity()) new AlertDialog.Builder(requireActivity())
.setMessage(R.string.override_current_data) .setMessage(R.string.override_current_data)
.setPositiveButton(R.string.finish, (d, id) -> .setPositiveButton(R.string.ok, (d, id) ->
importDatabase(file, lastImportDataUri)) importDatabase(file, lastImportDataUri))
.setNegativeButton(R.string.cancel, (d, id) -> .setNegativeButton(R.string.cancel, (d, id) ->
d.cancel()) d.cancel())
@ -235,11 +232,11 @@ public class ContentSettingsFragment extends BasePreferenceFragment {
final AlertDialog.Builder alert = new AlertDialog.Builder(requireContext()); final AlertDialog.Builder alert = new AlertDialog.Builder(requireContext());
alert.setTitle(R.string.import_settings); alert.setTitle(R.string.import_settings);
alert.setNegativeButton(android.R.string.no, (dialog, which) -> { alert.setNegativeButton(R.string.cancel, (dialog, which) -> {
dialog.dismiss(); dialog.dismiss();
finishImport(importDataUri); finishImport(importDataUri);
}); });
alert.setPositiveButton(getString(R.string.finish), (dialog, which) -> { alert.setPositiveButton(R.string.ok, (dialog, which) -> {
dialog.dismiss(); dialog.dismiss();
manager.loadSharedPreferences(PreferenceManager manager.loadSharedPreferences(PreferenceManager
.getDefaultSharedPreferences(requireContext())); .getDefaultSharedPreferences(requireContext()));

View file

@ -179,7 +179,7 @@ public class DownloadSettingsFragment extends BasePreferenceFragment {
final AlertDialog.Builder msg = new AlertDialog.Builder(ctx); final AlertDialog.Builder msg = new AlertDialog.Builder(ctx);
msg.setTitle(title); msg.setTitle(title);
msg.setMessage(message); msg.setMessage(message);
msg.setPositiveButton(getString(R.string.finish), null); msg.setPositiveButton(getString(R.string.ok), null);
msg.show(); msg.show();
} }

View file

@ -6,6 +6,7 @@ import android.os.Build;
import android.os.Environment; import android.os.Environment;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.StringRes;
import androidx.preference.PreferenceManager; import androidx.preference.PreferenceManager;
import org.schabi.newpipe.R; import org.schabi.newpipe.R;
@ -59,6 +60,10 @@ public final class NewPipeSettings {
isFirstRun = true; isFirstRun = true;
} }
// first run migrations, then setDefaultValues, since the latter requires the correct types
SettingMigrations.initMigrations(context, isFirstRun);
// readAgain is true so that if new settings are added their default value is set
PreferenceManager.setDefaultValues(context, R.xml.main_settings, true); PreferenceManager.setDefaultValues(context, R.xml.main_settings, true);
PreferenceManager.setDefaultValues(context, R.xml.video_audio_settings, true); PreferenceManager.setDefaultValues(context, R.xml.video_audio_settings, true);
PreferenceManager.setDefaultValues(context, R.xml.download_settings, true); PreferenceManager.setDefaultValues(context, R.xml.download_settings, true);
@ -72,8 +77,6 @@ public final class NewPipeSettings {
saveDefaultVideoDownloadDirectory(context); saveDefaultVideoDownloadDirectory(context);
saveDefaultAudioDownloadDirectory(context); saveDefaultAudioDownloadDirectory(context);
SettingMigrations.initMigrations(context, isFirstRun);
} }
static void saveDefaultVideoDownloadDirectory(final Context context) { static void saveDefaultVideoDownloadDirectory(final Context context) {
@ -125,4 +128,29 @@ public final class NewPipeSettings {
return prefs.getBoolean(key, true); return prefs.getBoolean(key, true);
} }
private static boolean showSearchSuggestions(final Context context,
final SharedPreferences sharedPreferences,
@StringRes final int key) {
final Set<String> enabledSearchSuggestions = sharedPreferences.getStringSet(
context.getString(R.string.show_search_suggestions_key), null);
if (enabledSearchSuggestions == null) {
return true; // defaults to true
} else {
return enabledSearchSuggestions.contains(context.getString(key));
}
}
public static boolean showLocalSearchSuggestions(final Context context,
final SharedPreferences sharedPreferences) {
return showSearchSuggestions(context, sharedPreferences,
R.string.show_local_search_suggestions_key);
}
public static boolean showRemoteSearchSuggestions(final Context context,
final SharedPreferences sharedPreferences) {
return showSearchSuggestions(context, sharedPreferences,
R.string.show_remote_search_suggestions_key);
}
} }

View file

@ -218,7 +218,7 @@ public class PeertubeInstanceListFragment extends Fragment {
.setIcon(R.drawable.place_holder_peertube) .setIcon(R.drawable.place_holder_peertube)
.setView(dialogBinding.getRoot()) .setView(dialogBinding.getRoot())
.setNegativeButton(R.string.cancel, null) .setNegativeButton(R.string.cancel, null)
.setPositiveButton(R.string.finish, (dialog1, which) -> { .setPositiveButton(R.string.ok, (dialog1, which) -> {
final String url = dialogBinding.dialogEditText.getText().toString(); final String url = dialogBinding.dialogEditText.getText().toString();
addInstance(url); addInstance(url);
}) })

View file

@ -14,13 +14,11 @@ import androidx.fragment.app.DialogFragment;
import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView; import androidx.recyclerview.widget.RecyclerView;
import com.nostra13.universalimageloader.core.DisplayImageOptions;
import com.nostra13.universalimageloader.core.ImageLoader;
import org.schabi.newpipe.R; import org.schabi.newpipe.R;
import org.schabi.newpipe.database.subscription.SubscriptionEntity; import org.schabi.newpipe.database.subscription.SubscriptionEntity;
import org.schabi.newpipe.error.ErrorActivity; import org.schabi.newpipe.error.ErrorActivity;
import org.schabi.newpipe.local.subscription.SubscriptionManager; import org.schabi.newpipe.local.subscription.SubscriptionManager;
import org.schabi.newpipe.util.PicassoHelper;
import org.schabi.newpipe.util.ThemeHelper; import org.schabi.newpipe.util.ThemeHelper;
import java.util.List; import java.util.List;
@ -54,13 +52,6 @@ import io.reactivex.rxjava3.schedulers.Schedulers;
*/ */
public class SelectChannelFragment extends DialogFragment { public class SelectChannelFragment extends DialogFragment {
/**
* This contains the base display options for images.
*/
private static final DisplayImageOptions DISPLAY_IMAGE_OPTIONS
= new DisplayImageOptions.Builder().cacheInMemory(true).build();
private final ImageLoader imageLoader = ImageLoader.getInstance();
private OnSelectedListener onSelectedListener = null; private OnSelectedListener onSelectedListener = null;
private OnCancelListener onCancelListener = null; private OnCancelListener onCancelListener = null;
@ -199,8 +190,7 @@ public class SelectChannelFragment extends DialogFragment {
final SubscriptionEntity entry = subscriptions.get(position); final SubscriptionEntity entry = subscriptions.get(position);
holder.titleView.setText(entry.getName()); holder.titleView.setText(entry.getName());
holder.view.setOnClickListener(view -> clickedItem(position)); holder.view.setOnClickListener(view -> clickedItem(position));
imageLoader.displayImage(entry.getAvatarUrl(), holder.thumbnailView, PicassoHelper.loadAvatar(entry.getAvatarUrl()).into(holder.thumbnailView);
DISPLAY_IMAGE_OPTIONS);
} }
@Override @Override

View file

@ -14,9 +14,6 @@ import androidx.fragment.app.DialogFragment;
import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView; import androidx.recyclerview.widget.RecyclerView;
import com.nostra13.universalimageloader.core.DisplayImageOptions;
import com.nostra13.universalimageloader.core.ImageLoader;
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.AppDatabase; import org.schabi.newpipe.database.AppDatabase;
@ -29,6 +26,7 @@ import org.schabi.newpipe.error.ErrorInfo;
import org.schabi.newpipe.error.UserAction; import org.schabi.newpipe.error.UserAction;
import org.schabi.newpipe.local.playlist.LocalPlaylistManager; import org.schabi.newpipe.local.playlist.LocalPlaylistManager;
import org.schabi.newpipe.local.playlist.RemotePlaylistManager; import org.schabi.newpipe.local.playlist.RemotePlaylistManager;
import org.schabi.newpipe.util.PicassoHelper;
import java.util.List; import java.util.List;
import java.util.Vector; import java.util.Vector;
@ -38,13 +36,6 @@ import io.reactivex.rxjava3.core.Flowable;
import io.reactivex.rxjava3.disposables.Disposable; import io.reactivex.rxjava3.disposables.Disposable;
public class SelectPlaylistFragment extends DialogFragment { public class SelectPlaylistFragment extends DialogFragment {
/**
* This contains the base display options for images.
*/
private static final DisplayImageOptions DISPLAY_IMAGE_OPTIONS
= new DisplayImageOptions.Builder().cacheInMemory(true).build();
private final ImageLoader imageLoader = ImageLoader.getInstance();
private OnSelectedListener onSelectedListener = null; private OnSelectedListener onSelectedListener = null;
@ -170,16 +161,15 @@ public class SelectPlaylistFragment extends DialogFragment {
holder.titleView.setText(entry.name); holder.titleView.setText(entry.name);
holder.view.setOnClickListener(view -> clickedItem(position)); holder.view.setOnClickListener(view -> clickedItem(position));
imageLoader.displayImage(entry.thumbnailUrl, holder.thumbnailView, PicassoHelper.loadPlaylistThumbnail(entry.thumbnailUrl).into(holder.thumbnailView);
DISPLAY_IMAGE_OPTIONS);
} else if (selectedItem instanceof PlaylistRemoteEntity) { } else if (selectedItem instanceof PlaylistRemoteEntity) {
final PlaylistRemoteEntity entry = ((PlaylistRemoteEntity) selectedItem); final PlaylistRemoteEntity entry = ((PlaylistRemoteEntity) selectedItem);
holder.titleView.setText(entry.getName()); holder.titleView.setText(entry.getName());
holder.view.setOnClickListener(view -> clickedItem(position)); holder.view.setOnClickListener(view -> clickedItem(position));
imageLoader.displayImage(entry.getThumbnailUrl(), holder.thumbnailView, PicassoHelper.loadPlaylistThumbnail(entry.getThumbnailUrl())
DISPLAY_IMAGE_OPTIONS); .into(holder.thumbnailView);
} }
} }

View file

@ -13,14 +13,22 @@ import org.schabi.newpipe.error.ErrorInfo;
import org.schabi.newpipe.error.UserAction; import org.schabi.newpipe.error.UserAction;
import org.schabi.newpipe.util.DeviceUtils; import org.schabi.newpipe.util.DeviceUtils;
import java.util.Collections;
import java.util.HashSet;
import java.util.Set;
import static org.schabi.newpipe.MainActivity.DEBUG; import static org.schabi.newpipe.MainActivity.DEBUG;
/**
* In order to add a migration, follow these steps, given P is the previous version:<br>
* - in the class body add a new {@code MIGRATION_P_P+1 = new Migration(P, P+1) { ... }} and put in
* the {@code migrate()} method the code that need to be run when migrating from P to P+1<br>
* - add {@code MIGRATION_P_P+1} at the end of {@link SettingMigrations#SETTING_MIGRATIONS}<br>
* - increment {@link SettingMigrations#VERSION}'s value by 1 (so it should become P+1)
*/
public final class SettingMigrations { public final class SettingMigrations {
private static final String TAG = SettingMigrations.class.toString(); private static final String TAG = SettingMigrations.class.toString();
/**
* Version number for preferences. Must be incremented every time a migration is necessary.
*/
public static final int VERSION = 3;
private static SharedPreferences sp; private static SharedPreferences sp;
public static final Migration MIGRATION_0_1 = new Migration(0, 1) { public static final Migration MIGRATION_0_1 = new Migration(0, 1) {
@ -72,6 +80,35 @@ public final class SettingMigrations {
} }
}; };
public static final Migration MIGRATION_3_4 = new Migration(3, 4) {
@Override
protected void migrate(final Context context) {
// Pull request #3546 added support for choosing the type of search suggestions to
// show, replacing the on-off switch used before, so migrate the previous user choice
final String showSearchSuggestionsKey =
context.getString(R.string.show_search_suggestions_key);
boolean addAllSearchSuggestionTypes;
try {
addAllSearchSuggestionTypes = sp.getBoolean(showSearchSuggestionsKey, true);
} catch (final ClassCastException e) {
// just in case it was not a boolean for some reason, let's consider it a "true"
addAllSearchSuggestionTypes = true;
}
final Set<String> showSearchSuggestionsValueList = new HashSet<>();
if (addAllSearchSuggestionTypes) {
// if the preference was true, all suggestions will be shown, otherwise none
Collections.addAll(showSearchSuggestionsValueList, context.getResources()
.getStringArray(R.array.show_search_suggestions_value_list));
}
sp.edit().putStringSet(
showSearchSuggestionsKey, showSearchSuggestionsValueList).apply();
}
};
/** /**
* List of all implemented migrations. * List of all implemented migrations.
* <p> * <p>
@ -81,9 +118,15 @@ public final class SettingMigrations {
private static final Migration[] SETTING_MIGRATIONS = { private static final Migration[] SETTING_MIGRATIONS = {
MIGRATION_0_1, MIGRATION_0_1,
MIGRATION_1_2, MIGRATION_1_2,
MIGRATION_2_3 MIGRATION_2_3,
MIGRATION_3_4,
}; };
/**
* Version number for preferences. Must be incremented every time a migration is necessary.
*/
public static final int VERSION = 4;
public static void initMigrations(final Context context, final boolean isFirstRun) { public static void initMigrations(final Context context, final boolean isFirstRun) {
// setup migrations and check if there is something to do // setup migrations and check if there is something to do

View file

@ -459,11 +459,12 @@ public class StoredFileHelper implements Serializable {
return !str1.equals(str2); return !str1.equals(str2);
} }
public static Intent getPicker(@NonNull final Context ctx) { public static Intent getPicker(@NonNull final Context ctx,
@NonNull final String mimeType) {
if (NewPipeSettings.useStorageAccessFramework(ctx)) { if (NewPipeSettings.useStorageAccessFramework(ctx)) {
return new Intent(Intent.ACTION_OPEN_DOCUMENT) return new Intent(Intent.ACTION_OPEN_DOCUMENT)
.putExtra("android.content.extra.SHOW_ADVANCED", true) .putExtra("android.content.extra.SHOW_ADVANCED", true)
.setType("*/*") .setType(mimeType)
.addCategory(Intent.CATEGORY_OPENABLE) .addCategory(Intent.CATEGORY_OPENABLE)
.addFlags(Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION .addFlags(Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION
| StoredDirectoryHelper.PERMISSION_FLAGS); | StoredDirectoryHelper.PERMISSION_FLAGS);
@ -477,8 +478,10 @@ public class StoredFileHelper implements Serializable {
} }
} }
public static Intent getPicker(@NonNull final Context ctx, @Nullable final Uri initialPath) { public static Intent getPicker(@NonNull final Context ctx,
return applyInitialPathToPickerIntent(ctx, getPicker(ctx), initialPath, null); @NonNull final String mimeType,
@Nullable final Uri initialPath) {
return applyInitialPathToPickerIntent(ctx, getPicker(ctx, mimeType), initialPath, null);
} }
public static Intent getNewPicker(@NonNull final Context ctx, public static Intent getNewPicker(@NonNull final Context ctx,

View file

@ -11,6 +11,7 @@ import android.view.KeyEvent;
import androidx.annotation.Dimension; import androidx.annotation.Dimension;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.appcompat.app.AppCompatActivity;
import androidx.core.content.ContextCompat; import androidx.core.content.ContextCompat;
import androidx.preference.PreferenceManager; import androidx.preference.PreferenceManager;
@ -130,4 +131,13 @@ public final class DeviceUtils {
&& !HI3798MV200 && !HI3798MV200
&& !CVT_MT5886_EU_1G; && !CVT_MT5886_EU_1G;
} }
public static boolean isLandscape(final Context context) {
return context.getResources().getDisplayMetrics().heightPixels < context.getResources()
.getDisplayMetrics().widthPixels;
}
public static boolean isInMultiWindow(final AppCompatActivity activity) {
return Build.VERSION.SDK_INT >= Build.VERSION_CODES.N && activity.isInMultiWindowMode();
}
} }

View file

@ -1,65 +0,0 @@
package org.schabi.newpipe.util;
import android.graphics.Bitmap;
import com.nostra13.universalimageloader.core.DisplayImageOptions;
import com.nostra13.universalimageloader.core.assist.ImageScaleType;
import com.nostra13.universalimageloader.core.display.FadeInBitmapDisplayer;
import org.schabi.newpipe.R;
public final class ImageDisplayConstants {
private static final int BITMAP_FADE_IN_DURATION_MILLIS = 250;
/**
* This constant contains the base display options.
*/
private static final DisplayImageOptions BASE_DISPLAY_IMAGE_OPTIONS =
new DisplayImageOptions.Builder()
.cacheInMemory(true)
.cacheOnDisk(true)
.resetViewBeforeLoading(true)
.bitmapConfig(Bitmap.Config.RGB_565)
.imageScaleType(ImageScaleType.EXACTLY)
.displayer(new FadeInBitmapDisplayer(BITMAP_FADE_IN_DURATION_MILLIS))
.build();
/*//////////////////////////////////////////////////////////////////////////
// DisplayImageOptions default configurations
//////////////////////////////////////////////////////////////////////////*/
public static final DisplayImageOptions DISPLAY_AVATAR_OPTIONS =
new DisplayImageOptions.Builder()
.cloneFrom(BASE_DISPLAY_IMAGE_OPTIONS)
.showImageForEmptyUri(R.drawable.buddy)
.showImageOnFail(R.drawable.buddy)
.build();
public static final DisplayImageOptions DISPLAY_THUMBNAIL_OPTIONS =
new DisplayImageOptions.Builder()
.cloneFrom(BASE_DISPLAY_IMAGE_OPTIONS)
.showImageForEmptyUri(R.drawable.dummy_thumbnail)
.showImageOnFail(R.drawable.dummy_thumbnail)
.build();
public static final DisplayImageOptions DISPLAY_BANNER_OPTIONS =
new DisplayImageOptions.Builder()
.cloneFrom(BASE_DISPLAY_IMAGE_OPTIONS)
.showImageForEmptyUri(R.drawable.channel_banner)
.showImageOnFail(R.drawable.channel_banner)
.build();
public static final DisplayImageOptions DISPLAY_PLAYLIST_OPTIONS =
new DisplayImageOptions.Builder()
.cloneFrom(BASE_DISPLAY_IMAGE_OPTIONS)
.showImageForEmptyUri(R.drawable.dummy_thumbnail_playlist)
.showImageOnFail(R.drawable.dummy_thumbnail_playlist)
.build();
public static final DisplayImageOptions DISPLAY_SEEKBAR_PREVIEW_OPTIONS =
new DisplayImageOptions.Builder()
.cloneFrom(BASE_DISPLAY_IMAGE_OPTIONS)
.build();
private ImageDisplayConstants() { }
}

View file

@ -226,6 +226,16 @@ public final class Localization {
shortCount(context, subscriberCount)); shortCount(context, subscriberCount));
} }
public static String downloadCount(final Context context, final int downloadCount) {
return getQuantity(context, R.plurals.download_finished_notification, 0,
downloadCount, shortCount(context, downloadCount));
}
public static String deletedDownloadCount(final Context context, final int deletedCount) {
return getQuantity(context, R.plurals.deleted_downloads_toast, 0,
deletedCount, shortCount(context, deletedCount));
}
private static String getQuantity(final Context context, @PluralsRes final int pluralId, private static String getQuantity(final Context context, @PluralsRes final int pluralId,
@StringRes final int zeroCaseStringId, final long count, @StringRes final int zeroCaseStringId, final long count,
final String formattedCount) { final String formattedCount) {

View file

@ -18,8 +18,6 @@ import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentManager; import androidx.fragment.app.FragmentManager;
import androidx.fragment.app.FragmentTransaction; import androidx.fragment.app.FragmentTransaction;
import com.nostra13.universalimageloader.core.ImageLoader;
import org.schabi.newpipe.MainActivity; import org.schabi.newpipe.MainActivity;
import org.schabi.newpipe.NewPipeDatabase; import org.schabi.newpipe.NewPipeDatabase;
import org.schabi.newpipe.R; import org.schabi.newpipe.R;
@ -60,6 +58,8 @@ import java.util.ArrayList;
import static org.schabi.newpipe.util.external_communication.ShareUtils.installApp; import static org.schabi.newpipe.util.external_communication.ShareUtils.installApp;
import com.jakewharton.processphoenix.ProcessPhoenix;
public final class NavigationHelper { public final class NavigationHelper {
public static final String MAIN_FRAGMENT_TAG = "main_fragment_tag"; public static final String MAIN_FRAGMENT_TAG = "main_fragment_tag";
public static final String SEARCH_FRAGMENT_TAG = "search_fragment_tag"; public static final String SEARCH_FRAGMENT_TAG = "search_fragment_tag";
@ -259,10 +259,9 @@ public final class NavigationHelper {
if (context instanceof Activity) { if (context instanceof Activity) {
new AlertDialog.Builder(context) new AlertDialog.Builder(context)
.setMessage(R.string.no_player_found) .setMessage(R.string.no_player_found)
.setPositiveButton(R.string.install, (dialog, which) -> { .setPositiveButton(R.string.install,
ShareUtils.openUrlInBrowser(context, (dialog, which) -> ShareUtils.openUrlInBrowser(context,
context.getString(R.string.fdroid_vlc_url), false); context.getString(R.string.fdroid_vlc_url), false))
})
.setNegativeButton(R.string.cancel, (dialog, which) .setNegativeButton(R.string.cancel, (dialog, which)
-> Log.i("NavigationHelper", "You unlocked a secret unicorn.")) -> Log.i("NavigationHelper", "You unlocked a secret unicorn."))
.show(); .show();
@ -284,8 +283,6 @@ public final class NavigationHelper {
} }
public static void gotoMainFragment(final FragmentManager fragmentManager) { public static void gotoMainFragment(final FragmentManager fragmentManager) {
ImageLoader.getInstance().clearMemoryCache();
final boolean popped = fragmentManager.popBackStackImmediate(MAIN_FRAGMENT_TAG, 0); final boolean popped = fragmentManager.popBackStackImmediate(MAIN_FRAGMENT_TAG, 0);
if (!popped) { if (!popped) {
openMainFragment(fragmentManager); openMainFragment(fragmentManager);
@ -365,13 +362,15 @@ public final class NavigationHelper {
autoPlay = false; autoPlay = false;
} }
final RunnableWithVideoDetailFragment onVideoDetailFragmentReady = (detailFragment) -> { final RunnableWithVideoDetailFragment onVideoDetailFragmentReady = detailFragment -> {
expandMainPlayer(detailFragment.requireActivity()); expandMainPlayer(detailFragment.requireActivity());
detailFragment.setAutoPlay(autoPlay); detailFragment.setAutoPlay(autoPlay);
if (switchingPlayers) { if (switchingPlayers) {
// Situation when user switches from players to main player. All needed data is // Situation when user switches from players to main player. All needed data is
// here, we can start watching (assuming newQueue equals playQueue). // here, we can start watching (assuming newQueue equals playQueue).
detailFragment.openVideoPlayer(); // Starting directly in fullscreen if the previous player type was popup.
detailFragment.openVideoPlayer(playerType == MainPlayer.PlayerType.POPUP
|| PlayerHelper.isStartMainPlayerFullscreenEnabled(context));
} else { } else {
detailFragment.selectAndLoadVideo(serviceId, url, title, playQueue); detailFragment.selectAndLoadVideo(serviceId, url, title, playQueue);
} }
@ -610,8 +609,7 @@ public final class NavigationHelper {
*/ */
public static void restartApp(final Activity activity) { public static void restartApp(final Activity activity) {
NewPipeDatabase.close(); NewPipeDatabase.close();
activity.finishAffinity();
final Intent intent = new Intent(activity, MainActivity.class); ProcessPhoenix.triggerRebirth(activity.getApplicationContext());
activity.startActivity(intent);
} }
} }

View file

@ -119,7 +119,7 @@ public final class PermissionHelper {
public static boolean isPopupEnabled(final Context context) { public static boolean isPopupEnabled(final Context context) {
return Build.VERSION.SDK_INT < Build.VERSION_CODES.M return Build.VERSION.SDK_INT < Build.VERSION_CODES.M
|| PermissionHelper.checkSystemAlertWindowPermission(context); || checkSystemAlertWindowPermission(context);
} }
public static void showPopupEnablementToast(final Context context) { public static void showPopupEnablementToast(final Context context) {

View file

@ -0,0 +1,171 @@
package org.schabi.newpipe.util;
import android.annotation.SuppressLint;
import android.content.Context;
import android.graphics.Bitmap;
import com.squareup.picasso.Cache;
import com.squareup.picasso.LruCache;
import com.squareup.picasso.OkHttp3Downloader;
import com.squareup.picasso.Picasso;
import com.squareup.picasso.RequestCreator;
import com.squareup.picasso.Transformation;
import org.schabi.newpipe.R;
import java.io.File;
import java.io.IOException;
import java.util.concurrent.TimeUnit;
import okhttp3.OkHttpClient;
import static org.schabi.newpipe.extractor.utils.Utils.isBlank;
public final class PicassoHelper {
public static final String PLAYER_THUMBNAIL_TAG = "PICASSO_PLAYER_THUMBNAIL_TAG";
private static final String PLAYER_THUMBNAIL_TRANSFORMATION_KEY
= "PICASSO_PLAYER_THUMBNAIL_TRANSFORMATION_KEY";
private PicassoHelper() {
}
private static Cache picassoCache;
private static OkHttpClient picassoDownloaderClient;
// suppress because terminate() is called in App.onTerminate(), preventing leaks
@SuppressLint("StaticFieldLeak")
private static Picasso picassoInstance;
private static boolean shouldLoadImages;
public static void init(final Context context) {
picassoCache = new LruCache(10 * 1024 * 1024);
picassoDownloaderClient = new OkHttpClient.Builder()
.cache(new okhttp3.Cache(new File(context.getExternalCacheDir(), "picasso"),
50 * 1024 * 1024))
// this should already be the default timeout in OkHttp3, but just to be sure...
.callTimeout(15, TimeUnit.SECONDS)
.build();
picassoInstance = new Picasso.Builder(context)
.memoryCache(picassoCache) // memory cache
.downloader(new OkHttp3Downloader(picassoDownloaderClient)) // disk cache
.defaultBitmapConfig(Bitmap.Config.RGB_565)
.build();
}
public static void terminate() {
picassoCache = null;
picassoDownloaderClient = null;
if (picassoInstance != null) {
picassoInstance.shutdown();
picassoInstance = null;
}
}
public static void clearCache(final Context context) throws IOException {
picassoInstance.shutdown();
picassoCache.clear(); // clear memory cache
final okhttp3.Cache diskCache = picassoDownloaderClient.cache();
if (diskCache != null) {
diskCache.delete(); // clear disk cache
}
init(context);
}
public static void cancelTag(final Object tag) {
picassoInstance.cancelTag(tag);
}
public static void setIndicatorsEnabled(final boolean enabled) {
picassoInstance.setIndicatorsEnabled(enabled); // useful for debugging
}
public static void setShouldLoadImages(final boolean shouldLoadImages) {
PicassoHelper.shouldLoadImages = shouldLoadImages;
}
public static boolean getShouldLoadImages() {
return shouldLoadImages;
}
public static RequestCreator loadAvatar(final String url) {
return loadImageDefault(url, R.drawable.buddy);
}
public static RequestCreator loadThumbnail(final String url) {
return loadImageDefault(url, R.drawable.dummy_thumbnail);
}
public static RequestCreator loadBanner(final String url) {
return loadImageDefault(url, R.drawable.channel_banner);
}
public static RequestCreator loadPlaylistThumbnail(final String url) {
return loadImageDefault(url, R.drawable.dummy_thumbnail_playlist);
}
public static RequestCreator loadSeekbarThumbnailPreview(final String url) {
return picassoInstance.load(url);
}
public static RequestCreator loadScaledDownThumbnail(final Context context, final String url) {
// scale down the notification thumbnail for performance
return PicassoHelper.loadThumbnail(url)
.tag(PLAYER_THUMBNAIL_TAG)
.transform(new Transformation() {
@Override
public Bitmap transform(final Bitmap source) {
final float notificationThumbnailWidth = Math.min(
context.getResources()
.getDimension(R.dimen.player_notification_thumbnail_width),
source.getWidth());
final Bitmap result = Bitmap.createScaledBitmap(
source,
(int) notificationThumbnailWidth,
(int) (source.getHeight()
/ (source.getWidth() / notificationThumbnailWidth)),
true);
if (result == source) {
// create a new mutable bitmap to prevent strange crashes on some
// devices (see #4638)
final Bitmap copied = Bitmap.createScaledBitmap(
source,
(int) notificationThumbnailWidth - 1,
(int) (source.getHeight() / (source.getWidth()
/ (notificationThumbnailWidth - 1))),
true);
source.recycle();
return copied;
} else {
source.recycle();
return result;
}
}
@Override
public String key() {
return PLAYER_THUMBNAIL_TRANSFORMATION_KEY;
}
});
}
private static RequestCreator loadImageDefault(final String url, final int placeholderResId) {
if (!shouldLoadImages || isBlank(url)) {
return picassoInstance
.load((String) null)
.placeholder(placeholderResId) // show placeholder when no image should load
.error(placeholderResId);
} else {
return picassoInstance
.load(url)
.error(placeholderResId); // don't show placeholder while loading, only on error
}
}
}

View file

@ -1,7 +1,7 @@
package org.schabi.newpipe.util package org.schabi.newpipe.util
import android.os.Parcelable import android.os.Parcelable
import kotlinx.android.parcel.Parcelize import kotlinx.parcelize.Parcelize
/** /**
* Information about the saved state on the disk. * Information about the saved state on the disk.

View file

@ -2,9 +2,11 @@ package org.schabi.newpipe.util;
import android.content.Context; import android.content.Context;
import android.net.Uri; import android.net.Uri;
import android.widget.Toast;
import androidx.fragment.app.Fragment; import androidx.fragment.app.Fragment;
import org.schabi.newpipe.NewPipeDatabase;
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.dialog.PlaylistAppendDialog; import org.schabi.newpipe.local.dialog.PlaylistAppendDialog;
@ -20,7 +22,9 @@ import java.util.Collections;
import java.util.List; import java.util.List;
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers; import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
import io.reactivex.rxjava3.schedulers.Schedulers;
import static org.schabi.newpipe.extractor.utils.Utils.isNullOrEmpty;
import static org.schabi.newpipe.player.MainPlayer.PlayerType.AUDIO; import static org.schabi.newpipe.player.MainPlayer.PlayerType.AUDIO;
import static org.schabi.newpipe.player.MainPlayer.PlayerType.POPUP; import static org.schabi.newpipe.player.MainPlayer.PlayerType.POPUP;
@ -29,12 +33,30 @@ public enum StreamDialogEntry {
// enum values with DEFAULT actions // // enum values with DEFAULT actions //
////////////////////////////////////// //////////////////////////////////////
show_channel_details(R.string.show_channel_details, (fragment, item) -> show_channel_details(R.string.show_channel_details, (fragment, item) -> {
// For some reason `getParentFragmentManager()` doesn't work, but this does. if (isNullOrEmpty(item.getUploaderUrl())) {
NavigationHelper.openChannelFragment( final int serviceId = item.getServiceId();
fragment.requireActivity().getSupportFragmentManager(), final String url = item.getUrl();
item.getServiceId(), item.getUploaderUrl(), item.getUploaderName()) Toast.makeText(fragment.getContext(), R.string.loading_channel_details,
), Toast.LENGTH_SHORT).show();
ExtractorHelper.getStreamInfo(serviceId, url, false)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(result -> {
NewPipeDatabase.getInstance(fragment.getContext()).streamDAO()
.setUploaderUrl(serviceId, url, result.getUploaderUrl())
.subscribeOn(Schedulers.io()).subscribe();
openChannelFragment(fragment, item, result.getUploaderUrl());
}, throwable -> Toast.makeText(
// TODO: Open the Error Activity
fragment.getContext(),
R.string.error_show_channel_details,
Toast.LENGTH_SHORT
).show());
} else {
openChannelFragment(fragment, item, item.getUploaderUrl());
}
}),
/** /**
* Enqueues the stream automatically to the current PlayerType.<br> * Enqueues the stream automatically to the current PlayerType.<br>
@ -179,4 +201,17 @@ public enum StreamDialogEntry {
public interface StreamDialogEntryAction { public interface StreamDialogEntryAction {
void onClick(Fragment fragment, StreamInfoItem infoItem); void onClick(Fragment fragment, StreamInfoItem infoItem);
} }
/////////////////////////////////////////////
// private method to open channel fragment //
/////////////////////////////////////////////
private static void openChannelFragment(final Fragment fragment,
final StreamInfoItem item,
final String uploaderUrl) {
// For some reason `getParentFragmentManager()` doesn't work, but this does.
NavigationHelper.openChannelFragment(
fragment.requireActivity().getSupportFragmentManager(),
item.getServiceId(), uploaderUrl, item.getUploaderName());
}
} }

View file

@ -1,9 +1,14 @@
package org.schabi.newpipe.util.external_communication; package org.schabi.newpipe.util.external_communication;
import android.content.Context; import android.content.Context;
import android.util.Log;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.appcompat.app.AlertDialog;
import org.schabi.newpipe.MainActivity;
import org.schabi.newpipe.R;
import org.schabi.newpipe.error.ErrorPanelHelper;
import org.schabi.newpipe.extractor.NewPipe; import org.schabi.newpipe.extractor.NewPipe;
import org.schabi.newpipe.extractor.StreamingService; import org.schabi.newpipe.extractor.StreamingService;
import org.schabi.newpipe.extractor.exceptions.ExtractionException; import org.schabi.newpipe.extractor.exceptions.ExtractionException;
@ -24,6 +29,9 @@ import io.reactivex.rxjava3.disposables.CompositeDisposable;
import io.reactivex.rxjava3.schedulers.Schedulers; import io.reactivex.rxjava3.schedulers.Schedulers;
public final class InternalUrlsHandler { public final class InternalUrlsHandler {
private static final String TAG = InternalUrlsHandler.class.getSimpleName();
private static final boolean DEBUG = MainActivity.DEBUG;
private static final Pattern AMPERSAND_TIMESTAMP_PATTERN = Pattern.compile("(.*)&t=(\\d+)"); private static final Pattern AMPERSAND_TIMESTAMP_PATTERN = Pattern.compile("(.*)&t=(\\d+)");
private static final Pattern HASHTAG_TIMESTAMP_PATTERN = private static final Pattern HASHTAG_TIMESTAMP_PATTERN =
Pattern.compile("(.*)#timestamp=(\\d+)"); Pattern.compile("(.*)#timestamp=(\\d+)");
@ -93,7 +101,12 @@ public final class InternalUrlsHandler {
return false; return false;
} }
final String matchedUrl = matcher.group(1); final String matchedUrl = matcher.group(1);
final int seconds = Integer.parseInt(matcher.group(2)); final int seconds;
if (matcher.group(2) == null) {
seconds = -1;
} else {
seconds = Integer.parseInt(matcher.group(2));
}
final StreamingService service; final StreamingService service;
final StreamingService.LinkType linkType; final StreamingService.LinkType linkType;
@ -146,8 +159,18 @@ public final class InternalUrlsHandler {
.observeOn(AndroidSchedulers.mainThread()) .observeOn(AndroidSchedulers.mainThread())
.subscribe(info -> { .subscribe(info -> {
final PlayQueue playQueue final PlayQueue playQueue
= new SinglePlayQueue(info, seconds * 1000); = new SinglePlayQueue(info, seconds * 1000L);
NavigationHelper.playOnPopupPlayer(context, playQueue, false); NavigationHelper.playOnPopupPlayer(context, playQueue, false);
}, throwable -> {
if (DEBUG) {
Log.e(TAG, "Could not play on popup: " + url, throwable);
}
new AlertDialog.Builder(context)
.setTitle(R.string.player_stream_failure)
.setMessage(
ErrorPanelHelper.Companion.getExceptionDescription(throwable))
.setPositiveButton(R.string.ok, (v, b) -> { })
.show();
})); }));
return true; return true;
} }

View file

@ -248,6 +248,7 @@ public final class ShareUtils {
shareIntent.putExtra(Intent.EXTRA_TEXT, content); shareIntent.putExtra(Intent.EXTRA_TEXT, content);
if (!title.isEmpty()) { if (!title.isEmpty()) {
shareIntent.putExtra(Intent.EXTRA_TITLE, title); shareIntent.putExtra(Intent.EXTRA_TITLE, title);
shareIntent.putExtra(Intent.EXTRA_SUBJECT, title);
} }
/* TODO: add the image of the content to Android share sheet with setClipData after /* TODO: add the image of the content to Android share sheet with setClipData after

View file

@ -32,9 +32,8 @@ import static org.schabi.newpipe.util.external_communication.InternalUrlsHandler
public final class TextLinkifier { public final class TextLinkifier {
public static final String TAG = TextLinkifier.class.getSimpleName(); public static final String TAG = TextLinkifier.class.getSimpleName();
private static final Pattern HASHTAGS_PATTERN = Pattern.compile("(#[A-Za-z0-9_]+)"); private static final Pattern HASHTAGS_PATTERN = Pattern.compile("(#[A-Za-z0-9_]+)");
private static final Pattern TIMESTAMPS_PATTERN = Pattern.compile(
"(?:^|(?!:)\\W)(?:([0-5]?[0-9]):)?([0-5]?[0-9]):([0-5][0-9])(?=$|(?!:)\\W)");
private TextLinkifier() { private TextLinkifier() {
} }
@ -174,33 +173,34 @@ public final class TextLinkifier {
final Info relatedInfo, final Info relatedInfo,
final CompositeDisposable disposables) { final CompositeDisposable disposables) {
final String descriptionText = spannableDescription.toString(); final String descriptionText = spannableDescription.toString();
final Matcher timestampsMatches = TIMESTAMPS_PATTERN.matcher(descriptionText); final Matcher timestampsMatches =
TimestampExtractor.TIMESTAMPS_PATTERN.matcher(descriptionText);
while (timestampsMatches.find()) { while (timestampsMatches.find()) {
final int timestampStart = timestampsMatches.start(2); final TimestampExtractor.TimestampMatchDTO timestampMatchDTO =
final int timestampEnd = timestampsMatches.end(3); TimestampExtractor.getTimestampFromMatcher(
final String parsedTimestamp = descriptionText.substring(timestampStart, timestampEnd); timestampsMatches,
final String[] timestampParts = parsedTimestamp.split(":"); descriptionText);
final int seconds; if (timestampMatchDTO == null) {
if (timestampParts.length == 3) { // timestamp format: XX:XX:XX
seconds = Integer.parseInt(timestampParts[0]) * 3600 // hours
+ Integer.parseInt(timestampParts[1]) * 60 // minutes
+ Integer.parseInt(timestampParts[2]); // seconds
} else if (timestampParts.length == 2) { // timestamp format: XX:XX
seconds = Integer.parseInt(timestampParts[0]) * 60 // minutes
+ Integer.parseInt(timestampParts[1]); // seconds
} else {
continue; continue;
} }
spannableDescription.setSpan(new ClickableSpan() { spannableDescription.setSpan(
@Override new ClickableSpan() {
public void onClick(@NonNull final View view) { @Override
playOnPopup(context, relatedInfo.getUrl(), relatedInfo.getService(), seconds, public void onClick(@NonNull final View view) {
disposables); playOnPopup(
} context,
}, timestampStart, timestampEnd, 0); relatedInfo.getUrl(),
relatedInfo.getService(),
timestampMatchDTO.seconds(),
disposables);
}
},
timestampMatchDTO.timestampStart(),
timestampMatchDTO.timestampEnd(),
0);
} }
} }

View file

@ -0,0 +1,79 @@
package org.schabi.newpipe.util.external_communication;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* Extracts timestamps.
*/
public final class TimestampExtractor {
public static final Pattern TIMESTAMPS_PATTERN = Pattern.compile(
"(?:^|(?!:)\\W)(?:([0-5]?[0-9]):)?([0-5]?[0-9]):([0-5][0-9])(?=$|(?!:)\\W)");
private TimestampExtractor() {
// No impl pls
}
/**
* Get's a single timestamp from a matcher.
*
* @param timestampMatches The matcher which was created using {@link #TIMESTAMPS_PATTERN}
* @param baseText The text where the pattern was applied to /
* where the matcher is based upon
* @return If a match occurred: a {@link TimestampMatchDTO} filled with information.<br/>
* If not <code>null</code>.
*/
public static TimestampMatchDTO getTimestampFromMatcher(
final Matcher timestampMatches,
final String baseText) {
int timestampStart = timestampMatches.start(1);
if (timestampStart == -1) {
timestampStart = timestampMatches.start(2);
}
final int timestampEnd = timestampMatches.end(3);
final String parsedTimestamp = baseText.substring(timestampStart, timestampEnd);
final String[] timestampParts = parsedTimestamp.split(":");
final int seconds;
if (timestampParts.length == 3) { // timestamp format: XX:XX:XX
seconds = Integer.parseInt(timestampParts[0]) * 3600 // hours
+ Integer.parseInt(timestampParts[1]) * 60 // minutes
+ Integer.parseInt(timestampParts[2]); // seconds
} else if (timestampParts.length == 2) { // timestamp format: XX:XX
seconds = Integer.parseInt(timestampParts[0]) * 60 // minutes
+ Integer.parseInt(timestampParts[1]); // seconds
} else {
return null;
}
return new TimestampMatchDTO(timestampStart, timestampEnd, seconds);
}
public static class TimestampMatchDTO {
private final int timestampStart;
private final int timestampEnd;
private final int seconds;
public TimestampMatchDTO(
final int timestampStart,
final int timestampEnd,
final int seconds) {
this.timestampStart = timestampStart;
this.timestampEnd = timestampEnd;
this.seconds = seconds;
}
public int timestampStart() {
return timestampStart;
}
public int timestampEnd() {
return timestampEnd;
}
public int seconds() {
return seconds;
}
}
}

View file

@ -1,7 +1,7 @@
package us.shandian.giga.get package us.shandian.giga.get
import android.os.Parcelable import android.os.Parcelable
import kotlinx.android.parcel.Parcelize import kotlinx.parcelize.Parcelize
import org.schabi.newpipe.extractor.MediaFormat import org.schabi.newpipe.extractor.MediaFormat
import org.schabi.newpipe.extractor.stream.AudioStream import org.schabi.newpipe.extractor.stream.AudioStream
import org.schabi.newpipe.extractor.stream.Stream import org.schabi.newpipe.extractor.stream.Stream

View file

@ -53,6 +53,8 @@ import us.shandian.giga.get.DownloadMission;
import us.shandian.giga.get.MissionRecoveryInfo; import us.shandian.giga.get.MissionRecoveryInfo;
import org.schabi.newpipe.streams.io.StoredDirectoryHelper; import org.schabi.newpipe.streams.io.StoredDirectoryHelper;
import org.schabi.newpipe.streams.io.StoredFileHelper; import org.schabi.newpipe.streams.io.StoredFileHelper;
import org.schabi.newpipe.util.Localization;
import us.shandian.giga.postprocessing.Postprocessing; import us.shandian.giga.postprocessing.Postprocessing;
import us.shandian.giga.service.DownloadManager.NetworkState; import us.shandian.giga.service.DownloadManager.NetworkState;
@ -494,7 +496,8 @@ public class DownloadManagerService extends Service {
.setContentIntent(makePendingIntent(ACTION_OPEN_DOWNLOADS_FINISHED)); .setContentIntent(makePendingIntent(ACTION_OPEN_DOWNLOADS_FINISHED));
} }
if (downloadDoneCount < 1) { downloadDoneCount++;
if (downloadDoneCount == 1) {
downloadDoneList.append(name); downloadDoneList.append(name);
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) { if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
@ -503,9 +506,9 @@ public class DownloadManagerService extends Service {
downloadDoneNotification.setContentTitle(null); downloadDoneNotification.setContentTitle(null);
} }
downloadDoneNotification.setContentText(getString(R.string.download_finished)); downloadDoneNotification.setContentText(Localization.downloadCount(this, downloadDoneCount));
downloadDoneNotification.setStyle(new NotificationCompat.BigTextStyle() downloadDoneNotification.setStyle(new NotificationCompat.BigTextStyle()
.setBigContentTitle(getString(R.string.download_finished)) .setBigContentTitle(Localization.downloadCount(this, downloadDoneCount))
.bigText(name) .bigText(name)
); );
} else { } else {
@ -513,12 +516,11 @@ public class DownloadManagerService extends Service {
downloadDoneList.append(name); downloadDoneList.append(name);
downloadDoneNotification.setStyle(new NotificationCompat.BigTextStyle().bigText(downloadDoneList)); downloadDoneNotification.setStyle(new NotificationCompat.BigTextStyle().bigText(downloadDoneList));
downloadDoneNotification.setContentTitle(getString(R.string.download_finished_more, String.valueOf(downloadDoneCount + 1))); downloadDoneNotification.setContentTitle(Localization.downloadCount(this, downloadDoneCount));
downloadDoneNotification.setContentText(downloadDoneList); downloadDoneNotification.setContentText(downloadDoneList);
} }
mNotificationManager.notify(DOWNLOADS_NOTIFICATION_ID, downloadDoneNotification.build()); mNotificationManager.notify(DOWNLOADS_NOTIFICATION_ID, downloadDoneNotification.build());
downloadDoneCount++;
} }
public void notifyFailedDownload(DownloadMission mission) { public void notifyFailedDownload(DownloadMission mission) {

View file

@ -47,6 +47,7 @@ import org.schabi.newpipe.extractor.NewPipe;
import org.schabi.newpipe.error.ErrorActivity; import org.schabi.newpipe.error.ErrorActivity;
import org.schabi.newpipe.error.ErrorInfo; import org.schabi.newpipe.error.ErrorInfo;
import org.schabi.newpipe.error.UserAction; import org.schabi.newpipe.error.UserAction;
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;
@ -580,7 +581,7 @@ public class MissionAdapter extends Adapter<ViewHolder> implements Handler.Callb
); );
} }
builder.setNegativeButton(R.string.finish, (dialog, which) -> dialog.cancel()) builder.setNegativeButton(R.string.ok, (dialog, which) -> dialog.cancel())
.setTitle(mission.storage.getName()) .setTitle(mission.storage.getName())
.create() .create()
.show(); .show();
@ -622,7 +623,7 @@ public class MissionAdapter extends Adapter<ViewHolder> implements Handler.Callb
} }
applyChanges(); applyChanges();
String msg = String.format(mContext.getString(R.string.deleted_downloads), mHidden.size()); String msg = Localization.deletedDownloadCount(mContext, mHidden.size());
mSnackbar = Snackbar.make(mView, msg, Snackbar.LENGTH_INDEFINITE); mSnackbar = Snackbar.make(mView, msg, Snackbar.LENGTH_INDEFINITE);
mSnackbar.setAction(R.string.undo, s -> { mSnackbar.setAction(R.string.undo, s -> {
Iterator<Mission> i = mHidden.iterator(); Iterator<Mission> i = mHidden.iterator();

View file

@ -1,9 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android"
android:interpolator="@android:interpolator/decelerate_quint">
<alpha
android:duration="150"
android:fromAlpha="0.00"
android:toAlpha="1.0" />
</set>

View file

@ -1,9 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android"
android:interpolator="@android:interpolator/accelerate_quint">
<alpha
android:duration="350"
android:fromAlpha="1.0"
android:toAlpha="0.00" />
</set>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 358 B

After

Width:  |  Height:  |  Size: 342 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 262 B

After

Width:  |  Height:  |  Size: 252 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 480 B

After

Width:  |  Height:  |  Size: 450 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 581 B

After

Width:  |  Height:  |  Size: 525 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 511 B

After

Width:  |  Height:  |  Size: 486 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 244 B

After

Width:  |  Height:  |  Size: 230 B

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