Merge pull request #7063 from TeamNewPipe/release/0.21.10

Release 0.21.10
This commit is contained in:
Tobi 2021-09-19 20:59:54 +02:00 committed by GitHub
commit ba6fdecbae
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
358 changed files with 4029 additions and 6851 deletions

View File

@ -57,7 +57,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

View File

@ -1,6 +1,7 @@
name: CI
on:
workflow_dispatch:
pull_request:
branches:
- dev
@ -36,20 +37,11 @@ jobs:
uses: actions/setup-java@v2
with:
java-version: 8
distribution: "adopt"
- name: Cache Gradle dependencies
uses: actions/cache@v2
with:
path: ~/.gradle/caches
key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle') }}
restore-keys: ${{ runner.os }}-gradle
distribution: "temurin"
cache: 'gradle'
- name: Check if kotlin files are formatted correctly
run: ./gradlew runKtlint
- name: Build debug APK and run jvm tests
run: ./gradlew assembleDebug lintDebug testDebugUnitTest --stacktrace
run: ./gradlew assembleDebug lintDebug testDebugUnitTest --stacktrace -DskipFormatKtlint
- name: Upload APK
uses: actions/upload-artifact@v2
@ -71,14 +63,8 @@ jobs:
uses: actions/setup-java@v2
with:
java-version: 8
distribution: "adopt"
- name: Cache Gradle dependencies
uses: actions/cache@v2
with:
path: ~/.gradle/caches
key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle') }}
restore-keys: ${{ runner.os }}-gradle
distribution: "temurin"
cache: 'gradle'
- name: Run android tests
uses: reactivecircus/android-emulator-runner@v2
@ -99,7 +85,8 @@ jobs:
# uses: actions/setup-java@v2
# with:
# java-version: 11 # Sonar requires JDK 11
# distribution: "adopt"
# distribution: "temurin"
# cache: 'gradle'
# - name: Cache SonarCloud packages
# uses: actions/cache@v2
@ -108,13 +95,6 @@ jobs:
# key: ${{ 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
# env:
# 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: 'kotlin-android'
apply plugin: 'kotlin-android-extensions'
apply plugin: 'kotlin-parcelize'
apply plugin: 'kotlin-kapt'
apply plugin: 'checkstyle'
@ -17,8 +17,8 @@ android {
resValue "string", "app_name", "NewPipe"
minSdkVersion 19
targetSdkVersion 29
versionCode 975
versionName "0.21.9"
versionCode 976
versionName "0.21.10"
multiDexEnabled true
@ -84,11 +84,6 @@ android {
jvmTarget = JavaVersion.VERSION_1_8
}
// Required and used only by groupie
androidExtensions {
experimental = true
}
sourceSets {
androidTest.assets.srcDirs += files("$projectDir/schemas".toString())
}
@ -165,7 +160,10 @@ task formatKtlint(type: JavaExec) {
}
afterEvaluate {
preDebugBuild.dependsOn formatKtlint, runCheckstyle, runKtlint
if (!System.properties.containsKey('skipFormatKtlint')) {
preDebugBuild.dependsOn formatKtlint
}
preDebugBuild.dependsOn runCheckstyle, runKtlint
}
sonarqube {
@ -186,7 +184,7 @@ dependencies {
// name and the commit hash with the commit hash of the (pushed) commit you want to test
// This works thanks to JitPack: https://jitpack.io/
implementation 'com.github.TeamNewPipe:nanojson:1d9e1aea9049fc9f85e68b43ba39fe7be1c1f751'
implementation 'com.github.TeamNewPipe:NewPipeExtractor:v0.21.9'
implementation 'com.github.TeamNewPipe:NewPipeExtractor:v0.21.10'
/** Checkstyle **/
checkstyle "com.puppycrawl.tools:checkstyle:${checkstyleVersion}"
@ -243,7 +241,8 @@ dependencies {
// Circular ImageView
implementation "de.hdodenhof:circleimageview:3.1.0"
// 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
implementation "io.noties.markwon:core:${markwonVersion}"
@ -255,6 +254,9 @@ dependencies {
// Crash reporting
implementation "ch.acra:acra-core:5.7.0"
// Properly restarting
implementation 'com.jakewharton:process-phoenix:2.1.2'
// Reactive extensions for Java VM
implementation "io.reactivex.rxjava3:rxjava:3.0.7"
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() {
val stream = StreamEntity(
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))
@ -69,12 +70,14 @@ class LocalPlaylistManagerTest {
fun createPlaylist_nonExistentStreamsAreUpserted() {
val stream = StreamEntity(
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)
val upserted = StreamEntity(
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))

View File

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

View File

@ -11,9 +11,7 @@ import androidx.core.app.NotificationManagerCompat;
import androidx.multidex.MultiDexApplication;
import androidx.preference.PreferenceManager;
import com.nostra13.universalimageloader.cache.memory.impl.LRULimitedMemoryCache;
import com.nostra13.universalimageloader.core.ImageLoader;
import com.nostra13.universalimageloader.core.ImageLoaderConfiguration;
import com.jakewharton.processphoenix.ProcessPhoenix;
import org.acra.ACRA;
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.settings.NewPipeSettings;
import org.schabi.newpipe.util.Localization;
import org.schabi.newpipe.util.PicassoHelper;
import org.schabi.newpipe.util.ServiceHelper;
import org.schabi.newpipe.util.StateSaver;
@ -65,9 +64,9 @@ import io.reactivex.rxjava3.plugins.RxJavaPlugins;
*/
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;
private static final String TAG = App.class.toString();
private static App app;
@Nullable
private Disposable disposable = null;
@ -89,6 +88,12 @@ public class App extends MultiDexApplication {
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
NewPipeSettings.initSettings(this);
@ -103,7 +108,12 @@ public class App extends MultiDexApplication {
ServiceHelper.initServices(this);
// 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();
@ -117,6 +127,7 @@ public class App extends MultiDexApplication {
disposable.dispose();
}
super.onTerminate();
PicassoHelper.terminate();
}
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.
* 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.FragmentManager;
import com.nostra13.universalimageloader.core.ImageLoader;
import icepick.Icepick;
import icepick.State;
import leakcanary.AppWatcher;
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 boolean DEBUG = MainActivity.DEBUG;
protected static final boolean DEBUG = MainActivity.DEBUG;
protected AppCompatActivity activity;
//These values are used for controlling fragments when they are part of the frontpage
@State

View File

@ -4,7 +4,6 @@ import android.app.Application;
import android.app.PendingIntent;
import android.content.Intent;
import android.content.SharedPreferences;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.content.pm.Signature;
import android.net.ConnectivityManager;
@ -16,6 +15,7 @@ import androidx.annotation.Nullable;
import androidx.core.app.NotificationCompat;
import androidx.core.app.NotificationManagerCompat;
import androidx.core.content.ContextCompat;
import androidx.core.content.pm.PackageInfoCompat;
import androidx.preference.PreferenceManager;
import com.grack.nanojson.JsonObject;
@ -34,6 +34,7 @@ import java.security.cert.CertificateEncodingException;
import java.security.cert.CertificateException;
import java.security.cert.CertificateFactory;
import java.security.cert.X509Certificate;
import java.util.List;
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
import io.reactivex.rxjava3.core.Maybe;
@ -58,20 +59,22 @@ public final class CheckForNewAppVersion {
*/
@NonNull
private static String getCertificateSHA1Fingerprint(@NonNull final Application application) {
final PackageInfo packageInfo;
final List<Signature> signatures;
try {
packageInfo = application.getPackageManager().getPackageInfo(
application.getPackageName(), PackageManager.GET_SIGNATURES);
signatures = PackageInfoCompat.getSignatures(application.getPackageManager(),
application.getPackageName());
} catch (final PackageManager.NameNotFoundException e) {
ErrorActivity.reportError(application, new ErrorInfo(e,
UserAction.CHECK_FOR_NEW_APP_VERSION, "Could not find package info"));
return "";
}
if (signatures.isEmpty()) {
return "";
}
final X509Certificate c;
try {
final Signature[] signatures = packageInfo.signatures;
final byte[] cert = signatures[0].toByteArray();
final byte[] cert = signatures.get(0).toByteArray();
final InputStream input = new ByteArrayInputStream(cert);
final CertificateFactory cf = CertificateFactory.getInstance("X509");
c = (X509Certificate) cf.generateCertificate(input);

View File

@ -17,7 +17,6 @@ import org.schabi.newpipe.util.InfoCache;
import org.schabi.newpipe.util.TLSSocketFactoryCompat;
import java.io.IOException;
import java.io.InputStream;
import java.security.KeyManagementException;
import java.security.KeyStore;
import java.security.KeyStoreException;
@ -194,36 +193,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();
final okhttp3.Response response = client.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
public Response execute(@NonNull final Request request)
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.Migrations.MIGRATION_1_2;
import static org.schabi.newpipe.database.Migrations.MIGRATION_2_3;
import static org.schabi.newpipe.database.Migrations.MIGRATION_3_4;
public final class NewPipeDatabase {
private static volatile AppDatabase databaseInstance;
@ -22,7 +23,7 @@ public final class NewPipeDatabase {
private static AppDatabase getDatabase(final Context context) {
return Room
.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();
}

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.",
"https://square.github.io/okhttp/", StandardLicenses.APACHE2
),
SoftwareComponent(
"Picasso", "2013", "Square, Inc.",
"https://square.github.io/picasso/", StandardLicenses.APACHE2
),
SoftwareComponent(
"PrettyTime", "2012 - 2020", "Lincoln Baxter, III",
"https://github.com/ocpsoft/prettytime", StandardLicenses.APACHE2
),
SoftwareComponent(
"ProcessPhoenix", "2015", "Jake Wharton",
"https://github.com/JakeWharton/ProcessPhoenix", StandardLicenses.APACHE2
),
SoftwareComponent(
"RxAndroid", "2015", "The RxAndroid authors",
"https://github.com/ReactiveX/RxAndroid", StandardLicenses.APACHE2
@ -177,11 +185,6 @@ class AboutActivity : AppCompatActivity() {
SoftwareComponent(
"RxJava", "2016 - 2020", "RxJava Contributors",
"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

View File

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

View File

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

View File

@ -1,7 +1,7 @@
package org.schabi.newpipe.about
import android.os.Parcelable
import kotlinx.android.parcel.Parcelize
import kotlinx.parcelize.Parcelize
@Parcelize
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.SubscriptionEntity;
import static org.schabi.newpipe.database.Migrations.DB_VER_3;
import static org.schabi.newpipe.database.Migrations.DB_VER_4;
@TypeConverters({Converters.class})
@Database(
@ -38,7 +38,7 @@ import static org.schabi.newpipe.database.Migrations.DB_VER_3;
FeedEntity.class, FeedGroupEntity.class, FeedGroupSubscriptionEntity.class,
FeedLastUpdatedEntity.class
},
version = DB_VER_3
version = DB_VER_4
)
public abstract class AppDatabase extends RoomDatabase {
public static final String DATABASE_NAME = "newpipe.db";

View File

@ -9,9 +9,19 @@ import androidx.sqlite.db.SupportSQLiteDatabase;
import org.schabi.newpipe.MainActivity;
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_2 = 2;
public static final int DB_VER_3 = 3;
public static final int DB_VER_4 = 4;
private static final String TAG = Migrations.class.getName();
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() { }
}

View File

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

View File

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

View File

@ -6,6 +6,7 @@ import androidx.room.Insert
import androidx.room.OnConflictStrategy
import androidx.room.Query
import androidx.room.Transaction
import io.reactivex.rxjava3.core.Completable
import io.reactivex.rxjava3.core.Flowable
import org.schabi.newpipe.database.BasicDAO
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")
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)
internal abstract fun silentInsertInternal(stream: StreamEntity): Long

View File

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

View File

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

View File

@ -2,7 +2,7 @@ package org.schabi.newpipe.error
import android.os.Parcelable
import androidx.annotation.StringRes
import kotlinx.android.parcel.Parcelize
import kotlinx.parcelize.Parcelize
import org.schabi.newpipe.R
import org.schabi.newpipe.extractor.Info
import org.schabi.newpipe.extractor.NewPipe

View File

@ -6,6 +6,8 @@ import android.util.Log
import android.view.View
import android.widget.Button
import android.widget.TextView
import androidx.annotation.Nullable
import androidx.annotation.StringRes
import androidx.core.view.isVisible
import androidx.fragment.app.Fragment
import com.jakewharton.rxbinding4.view.clicks
@ -37,22 +39,39 @@ class ErrorPanelHelper(
onRetry: Runnable
) {
private val context: Context = rootView.context!!
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)
private val errorServiceExplenationTextView: TextView = errorPanelRoot.findViewById(R.id.error_message_service_explenation_view)
private val errorButtonAction: Button = errorPanelRoot.findViewById(R.id.error_button_action)
private val errorButtonRetry: Button = errorPanelRoot.findViewById(R.id.error_button_retry)
// the only element that is visible by default
private val errorTextView: TextView =
errorPanelRoot.findViewById(R.id.error_message_view)
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
init {
errorDisposable = errorButtonRetry.clicks()
errorDisposable = errorRetryButton.clicks()
.debounce(300, TimeUnit.MILLISECONDS)
.observeOn(AndroidSchedulers.mainThread())
.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) {
if (errorInfo.throwable != null && errorInfo.throwable!!.isInterruptedCaused) {
@ -62,10 +81,14 @@ class ErrorPanelHelper(
return
}
errorButtonAction.isVisible = true
ensureDefaultVisibility()
if (errorInfo.throwable is ReCaptchaException) {
errorButtonAction.setText(R.string.recaptcha_solve)
errorButtonAction.setOnClickListener {
errorTextView.setText(R.string.recaptcha_request_toast)
showAndSetErrorButtonAction(
R.string.recaptcha_solve
) {
// Starting ReCaptcha Challenge Activity
val intent = Intent(context, ReCaptchaActivity::class.java)
intent.putExtra(
@ -73,78 +96,70 @@ class ErrorPanelHelper(
(errorInfo.throwable as ReCaptchaException).url
)
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
errorServiceInfoTextView.isVisible = false
errorServiceExplenationTextView.isVisible = false
errorButtonRetry.isVisible = true
errorRetryButton.isVisible = true
} else if (errorInfo.throwable is AccountTerminatedException) {
errorButtonRetry.isVisible = false
errorButtonAction.isVisible = false
errorTextView.setText(R.string.account_terminated)
if (!isNullOrEmpty((errorInfo.throwable as AccountTerminatedException).message)) {
errorServiceInfoTextView.setText(
context.resources.getString(
R.string.service_provides_reason,
NewPipe.getNameOfService(ServiceHelper.getSelectedServiceId(context))
)
)
errorServiceExplenationTextView.setText(
(errorInfo.throwable as AccountTerminatedException).message
errorServiceInfoTextView.text = context.resources.getString(
R.string.service_provides_reason,
NewPipe.getNameOfService(ServiceHelper.getSelectedServiceId(context))
)
errorServiceInfoTextView.isVisible = true
errorServiceExplenationTextView.isVisible = true
} else {
errorServiceInfoTextView.isVisible = false
errorServiceExplenationTextView.isVisible = false
errorServiceExplanationTextView.text =
(errorInfo.throwable as AccountTerminatedException).message
errorServiceExplanationTextView.isVisible = true
}
} else {
errorButtonAction.setText(R.string.error_snackbar_action)
errorButtonAction.setOnClickListener {
showAndSetErrorButtonAction(
R.string.error_snackbar_action
) {
ErrorActivity.reportError(context, errorInfo)
}
// additional info is only provided by AccountTerminatedException
errorServiceInfoTextView.isVisible = false
errorServiceExplenationTextView.isVisible = false
errorTextView.setText(getExceptionDescription(errorInfo.throwable))
// hide retry button by default, then show only if not unavailable/unsupported content
errorButtonRetry.isVisible = false
errorTextView.setText(
when (errorInfo.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
errorButtonRetry.isVisible = true
if (errorInfo.throwable != null && errorInfo.throwable!!.isNetworkRelated) {
R.string.network_error
} else {
R.string.error_snackbar_message
}
}
}
)
if (errorInfo.throwable !is ContentNotAvailableException &&
errorInfo.throwable !is ContentNotSupportedException
) {
// show retry button only for content which is not unavailable or unsupported
errorRetryButton.isVisible = true
}
}
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) {
errorButtonAction.isVisible = false
errorButtonRetry.isVisible = false
ensureDefaultVisibility()
errorTextView.text = errorString
setRootVisible()
}
private fun setRootVisible() {
errorPanelRoot.animate(true, 300)
}
fun hide() {
errorButtonAction.setOnClickListener(null)
errorActionButton.setOnClickListener(null)
errorPanelRoot.animate(false, 150)
}
@ -153,13 +168,35 @@ class ErrorPanelHelper(
}
fun dispose() {
errorButtonAction.setOnClickListener(null)
errorButtonRetry.setOnClickListener(null)
errorActionButton.setOnClickListener(null)
errorRetryButton.setOnClickListener(null)
errorDisposable?.dispose()
}
companion object {
val TAG: String = ErrorPanelHelper::class.simpleName!!
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;
import android.annotation.SuppressLint;
import android.content.Intent;
import android.content.SharedPreferences;
import android.os.Build;
@ -66,6 +67,7 @@ public class ReCaptchaActivity extends AppCompatActivity {
private ActivityRecaptchaBinding recaptchaBinding;
private String foundCookies = "";
@SuppressLint("SetJavaScriptEnabled")
@Override
protected void onCreate(final Bundle savedInstanceState) {
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.bottomsheet.BottomSheetBehavior;
import com.google.android.material.tabs.TabLayout;
import com.nostra13.universalimageloader.core.assist.FailReason;
import com.nostra13.universalimageloader.core.listener.ImageLoadingListener;
import com.nostra13.universalimageloader.core.listener.SimpleImageLoadingListener;
import com.squareup.picasso.Callback;
import org.schabi.newpipe.App;
import org.schabi.newpipe.R;
@ -90,14 +88,14 @@ import org.schabi.newpipe.player.playqueue.SinglePlayQueue;
import org.schabi.newpipe.util.Constants;
import org.schabi.newpipe.util.DeviceUtils;
import org.schabi.newpipe.util.ExtractorHelper;
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.Localization;
import org.schabi.newpipe.util.NavigationHelper;
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.ThemeHelper;
import org.schabi.newpipe.util.external_communication.KoreUtils;
import org.schabi.newpipe.util.external_communication.ShareUtils;
import java.util.ArrayList;
import java.util.Iterator;
@ -151,6 +149,8 @@ public final class VideoDetailFragment
private static final String DESCRIPTION_TAB_TAG = "DESCRIPTION TAB";
private static final String EMPTY_TAB_TAG = "EMPTY TAB";
private static final String PICASSO_VIDEO_DETAILS_TAG = "PICASSO_VIDEO_DETAILS_TAG";
// tabs
private boolean showComments;
private boolean showRelatedItems;
@ -201,7 +201,7 @@ public final class VideoDetailFragment
@Nullable
private MainPlayer playerService;
private Player player;
private PlayerHolder playerHolder = PlayerHolder.getInstance();
private final PlayerHolder playerHolder = PlayerHolder.getInstance();
/*//////////////////////////////////////////////////////////////////////////
// Service management
@ -220,7 +220,7 @@ public final class VideoDetailFragment
return;
}
if (isLandscape()) {
if (DeviceUtils.isLandscape(requireContext())) {
// If the video is playing but orientation changed
// let's make the video in fullscreen again
checkLandscape();
@ -241,7 +241,7 @@ public final class VideoDetailFragment
&& isAutoplayEnabled()
&& player.getParentActivity() == null)) {
autoPlayEnabled = true; // forcefully start playing
openVideoPlayer();
openVideoPlayerAutoFullscreen();
}
}
@ -423,7 +423,7 @@ public final class VideoDetailFragment
showRelatedItems = sharedPreferences.getBoolean(key, true);
tabSettingsChanged = true;
} else if (key.equals(getString(R.string.show_description_key))) {
showComments = sharedPreferences.getBoolean(key, true);
showDescription = sharedPreferences.getBoolean(key, true);
tabSettingsChanged = true;
}
}
@ -499,7 +499,7 @@ public final class VideoDetailFragment
break;
case R.id.detail_thumbnail_root_layout:
autoPlayEnabled = true; // forcefully start playing
openVideoPlayer();
openVideoPlayerAutoFullscreen();
break;
case R.id.detail_title_root_layout:
toggleTitleAndSecondaryControls();
@ -516,7 +516,7 @@ public final class VideoDetailFragment
showSystemUi();
} else {
autoPlayEnabled = true; // forcefully start playing
openVideoPlayer();
openVideoPlayer(false);
}
setOverlayPlayPauseImage(isPlayerAvailable() && player.isPlaying());
@ -686,33 +686,24 @@ public final class VideoDetailFragment
}
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())) {
final ImageLoadingListener onFailListener = new SimpleImageLoadingListener() {
@Override
public void onLoadingFailed(final String imageUri, final View view,
final FailReason failReason) {
showSnackBarError(new ErrorInfo(failReason.getCause(), UserAction.LOAD_IMAGE,
imageUri, info));
}
};
@Override
public void onError(final Exception e) {
showSnackBarError(new ErrorInfo(e, UserAction.LOAD_IMAGE,
info.getThumbnailUrl(), info));
}
});
IMAGE_LOADER.displayImage(info.getThumbnailUrl(), binding.detailThumbnailImageView,
ImageDisplayConstants.DISPLAY_THUMBNAIL_OPTIONS, onFailListener);
}
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);
}
PicassoHelper.loadAvatar(info.getSubChannelAvatarUrl()).tag(PICASSO_VIDEO_DETAILS_TAG)
.into(binding.detailSubChannelThumbnailView);
PicassoHelper.loadAvatar(info.getUploaderAvatarUrl()).tag(PICASSO_VIDEO_DETAILS_TAG)
.into(binding.detailUploaderThumbnailView);
}
/*//////////////////////////////////////////////////////////////////////////
@ -751,27 +742,26 @@ public final class VideoDetailFragment
&& player.getPlayQueue() != null
&& player.videoPlayerSelected()
&& 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,
// return false to let the MainActivity handle the onBack
if (stack.size() <= 1) {
restoreDefaultOrientation();
return false;
return false; // let MainActivity handle the onBack (e.g. to minimize the mini player)
}
// Remove top
stack.pop();
// Get stack item from the new top
assert stack.peek() != null;
setupFromHistoryItem(stack.peek());
setupFromHistoryItem(Objects.requireNonNull(stack.peek()));
return true;
}
private void setupFromHistoryItem(final StackItem item) {
setAutoPlay(false);
hideMainPlayer();
hideMainPlayerOnLoadingNewStream();
setInitialData(item.getServiceId(), item.getUrl(),
item.getTitle() == null ? "" : item.getTitle(), item.getPlayQueue());
@ -891,7 +881,7 @@ public final class VideoDetailFragment
.observeOn(AndroidSchedulers.mainThread())
.subscribe(result -> {
isLoading.set(false);
hideMainPlayer();
hideMainPlayerOnLoadingNewStream();
if (result.getAgeLimit() != NO_AGE_LIMIT && !prefs.getBoolean(
getString(R.string.show_age_restricted_content), false)) {
hideAgeRestrictedContent();
@ -906,8 +896,9 @@ public final class VideoDetailFragment
stack.push(new StackItem(serviceId, url, title, playQueue));
}
}
if (isAutoplayEnabled()) {
openVideoPlayer();
openVideoPlayerAutoFullscreen();
}
}
}, throwable -> showError(new ErrorInfo(throwable, UserAction.REQUESTED_STREAM,
@ -1112,7 +1103,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)
.getBoolean(this.getString(R.string.use_external_video_player_key), false)) {
showExternalPlaybackDialog();
@ -1121,6 +1134,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) {
// See UI changes while remote playQueue changes
if (!isPlayerAvailable()) {
@ -1154,12 +1179,19 @@ public final class VideoDetailFragment
}
addVideoPlayerView();
final Intent playerIntent = NavigationHelper
.getPlayerIntent(requireContext(), MainPlayer.class, queue, true, autoPlayEnabled);
final Intent playerIntent = NavigationHelper.getPlayerIntent(requireContext(),
MainPlayer.class, queue, true, autoPlayEnabled);
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()
|| playerService.getView() == null
|| !player.videoPlayerSelected()) {
@ -1167,8 +1199,12 @@ public final class VideoDetailFragment
}
removeVideoPlayerView();
playerService.stop(isAutoplayEnabled());
playerService.getView().setVisibility(View.GONE);
if (isAutoplayEnabled()) {
playerService.stopForImmediateReusing();
playerService.getView().setVisibility(View.GONE);
} else {
playerHolder.stopService();
}
}
private PlayQueue setupPlayQueueForIntent(final boolean append) {
@ -1261,7 +1297,7 @@ public final class VideoDetailFragment
final DisplayMetrics metrics = getResources().getDisplayMetrics();
if (getView() != null) {
final int height = (isInMultiWindow()
final int height = (DeviceUtils.isInMultiWindow(activity)
? requireView()
: activity.getWindow().getDecorView()).getHeight();
setHeightThumbnail(height, metrics);
@ -1284,7 +1320,7 @@ public final class VideoDetailFragment
requireView().getViewTreeObserver().removeOnPreDrawListener(preDrawListener);
if (isPlayerAvailable() && player.isFullscreen()) {
final int height = (isInMultiWindow()
final int height = (DeviceUtils.isInMultiWindow(activity)
? requireView()
: activity.getWindow().getDecorView()).getHeight();
// Height is zero when the view is not yet displayed like after orientation change
@ -1395,17 +1431,15 @@ public final class VideoDetailFragment
//////////////////////////////////////////////////////////////////////////*/
private void restoreDefaultOrientation() {
if (!isPlayerAvailable() || !player.videoPlayerSelected() || activity == null) {
return;
if (isPlayerAvailable() && player.videoPlayerSelected()) {
toggleFullscreenIfInFullscreenMode();
}
toggleFullscreenIfInFullscreenMode();
// This will show systemUI and pause the player.
// 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
// to physically rotate the tablet every time
if (!DeviceUtils.isTablet(activity)) {
if (activity != null && !DeviceUtils.isTablet(activity)) {
activity.setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED);
}
}
@ -1446,8 +1480,7 @@ public final class VideoDetailFragment
}
}
IMAGE_LOADER.cancelDisplayTask(binding.detailThumbnailImageView);
IMAGE_LOADER.cancelDisplayTask(binding.detailSubChannelThumbnailView);
PicassoHelper.cancelTag(PICASSO_VIDEO_DETAILS_TAG);
binding.detailThumbnailImageView.setImageBitmap(null);
binding.detailSubChannelThumbnailView.setImageBitmap(null);
}
@ -1818,7 +1851,7 @@ public final class VideoDetailFragment
|| error.type == ExoPlaybackException.TYPE_UNEXPECTED) {
// Properly exit from fullscreen
toggleFullscreenIfInFullscreenMode();
hideMainPlayer();
hideMainPlayerOnLoadingNewStream();
}
}
@ -1874,13 +1907,14 @@ public final class VideoDetailFragment
// from landscape to portrait every time.
// Just turn on fullscreen mode in landscape orientation
// or portrait & unlocked global orientation
final boolean isLandscape = DeviceUtils.isLandscape(requireContext());
if (DeviceUtils.isTablet(activity)
&& (!globalScreenOrientationLocked(activity) || isLandscape())) {
&& (!globalScreenOrientationLocked(activity) || isLandscape)) {
player.toggleFullscreen();
return;
}
final int newOrientation = isLandscape()
final int newOrientation = isLandscape
? ActivityInfo.SCREEN_ORIENTATION_PORTRAIT
: ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE;
@ -1952,15 +1986,17 @@ public final class VideoDetailFragment
| View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
| View.SYSTEM_UI_FLAG_HIDE_NAVIGATION
| View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY;
// 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 (!isInMultiWindow()) {
final boolean isInMultiWindow = DeviceUtils.isInMultiWindow(activity);
if (!isInMultiWindow) {
visibility |= View.SYSTEM_UI_FLAG_FULLSCREEN;
}
activity.getWindow().getDecorView().setSystemUiVisibility(visibility);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP
&& (isInMultiWindow() || (isPlayerAvailable() && player.isFullscreen()))) {
&& (isInMultiWindow || (isPlayerAvailable() && player.isFullscreen()))) {
activity.getWindow().setStatusBarColor(Color.TRANSPARENT);
activity.getWindow().setNavigationBarColor(Color.TRANSPARENT);
}
@ -2032,15 +2068,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
* and is empty but ready for any new actions. See cleanUp()
@ -2080,8 +2107,8 @@ public final class VideoDetailFragment
private void showClearingQueueConfirmation(final Runnable onAllow) {
new AlertDialog.Builder(activity)
.setTitle(R.string.clear_queue_confirmation_description)
.setNegativeButton(android.R.string.cancel, null)
.setPositiveButton(android.R.string.yes, (dialog, which) -> {
.setNegativeButton(R.string.cancel, null)
.setPositiveButton(R.string.ok, (dialog, which) -> {
onAllow.run();
dialog.dismiss();
}).show();
@ -2096,7 +2123,7 @@ public final class VideoDetailFragment
resolutions[i] = sortedVideoStreams.get(i).getResolution();
}
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) ->
ShareUtils.openUrlInBrowser(requireActivity(), url)
);
@ -2223,7 +2250,7 @@ public final class VideoDetailFragment
setOverlayElementsClickable(false);
hideSystemUiIfNeeded();
// Conditions when the player should be expanded to fullscreen
if (isLandscape()
if (DeviceUtils.isLandscape(requireContext())
&& isPlayerAvailable()
&& player.isPlaying()
&& !player.isFullscreen()
@ -2278,10 +2305,8 @@ public final class VideoDetailFragment
binding.overlayTitleTextView.setText(isEmpty(overlayTitle) ? "" : overlayTitle);
binding.overlayChannelTextView.setText(isEmpty(uploader) ? "" : uploader);
binding.overlayThumbnail.setImageResource(R.drawable.dummy_thumbnail_dark);
if (!isEmpty(thumbnailUrl)) {
IMAGE_LOADER.displayImage(thumbnailUrl, binding.overlayThumbnail,
ImageDisplayConstants.DISPLAY_THUMBNAIL_OPTIONS, null);
}
PicassoHelper.loadThumbnail(thumbnailUrl).tag(PICASSO_VIDEO_DETAILS_TAG)
.into(binding.overlayThumbnail);
}
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.PlayQueue;
import org.schabi.newpipe.util.ExtractorHelper;
import org.schabi.newpipe.util.ImageDisplayConstants;
import org.schabi.newpipe.util.Localization;
import org.schabi.newpipe.util.NavigationHelper;
import org.schabi.newpipe.util.external_communication.ShareUtils;
import org.schabi.newpipe.util.PicassoHelper;
import org.schabi.newpipe.util.ThemeHelper;
import java.util.ArrayList;
@ -66,7 +66,10 @@ import static org.schabi.newpipe.ktx.ViewUtils.animateBackgroundColor;
public class ChannelFragment extends BaseListInfoFragment<ChannelInfo>
implements View.OnClickListener {
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 Disposable subscribeButtonMonitor;
@ -421,10 +424,7 @@ public class ChannelFragment extends BaseListInfoFragment<ChannelInfo>
@Override
public void showLoading() {
super.showLoading();
IMAGE_LOADER.cancelDisplayTask(headerBinding.channelBannerImage);
IMAGE_LOADER.cancelDisplayTask(headerBinding.channelAvatarView);
IMAGE_LOADER.cancelDisplayTask(headerBinding.subChannelAvatarView);
PicassoHelper.cancelTag(PICASSO_CHANNEL_TAG);
animate(headerBinding.channelSubscribeButton, false, 100);
}
@ -433,13 +433,12 @@ public class ChannelFragment extends BaseListInfoFragment<ChannelInfo>
super.handleResult(result);
headerBinding.getRoot().setVisibility(View.VISIBLE);
IMAGE_LOADER.displayImage(result.getBannerUrl(), headerBinding.channelBannerImage,
ImageDisplayConstants.DISPLAY_BANNER_OPTIONS);
IMAGE_LOADER.displayImage(result.getAvatarUrl(), headerBinding.channelAvatarView,
ImageDisplayConstants.DISPLAY_AVATAR_OPTIONS);
IMAGE_LOADER.displayImage(result.getParentChannelAvatarUrl(),
headerBinding.subChannelAvatarView,
ImageDisplayConstants.DISPLAY_AVATAR_OPTIONS);
PicassoHelper.loadBanner(result.getBannerUrl()).tag(PICASSO_CHANNEL_TAG)
.into(headerBinding.channelBannerImage);
PicassoHelper.loadAvatar(result.getAvatarUrl()).tag(PICASSO_CHANNEL_TAG)
.into(headerBinding.channelAvatarView);
PicassoHelper.loadAvatar(result.getParentChannelAvatarUrl()).tag(PICASSO_CHANNEL_TAG)
.into(headerBinding.subChannelAvatarView);
headerBinding.channelSubscriberView.setVisibility(View.VISIBLE);
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.PlaylistPlayQueue;
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.Localization;
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;
public class PlaylistFragment extends BaseListInfoFragment<PlaylistInfo> {
private static final String PICASSO_PLAYLIST_TAG = "PICASSO_PLAYLIST_TAG";
private CompositeDisposable disposables;
private Subscription bookmarkReactor;
private AtomicBoolean isBookmarkButtonReady;
private RemotePlaylistManager remotePlaylistManager;
private PlaylistRemoteEntity playlistEntity;
/*//////////////////////////////////////////////////////////////////////////
// Views
//////////////////////////////////////////////////////////////////////////*/
@ -274,7 +278,7 @@ public class PlaylistFragment extends BaseListInfoFragment<PlaylistInfo> {
animate(headerBinding.getRoot(), false, 200);
animateHideRecyclerViewAllowingScrolling(itemsList);
IMAGE_LOADER.cancelDisplayTask(headerBinding.uploaderAvatarView);
PicassoHelper.cancelTag(PICASSO_PLAYLIST_TAG);
animate(headerBinding.uploaderLayout, false, 200);
}
@ -317,8 +321,8 @@ public class PlaylistFragment extends BaseListInfoFragment<PlaylistInfo> {
R.drawable.ic_radio)
);
} else {
IMAGE_LOADER.displayImage(avatarUrl, headerBinding.uploaderAvatarView,
ImageDisplayConstants.DISPLAY_AVATAR_OPTIONS);
PicassoHelper.loadAvatar(avatarUrl).tag(PICASSO_PLAYLIST_TAG)
.into(headerBinding.uploaderAvatarView);
}
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.ExceptionUtils;
import org.schabi.newpipe.local.history.HistoryRecordManager;
import org.schabi.newpipe.settings.NewPipeSettings;
import org.schabi.newpipe.util.Constants;
import org.schabi.newpipe.util.DeviceUtils;
import org.schabi.newpipe.util.ExtractorHelper;
@ -65,16 +66,19 @@ import org.schabi.newpipe.util.ServiceHelper;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Queue;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import icepick.State;
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
import io.reactivex.rxjava3.core.Flowable;
import io.reactivex.rxjava3.core.Observable;
import io.reactivex.rxjava3.core.Single;
import io.reactivex.rxjava3.disposables.CompositeDisposable;
import io.reactivex.rxjava3.disposables.Disposable;
import io.reactivex.rxjava3.schedulers.Schedulers;
@ -143,7 +147,8 @@ public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.I
@Nullable private Map<Integer, String> menuItemToFilterName = null;
private StreamingService service;
private Page nextPage;
private boolean isSuggestionsEnabled = true;
private boolean showLocalSuggestions = true;
private boolean showRemoteSuggestions = true;
private Disposable searchDisposable;
private Disposable suggestionDisposable;
@ -194,26 +199,14 @@ public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.I
public void onAttach(@NonNull final Context context) {
super.onAttach(context);
final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(activity);
showLocalSuggestions = NewPipeSettings.showLocalSearchSuggestions(activity, prefs);
showRemoteSuggestions = NewPipeSettings.showRemoteSearchSuggestions(activity, prefs);
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);
}
@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
public View onCreateView(final LayoutInflater inflater, @Nullable final ViewGroup container,
@Nullable final Bundle savedInstanceState) {
@ -222,6 +215,7 @@ public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.I
@Override
public void onViewCreated(@NonNull final View rootView, final Bundle savedInstanceState) {
searchBinding = FragmentSearchBinding.bind(rootView);
super.onViewCreated(rootView, savedInstanceState);
showSearchOnStart();
initSearchListeners();
@ -348,7 +342,6 @@ public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.I
@Override
protected void initViews(final View rootView, final Bundle savedInstanceState) {
super.initViews(rootView, savedInstanceState);
searchBinding = FragmentSearchBinding.bind(rootView);
searchBinding.suggestionsList.setAdapter(suggestionListAdapter);
new ItemTouchHelper(new ItemTouchHelper.Callback() {
@ -554,7 +547,7 @@ public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.I
if (DEBUG) {
Log.d(TAG, "onClick() called with: v = [" + v + "]");
}
if (isSuggestionsEnabled && !isErrorPanelVisible()) {
if ((showLocalSuggestions || showRemoteSuggestions) && !isErrorPanelVisible()) {
showSuggestionsPanel();
}
if (DeviceUtils.isTv(getContext())) {
@ -567,7 +560,8 @@ public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.I
Log.d(TAG, "onFocusChange() called with: "
+ "v = [" + v + "], hasFocus = [" + hasFocus + "]");
}
if (isSuggestionsEnabled && hasFocus && !isErrorPanelVisible()) {
if ((showLocalSuggestions || showRemoteSuggestions)
&& hasFocus && !isErrorPanelVisible()) {
showSuggestionsPanel();
}
});
@ -743,6 +737,34 @@ public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.I
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() {
if (DEBUG) {
Log.d(TAG, "initSuggestionObserver() called");
@ -753,73 +775,53 @@ public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.I
suggestionDisposable = suggestionPublisher
.debounce(SUGGESTIONS_DEBOUNCE, TimeUnit.MILLISECONDS)
.startWithItem(searchString != null
? searchString
: "")
.filter(ss -> isSuggestionsEnabled)
.startWithItem(searchString == null ? "" : searchString)
.switchMap(query -> {
final Flowable<List<SearchHistoryEntry>> flowable = historyRecordManager
.getRelatedSearches(query, 3, 25);
final Observable<List<SuggestionItem>> local = flowable.toObservable()
.map(searchHistoryEntries -> {
final List<SuggestionItem> result = new ArrayList<>();
for (final SearchHistoryEntry entry : searchHistoryEntries) {
result.add(new SuggestionItem(true, entry.getSearch()));
}
return result;
});
// Only show remote suggestions if they are enabled in settings and
// the query length is at least THRESHOLD_NETWORK_SUGGESTION
final boolean shallShowRemoteSuggestionsNow = showRemoteSuggestions
&& query.length() >= THRESHOLD_NETWORK_SUGGESTION;
if (query.length() < THRESHOLD_NETWORK_SUGGESTION) {
// Only pass through if the query length
// is equal or greater than THRESHOLD_NETWORK_SUGGESTION
return local.materialize();
if (showLocalSuggestions && shallShowRemoteSuggestionsNow) {
return Observable.zip(
getLocalSuggestionsObservable(query, 3),
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())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(listNotification -> {
if (listNotification.isOnNext()) {
handleSuggestions(listNotification.getValue());
} else if (listNotification.isOnError()) {
showError(new ErrorInfo(listNotification.getError(),
UserAction.GET_SUGGESTIONS, searchString, serviceId));
}
});
.subscribe(
listNotification -> {
if (listNotification.isOnNext()) {
if (listNotification.getValue() != null) {
handleSuggestions(listNotification.getValue());
}
} 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

View File

@ -1,5 +1,7 @@
package org.schabi.newpipe.fragments.list.search;
import androidx.annotation.NonNull;
public class SuggestionItem {
final boolean fromHistory;
public final String query;
@ -9,6 +11,20 @@ public class SuggestionItem {
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
public String toString() {
return "[" + fromHistory + "" + query + "]";

View File

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

View File

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

View File

@ -3,13 +3,12 @@ package org.schabi.newpipe.info_list
import android.view.View
import android.widget.ImageView
import android.widget.TextView
import com.nostra13.universalimageloader.core.ImageLoader
import com.xwray.groupie.GroupieViewHolder
import com.xwray.groupie.Item
import org.schabi.newpipe.R
import org.schabi.newpipe.extractor.stream.StreamSegment
import org.schabi.newpipe.util.ImageDisplayConstants
import org.schabi.newpipe.util.Localization
import org.schabi.newpipe.util.PicassoHelper
class StreamSegmentItem(
private val item: StreamSegment,
@ -24,10 +23,8 @@ class StreamSegmentItem(
override fun bind(viewHolder: GroupieViewHolder, position: Int) {
item.previewUrl?.let {
ImageLoader.getInstance().displayImage(
it, viewHolder.root.findViewById<ImageView>(R.id.previewImage),
ImageDisplayConstants.DISPLAY_THUMBNAIL_OPTIONS
)
PicassoHelper.loadThumbnail(it)
.into(viewHolder.root.findViewById<ImageView>(R.id.previewImage))
}
viewHolder.root.findViewById<TextView>(R.id.textViewTitle).text = item.title
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.info_list.InfoItemBuilder;
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 de.hdodenhof.circleimageview.CircleImageView;
@ -43,10 +43,7 @@ public class ChannelMiniInfoItemHolder extends InfoItemHolder {
itemTitleView.setText(item.getName());
itemAdditionalDetailView.setText(getDetailLine(item));
itemBuilder.getImageLoader()
.displayImage(item.getThumbnailUrl(),
itemThumbnailView,
ImageDisplayConstants.DISPLAY_THUMBNAIL_OPTIONS);
PicassoHelper.loadThumbnail(item.getThumbnailUrl()).into(itemThumbnailView);
itemView.setOnClickListener(view -> {
if (itemBuilder.getOnChannelSelectedListener() != null) {

View File

@ -1,17 +1,16 @@
package org.schabi.newpipe.info_list.holder;
import android.content.SharedPreferences;
import android.text.TextUtils;
import android.text.method.LinkMovementMethod;
import android.text.style.URLSpan;
import android.text.util.Linkify;
import android.util.Log;
import android.view.View;
import android.view.ViewGroup;
import android.widget.RelativeLayout;
import android.widget.TextView;
import androidx.appcompat.app.AppCompatActivity;
import androidx.preference.PreferenceManager;
import org.schabi.newpipe.R;
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.util.CommentTextOnTouchListener;
import org.schabi.newpipe.util.DeviceUtils;
import org.schabi.newpipe.util.ImageDisplayConstants;
import org.schabi.newpipe.util.Localization;
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.PicassoHelper;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import de.hdodenhof.circleimageview.CircleImageView;
public class CommentsMiniInfoItemHolder extends InfoItemHolder {
private static final String TAG = "CommentsMiniIIHolder";
private static final int COMMENT_DEFAULT_LINES = 2;
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 commentVerticalPadding;
private SharedPreferences preferences = null;
private final RelativeLayout itemRoot;
public final CircleImageView itemThumbnailView;
private final TextView itemContentView;
private final TextView itemLikesCountView;
private final TextView itemDislikesCountView;
private final TextView itemPublishedTime;
private String commentText;
@ -53,20 +51,21 @@ public class CommentsMiniInfoItemHolder extends InfoItemHolder {
private final Linkify.TransformFilter timestampLink = new Linkify.TransformFilter() {
@Override
public String transformUrl(final Matcher match, final String url) {
int timestamp = 0;
final String hours = match.group(1);
final String minutes = match.group(2);
final String seconds = match.group(3);
if (hours != null) {
timestamp += (Integer.parseInt(hours.replace(":", "")) * 3600);
try {
final TimestampExtractor.TimestampMatchDTO timestampMatchDTO =
TimestampExtractor.getTimestampFromMatcher(match, commentText);
if (timestampMatchDTO == null) {
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);
itemThumbnailView = itemView.findViewById(R.id.itemThumbnailView);
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);
itemContentView = itemView.findViewById(R.id.itemCommentContentView);
downloadThumbnailKey = infoItemBuilder.getContext().
getString(R.string.download_thumbnail_key);
commentHorizontalPadding = (int) infoItemBuilder.getContext()
.getResources().getDimension(R.dimen.comments_horizontal_padding);
commentVerticalPadding = (int) infoItemBuilder.getContext()
@ -103,14 +98,8 @@ public class CommentsMiniInfoItemHolder extends InfoItemHolder {
}
final CommentsInfoItem item = (CommentsInfoItem) infoItem;
preferences = PreferenceManager.getDefaultSharedPreferences(itemBuilder.getContext());
itemBuilder.getImageLoader()
.displayImage(item.getUploaderAvatarUrl(),
itemThumbnailView,
ImageDisplayConstants.DISPLAY_THUMBNAIL_OPTIONS);
if (preferences.getBoolean(downloadThumbnailKey, true)) {
PicassoHelper.loadAvatar(item.getUploaderAvatarUrl()).into(itemThumbnailView);
if (PicassoHelper.getShouldLoadImages()) {
itemThumbnailView.setVisibility(View.VISIBLE);
itemRoot.setPadding(commentVerticalPadding, commentVerticalPadding,
commentVerticalPadding, commentVerticalPadding);
@ -254,7 +243,14 @@ public class CommentsMiniInfoItemHolder extends InfoItemHolder {
}
private void linkify() {
Linkify.addLinks(itemContentView, Linkify.WEB_URLS);
Linkify.addLinks(itemContentView, PATTERN, null, null, timestampLink);
Linkify.addLinks(
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.info_list.InfoItemBuilder;
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;
public class PlaylistMiniInfoItemHolder extends InfoItemHolder {
@ -46,9 +46,7 @@ public class PlaylistMiniInfoItemHolder extends InfoItemHolder {
.localizeStreamCountMini(itemStreamCountView.getContext(), item.getStreamCount()));
itemUploaderView.setText(item.getUploaderName());
itemBuilder.getImageLoader()
.displayImage(item.getThumbnailUrl(), itemThumbnailView,
ImageDisplayConstants.DISPLAY_THUMBNAIL_OPTIONS);
PicassoHelper.loadPlaylistThumbnail(item.getThumbnailUrl()).into(itemThumbnailView);
itemView.setOnClickListener(view -> {
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.ktx.ViewUtils;
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.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
itemBuilder.getImageLoader()
.displayImage(item.getThumbnailUrl(),
itemThumbnailView,
ImageDisplayConstants.DISPLAY_THUMBNAIL_OPTIONS);
PicassoHelper.loadThumbnail(item.getThumbnailUrl()).into(itemThumbnailView);
itemView.setOnClickListener(view -> {
if (itemBuilder.getOnStreamSelectedListener() != null) {

View File

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

View File

@ -206,7 +206,7 @@ class FeedFragment : BaseStateFragment<FeedState>() {
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()
.show()
return true
@ -362,6 +362,7 @@ class FeedFragment : BaseStateFragment<FeedState>() {
StreamDialogEntry.mark_as_watched
)
}
entries.add(StreamDialogEntry.show_channel_details)
StreamDialogEntry.setEnabledEntries(entries)
InfoItemDialog(activity, item, StreamDialogEntry.getCommands(context)) { _, which ->

View File

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

View File

@ -300,6 +300,12 @@ class FeedLoadService : Service() {
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.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) {
Log.e(TAG, "Error while storing result", 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.Disposable;
import static org.schabi.newpipe.extractor.utils.Utils.isNullOrEmpty;
public class StatisticsPlaylistFragment
extends BaseLocalListFragment<List<StreamStatisticsEntry>, Void> {
private final CompositeDisposable disposables = new CompositeDisposable();
@ -363,10 +361,7 @@ public class StatisticsPlaylistFragment
if (KoreUtils.shouldShowPlayWithKodi(context, infoItem.getServiceId())) {
entries.add(StreamDialogEntry.play_with_kodi);
}
if (!isNullOrEmpty(infoItem.getUploaderUrl())) {
entries.add(StreamDialogEntry.show_channel_details);
}
entries.add(StreamDialogEntry.show_channel_details);
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.local.LocalItemBuilder;
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 java.time.format.DateTimeFormatter;
@ -36,8 +36,7 @@ public class LocalPlaylistItemHolder extends PlaylistItemHolder {
itemStreamCountView.getContext(), item.streamCount));
itemUploaderView.setVisibility(View.INVISIBLE);
itemBuilder.displayImage(item.thumbnailUrl, itemThumbnailView,
ImageDisplayConstants.DISPLAY_PLAYLIST_OPTIONS);
PicassoHelper.loadPlaylistThumbnail(item.thumbnailUrl).into(itemThumbnailView);
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.local.LocalItemBuilder;
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.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
itemBuilder.displayImage(item.getStreamEntity().getThumbnailUrl(), itemThumbnailView,
ImageDisplayConstants.DISPLAY_THUMBNAIL_OPTIONS);
PicassoHelper.loadThumbnail(item.getStreamEntity().getThumbnailUrl())
.into(itemThumbnailView);
itemView.setOnClickListener(view -> {
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.local.LocalItemBuilder;
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.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
itemBuilder.displayImage(item.getStreamEntity().getThumbnailUrl(), itemThumbnailView,
ImageDisplayConstants.DISPLAY_THUMBNAIL_OPTIONS);
PicassoHelper.loadThumbnail(item.getStreamEntity().getThumbnailUrl())
.into(itemThumbnailView);
itemView.setOnClickListener(view -> {
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.local.LocalItemBuilder;
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 java.time.format.DateTimeFormatter;
@ -44,9 +44,7 @@ public class RemotePlaylistItemHolder extends PlaylistItemHolder {
itemUploaderView.setText(NewPipe.getNameOfService(item.getServiceId()));
}
itemBuilder.displayImage(item.getThumbnailUrl(), itemThumbnailView,
ImageDisplayConstants.DISPLAY_PLAYLIST_OPTIONS);
PicassoHelper.loadPlaylistThumbnail(item.getThumbnailUrl()).into(itemThumbnailView);
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.subjects.PublishSubject;
import static org.schabi.newpipe.extractor.utils.Utils.isNullOrEmpty;
import static org.schabi.newpipe.ktx.ViewUtils.animate;
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())) {
entries.add(StreamDialogEntry.play_with_kodi);
}
if (!isNullOrEmpty(infoItem.getUploaderUrl())) {
entries.add(StreamDialogEntry.show_channel_details);
}
entries.add(StreamDialogEntry.show_channel_details);
StreamDialogEntry.setEnabledEntries(entries);

View File

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

View File

@ -179,7 +179,7 @@ class SubscriptionFragment : BaseStateFragment<SubscriptionState>() {
}
private fun onImportPreviousSelected() {
requestImportLauncher.launch(StoredFileHelper.getPicker(activity))
requestImportLauncher.launch(StoredFileHelper.getPicker(activity, JSON_MIME_TYPE))
}
private fun onExportSelected() {
@ -187,7 +187,7 @@ class SubscriptionFragment : BaseStateFragment<SubscriptionState>() {
val exportName = "newpipe_subscriptions_$date.json"
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)
}
fun requestExportResult(result: ActivityResult) {
private fun requestExportResult(result: ActivityResult) {
if (result.data != null && result.resultCode == Activity.RESULT_OK) {
activity.startService(
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) {
ImportConfirmationDialog.show(
this,
@ -407,4 +407,8 @@ class SubscriptionFragment : BaseStateFragment<SubscriptionState>() {
super.hideLoading()
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() {
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) {

View File

@ -143,21 +143,15 @@ class FeedGroupDialog : DialogFragment(), BackPressable {
).get(FeedGroupDialogViewModel::class.java)
viewModel.groupLiveData.observe(viewLifecycleOwner, Observer(::handleGroup))
viewModel.subscriptionsLiveData.observe(
viewLifecycleOwner,
Observer {
setupSubscriptionPicker(it.first, it.second)
viewModel.subscriptionsLiveData.observe(viewLifecycleOwner) {
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 {
add(subscriptionMainSection)
@ -437,7 +431,7 @@ class FeedGroupDialog : DialogFragment(), BackPressable {
feedGroupCreateBinding.confirmButton.setText(
when {
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.widget.ImageView
import android.widget.TextView
import com.nostra13.universalimageloader.core.ImageLoader
import com.xwray.groupie.GroupieViewHolder
import com.xwray.groupie.Item
import org.schabi.newpipe.R
import org.schabi.newpipe.extractor.channel.ChannelInfoItem
import org.schabi.newpipe.util.ImageDisplayConstants
import org.schabi.newpipe.util.Localization
import org.schabi.newpipe.util.OnClickGesture
import org.schabi.newpipe.util.PicassoHelper
class ChannelItem(
private val infoItem: ChannelInfoItem,
@ -40,10 +39,7 @@ class ChannelItem(
itemChannelDescriptionView.text = infoItem.description
}
ImageLoader.getInstance().displayImage(
infoItem.thumbnailUrl, itemThumbnailView,
ImageDisplayConstants.DISPLAY_THUMBNAIL_OPTIONS
)
PicassoHelper.loadThumbnail(infoItem.thumbnailUrl).into(itemThumbnailView)
gesturesListener?.run {
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 androidx.core.view.isGone
import androidx.core.view.isVisible
import com.nostra13.universalimageloader.core.ImageLoader
import com.xwray.groupie.viewbinding.BindableItem
import com.xwray.groupie.viewbinding.GroupieViewHolder
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.ktx.AnimationType
import org.schabi.newpipe.ktx.animate
import org.schabi.newpipe.util.ImageDisplayConstants
import org.schabi.newpipe.util.PicassoHelper
data class PickerSubscriptionItem(
val subscriptionEntity: SubscriptionEntity,
@ -22,11 +21,7 @@ data class PickerSubscriptionItem(
override fun getSpanSize(spanCount: Int, position: Int): Int = 1
override fun bind(viewBinding: PickerSubscriptionItemBinding, position: Int) {
ImageLoader.getInstance().displayImage(
subscriptionEntity.avatarUrl,
viewBinding.thumbnailView, ImageDisplayConstants.DISPLAY_AVATAR_OPTIONS
)
PicassoHelper.loadAvatar(subscriptionEntity.avatarUrl).into(viewBinding.thumbnailView)
viewBinding.titleView.text = subscriptionEntity.name
viewBinding.selectedHighlight.isVisible = isSelected
}

View File

@ -19,6 +19,9 @@
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.net.Uri;
import android.text.TextUtils;
@ -46,6 +49,7 @@ import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
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.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 static final int CHANNEL_URL_MODE = 0;
public static final int INPUT_STREAM_MODE = 1;
@ -89,6 +90,8 @@ public class SubscriptionsImportService extends BaseImportExportService {
private String channelUrl;
@Nullable
private InputStream inputStream;
@Nullable
private String inputStreamType;
@Override
public int onStartCommand(final Intent intent, final int flags, final int startId) {
@ -111,8 +114,20 @@ public class SubscriptionsImportService extends BaseImportExportService {
}
try {
inputStream = new SharpInputStream(
new StoredFileHelper(this, uri, DEFAULT_MIME).getStream());
final StoredFileHelper fileHelper = new StoredFileHelper(this, uri, DEFAULT_MIME);
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) {
handleError(e);
return START_NOT_STICKY;
@ -248,9 +263,9 @@ public class SubscriptionsImportService extends BaseImportExportService {
final Throwable error = notification.getError();
final Throwable cause = error.getCause();
if (error instanceof IOException) {
throw (IOException) error;
throw error;
} else if (cause instanceof IOException) {
throw (IOException) cause;
throw cause;
} else if (ExceptionUtils.isNetworkRelated(error)) {
throw new IOException(error);
}
@ -280,9 +295,12 @@ public class SubscriptionsImportService extends BaseImportExportService {
}
private Flowable<List<SubscriptionItem>> importFromInputStream() {
Objects.requireNonNull(inputStream);
Objects.requireNonNull(inputStreamType);
return Flowable.fromCallable(() -> NewPipe.getService(currentServiceId)
.getSubscriptionExtractor()
.fromInputStream(inputStream));
.fromInputStream(inputStream, inputStreamType));
}
private Flowable<List<SubscriptionItem>> importFromPreviousExport() {

View File

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

View File

@ -1,9 +1,5 @@
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.Context;
import android.content.Intent;
@ -16,7 +12,6 @@ import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.widget.PopupMenu;
import android.widget.SeekBar;
import androidx.annotation.Nullable;
@ -47,16 +42,18 @@ import org.schabi.newpipe.util.PermissionHelper;
import org.schabi.newpipe.util.ServiceHelper;
import org.schabi.newpipe.util.ThemeHelper;
import java.util.Collections;
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;
public final class PlayQueueActivity extends AppCompatActivity
implements PlayerEventListener, SeekBar.OnSeekBarChangeListener,
View.OnClickListener, PlaybackParameterDialog.Callback {
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;
protected Player player;
@ -279,49 +276,6 @@ public final class PlayQueueActivity extends AppCompatActivity
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
////////////////////////////////////////////////////////////////////////////
@ -369,13 +323,9 @@ public final class PlayQueueActivity extends AppCompatActivity
@Override
public void held(final PlayQueueItem item, final View view) {
if (player == null) {
return;
}
final int index = player.getPlayQueue().indexOf(item);
if (index != -1) {
buildItemPopupMenu(item, view);
if (player != null && player.getPlayQueue().indexOf(item) != -1) {
openPopupMenu(player.getPlayQueue(), item, view, false,
getSupportFragmentManager(), PlayQueueActivity.this);
}
}

View File

@ -18,6 +18,7 @@ import android.graphics.BitmapFactory;
import android.graphics.Color;
import android.graphics.PorterDuff;
import android.graphics.PorterDuffColorFilter;
import android.graphics.drawable.Drawable;
import android.media.AudioManager;
import android.net.Uri;
import android.os.Build;
@ -82,9 +83,8 @@ import com.google.android.exoplayer2.upstream.DefaultBandwidthMeter;
import com.google.android.exoplayer2.util.Util;
import com.google.android.exoplayer2.video.VideoListener;
import com.google.android.material.floatingactionbutton.FloatingActionButton;
import com.nostra13.universalimageloader.core.ImageLoader;
import com.nostra13.universalimageloader.core.assist.FailReason;
import com.nostra13.universalimageloader.core.listener.ImageLoadingListener;
import com.squareup.picasso.Picasso;
import com.squareup.picasso.Target;
import org.schabi.newpipe.DownloaderImpl;
import org.schabi.newpipe.MainActivity;
@ -128,11 +128,11 @@ import org.schabi.newpipe.player.resolver.VideoPlaybackResolver;
import org.schabi.newpipe.player.seekbarpreview.SeekbarPreviewThumbnailHelper;
import org.schabi.newpipe.player.seekbarpreview.SeekbarPreviewThumbnailHolder;
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.NavigationHelper;
import org.schabi.newpipe.util.PicassoHelper;
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.views.ExpandableSurfaceView;
@ -159,7 +159,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.RepeatMode;
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.utils.Utils.isNullOrEmpty;
import static org.schabi.newpipe.ktx.ViewUtils.animate;
import static org.schabi.newpipe.ktx.ViewUtils.animateRotation;
import static org.schabi.newpipe.player.MainPlayer.ACTION_CLOSE;
@ -196,7 +198,6 @@ import static org.schabi.newpipe.util.Localization.containsCaseInsensitive;
public final class Player implements
EventListener,
PlaybackListener,
ImageLoadingListener,
VideoListener,
SeekBar.OnSeekBarChangeListener,
View.OnClickListener,
@ -237,7 +238,7 @@ public final class Player implements
//////////////////////////////////////////////////////////////////////////*/
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_HIDE_TIME = 2000; // 2 Seconds
public static final int DPAD_CONTROLS_HIDE_TIME = 7000; // 7 Seconds
@ -390,7 +391,7 @@ public final class Player implements
/*//////////////////////////////////////////////////////////////////////////
// Constructor
//////////////////////////////////////////////////////////////////////////*/
//region
//region Constructor
public Player(@NonNull final MainPlayer service) {
this.service = service;
@ -437,7 +438,7 @@ public final class Player implements
/*//////////////////////////////////////////////////////////////////////////
// Setup and initialization
//////////////////////////////////////////////////////////////////////////*/
//region
//region Setup and initialization
public void setupFromView(@NonNull final PlayerBinding playerBinding) {
initViews(playerBinding);
@ -585,7 +586,7 @@ public final class Player implements
/*//////////////////////////////////////////////////////////////////////////
// Playback initialization via intent
//////////////////////////////////////////////////////////////////////////*/
//region
//region Playback initialization via intent
public void handleIntent(@NonNull final Intent intent) {
// fail fast if no play queue was provided
@ -613,13 +614,16 @@ public final class Player implements
playQueue.append(newQueue.getStreams());
if ((intent.getBooleanExtra(SELECT_ON_APPEND, false)
|| currentState == STATE_COMPLETED) && newQueue.getStreams().size() > 0) {
|| currentState == STATE_COMPLETED) && !newQueue.getStreams().isEmpty()) {
playQueue.setIndex(sizeBeforeAppend);
}
return;
}
// needed for tablets, check the function for a better explanation
directlyOpenFullscreenIfNeeded();
final PlaybackParameters savedParameters = retrievePlaybackParametersFromPrefs(this);
final float playbackSpeed = savedParameters.speed;
final float playbackPitch = savedParameters.pitch;
@ -671,6 +675,7 @@ public final class Player implements
&& isPlaybackResumeEnabled(this)
&& !samePlayQueue
&& !newQueue.isEmpty()
&& newQueue.getItem() != null
&& newQueue.getItem().getRecoveryPosition() == PlayQueueItem.RECOVERY_UNSET) {
databaseUpdateDisposable.add(recordManager.loadStreamState(newQueue.getItem())
.observeOn(AndroidSchedulers.mainThread())
@ -742,6 +747,22 @@ public final class Player implements
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,
@RepeatMode final int repeatMode,
final float playbackSpeed,
@ -774,7 +795,7 @@ public final class Player implements
/*//////////////////////////////////////////////////////////////////////////
// Destroy and recovery
//////////////////////////////////////////////////////////////////////////*/
//region
//region Destroy and recovery
private void destroyPlayer() {
if (DEBUG) {
@ -820,7 +841,7 @@ public final class Player implements
databaseUpdateDisposable.clear();
progressUpdateDisposable.set(null);
ImageLoader.getInstance().stop();
PicassoHelper.cancelTag(PicassoHelper.PLAYER_THUMBNAIL_TAG); // cancel thumbnail loading
if (binding != null) {
binding.endScreen.setImageBitmap(null);
@ -883,7 +904,7 @@ public final class Player implements
/*//////////////////////////////////////////////////////////////////////////
// Player type specific setup
//////////////////////////////////////////////////////////////////////////*/
//region
//region Player type specific setup
private void initVideoPlayer() {
// restore last resize mode
@ -945,7 +966,7 @@ public final class Player implements
/*//////////////////////////////////////////////////////////////////////////
// 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.
@ -1059,7 +1080,7 @@ public final class Player implements
/*//////////////////////////////////////////////////////////////////////////
// Broadcast receiver
//////////////////////////////////////////////////////////////////////////*/
//region
//region Broadcast receiver
private void setupBroadcastReceiver() {
if (DEBUG) {
@ -1211,18 +1232,49 @@ public final class Player implements
/*//////////////////////////////////////////////////////////////////////////
// Thumbnail loading
//////////////////////////////////////////////////////////////////////////*/
//region
//region Thumbnail loading
private void initThumbnail(final String url) {
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;
}
ImageLoader.getInstance().resume();
ImageLoader.getInstance()
.loadImage(url, ImageDisplayConstants.DISPLAY_THUMBNAIL_OPTIONS, this);
// scale down the notification thumbnail for performance
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 + "]");
}
}
});
}
/**
@ -1296,61 +1348,6 @@ public final class Player implements
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
@ -1358,7 +1355,7 @@ public final class Player implements
/*//////////////////////////////////////////////////////////////////////////
// Popup player utils
//////////////////////////////////////////////////////////////////////////*/
//region
//region Popup player utils
/**
* Check if {@link #popupLayoutParams}' position is within a arbitrary boundary
@ -1533,7 +1530,7 @@ public final class Player implements
/*//////////////////////////////////////////////////////////////////////////
// Playback parameters
//////////////////////////////////////////////////////////////////////////*/
//region
//region Playback parameters
public float getPlaybackSpeed() {
return getPlaybackParameters().speed;
@ -1586,7 +1583,7 @@ public final class Player implements
/*//////////////////////////////////////////////////////////////////////////
// Progress loop and updates
//////////////////////////////////////////////////////////////////////////*/
//region
//region Progress loop and updates
private void onUpdateProgress(final int currentProgress,
final int duration,
@ -1596,8 +1593,7 @@ public final class Player implements
}
if (duration != binding.playbackSeekBar.getMax()) {
binding.playbackEndTime.setText(getTimeString(duration));
binding.playbackSeekBar.setMax(duration);
setVideoDurationToControls(duration);
}
if (currentState != STATE_PAUSED) {
if (currentState != STATE_PAUSED_SEEK) {
@ -1802,7 +1798,7 @@ public final class Player implements
/*//////////////////////////////////////////////////////////////////////////
// Controls showing / hiding
//////////////////////////////////////////////////////////////////////////*/
//region
//region Controls showing / hiding
public boolean isControlsVisible() {
return binding != null && binding.playbackControlRoot.getVisibility() == View.VISIBLE;
@ -1972,7 +1968,7 @@ public final class Player implements
/*//////////////////////////////////////////////////////////////////////////
// Playback states
//////////////////////////////////////////////////////////////////////////*/
//region
//region Playback states
@Override // exoplayer listener
public void onPlayerStateChanged(final boolean playWhenReady, final int playbackState) {
@ -2097,8 +2093,8 @@ public final class Player implements
Log.d(TAG, "onPrepared() called with: playWhenReady = [" + playWhenReady + "]");
}
binding.playbackSeekBar.setMax((int) simpleExoPlayer.getDuration());
binding.playbackEndTime.setText(getTimeString((int) simpleExoPlayer.getDuration()));
setVideoDurationToControls((int) simpleExoPlayer.getDuration());
binding.playbackSpeed.setText(formatSpeed(getPlaybackSpeed()));
if (playWhenReady) {
@ -2293,7 +2289,7 @@ public final class Player implements
/*//////////////////////////////////////////////////////////////////////////
// Repeat and shuffle
//////////////////////////////////////////////////////////////////////////*/
//region
//region Repeat and shuffle
public void onRepeatClicked() {
if (DEBUG) {
@ -2330,7 +2326,7 @@ public final class Player implements
Log.d(TAG, "ExoPlayer - onRepeatModeChanged() called with: "
+ "repeatMode = [" + repeatMode + "]");
}
setRepeatModeButton(((AppCompatImageButton) binding.repeatButton), repeatMode);
setRepeatModeButton(binding.repeatButton, repeatMode);
onShuffleOrRepeatModeChanged();
}
@ -2382,7 +2378,7 @@ public final class Player implements
/*//////////////////////////////////////////////////////////////////////////
// Mute / Unmute
//////////////////////////////////////////////////////////////////////////*/
//region
//region Mute / Unmute
public void onMuteUnmuteButtonClicked() {
if (DEBUG) {
@ -2408,7 +2404,7 @@ public final class Player implements
/*//////////////////////////////////////////////////////////////////////////
// ExoPlayer listeners (that didn't fit in other categories)
//////////////////////////////////////////////////////////////////////////*/
//region
//region ExoPlayer listeners (that didn't fit in other categories)
@Override
public void onTimelineChanged(@NonNull final Timeline timeline, final int reason) {
@ -2496,7 +2492,7 @@ public final class Player implements
/*//////////////////////////////////////////////////////////////////////////
// Errors
//////////////////////////////////////////////////////////////////////////*/
//region
//region Errors
/**
* Process exceptions produced by {@link com.google.android.exoplayer2.ExoPlayer ExoPlayer}.
* <p>There are multiple types of errors:</p>
@ -2597,7 +2593,7 @@ public final class Player implements
/*//////////////////////////////////////////////////////////////////////////
// Playback position and seek
//////////////////////////////////////////////////////////////////////////*/
//region
//region Playback position and seek
@Override // own playback listener (this is a getter)
public boolean isApproachingPlaybackEdge(final long timeToEndMillis) {
@ -2740,6 +2736,20 @@ public final class Player implements
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
@ -2747,7 +2757,7 @@ public final class Player implements
/*//////////////////////////////////////////////////////////////////////////
// Player actions (play, pause, previous, fast-forward, ...)
//////////////////////////////////////////////////////////////////////////*/
//region
//region Player actions (play, pause, previous, fast-forward, ...)
public void play() {
if (DEBUG) {
@ -2789,7 +2799,9 @@ public final class Player implements
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();
} else {
play();
@ -2855,7 +2867,7 @@ public final class Player implements
/*//////////////////////////////////////////////////////////////////////////
// StreamInfo history: views and progress
//////////////////////////////////////////////////////////////////////////*/
//region
//region StreamInfo history: views and progress
private void registerStreamViewed() {
if (currentMetadata != null) {
@ -2913,7 +2925,7 @@ public final class Player implements
/*//////////////////////////////////////////////////////////////////////////
// Metadata
//////////////////////////////////////////////////////////////////////////*/
//region
//region Metadata
private void onMetadataChanged(@NonNull final MediaSourceTag tag) {
final StreamInfo info = tag.getMetadata();
@ -3022,7 +3034,7 @@ public final class Player implements
/*//////////////////////////////////////////////////////////////////////////
// Play queue, segments and streams
//////////////////////////////////////////////////////////////////////////*/
//region
//region Play queue, segments and streams
private void maybeAutoQueueNextStream(@NonNull final MediaSourceTag metadata) {
if (playQueue == null || playQueue.getIndex() != playQueue.size() - 1
@ -3177,7 +3189,7 @@ public final class Player implements
private StreamSegmentAdapter.StreamSegmentListener getStreamSegmentListener() {
return (item, seconds) -> {
segmentAdapter.selectSegment(item);
seekTo(seconds * 1000);
seekTo(seconds * 1000L);
triggerProgressUpdate();
};
}
@ -3187,7 +3199,7 @@ public final class Player implements
final List<StreamSegment> segments = currentMetadata.getMetadata().getStreamSegments();
for (int i = 0; i < segments.size(); i++) {
if (segments.get(i).getStartTimeSeconds() * 1000 > playbackPosition) {
if (segments.get(i).getStartTimeSeconds() * 1000L > playbackPosition) {
break;
}
nearestPosition++;
@ -3222,9 +3234,9 @@ public final class Player implements
@Override
public void held(final PlayQueueItem item, final View view) {
final int index = playQueue.indexOf(item);
if (index != -1) {
playQueue.remove(index);
if (playQueue.indexOf(item) != -1) {
openPopupMenu(playQueue, item, view, true,
getParentActivity().getSupportFragmentManager(), context);
}
}
@ -3338,7 +3350,7 @@ public final class Player implements
/*//////////////////////////////////////////////////////////////////////////
// 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() {
if (qualityPopupMenu == null) {
@ -3541,7 +3553,7 @@ public final class Player implements
/*//////////////////////////////////////////////////////////////////////////
// Captions (text tracks)
//////////////////////////////////////////////////////////////////////////*/
//region
//region Captions (text tracks)
private void setupSubtitleView() {
final float captionScale = PlayerHelper.getCaptionScale(context);
@ -3620,7 +3632,7 @@ public final class Player implements
/*//////////////////////////////////////////////////////////////////////////
// Click listeners
//////////////////////////////////////////////////////////////////////////*/
//region
//region Click listeners
@Override
public void onClick(final View v) {
@ -3808,7 +3820,7 @@ public final class Player implements
/*//////////////////////////////////////////////////////////////////////////
// Video size, resize, orientation, fullscreen
//////////////////////////////////////////////////////////////////////////*/
//region
//region Video size, resize, orientation, fullscreen
private void setupScreenRotationButton() {
binding.screenRotationButton.setVisibility(videoPlayerSelected()
@ -3863,11 +3875,9 @@ public final class Player implements
if (DEBUG) {
Log.d(TAG, "toggleFullscreen() called");
}
if (popupPlayerSelected() || exoPlayerIsNull() || currentMetadata == null
|| fragmentListener == null) {
if (popupPlayerSelected() || exoPlayerIsNull() || fragmentListener == null) {
return;
}
//changeState(STATE_BLOCKED); TODO check what this does
isFullscreen = !isFullscreen;
if (!isFullscreen) {
@ -3915,7 +3925,7 @@ public final class Player implements
/*//////////////////////////////////////////////////////////////////////////
// Gestures
//////////////////////////////////////////////////////////////////////////*/
//region
//region Gestures
@SuppressWarnings("checkstyle:ParameterNumber")
private void onLayoutChange(final View view, final int l, final int t, final int r, final int b,
@ -3979,7 +3989,7 @@ public final class Player implements
/*//////////////////////////////////////////////////////////////////////////
// Activity / fragment binding
//////////////////////////////////////////////////////////////////////////*/
//region
//region Activity / fragment binding
public void setFragmentListener(final PlayerServiceEventListener listener) {
fragmentListener = listener;
@ -4118,7 +4128,7 @@ public final class Player implements
/*//////////////////////////////////////////////////////////////////////////
// Getters
//////////////////////////////////////////////////////////////////////////*/
//region
//region Getters
public int getCurrentState() {
return currentState;
@ -4304,6 +4314,7 @@ public final class Player implements
// SurfaceHolderCallback helpers
//////////////////////////////////////////////////////////////////////////*/
//region SurfaceHolderCallback helpers
private void setupVideoSurface() {
// make sure there is nothing left over from previous calls
cleanupVideoSurface();
@ -4331,5 +4342,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.analytics.AnalyticsListener;
import com.google.android.exoplayer2.decoder.DecoderCounters;
public class AudioReactor implements AudioManager.OnAudioFocusChangeListener, AnalyticsListener {
@ -50,6 +51,7 @@ public class AudioReactor implements AudioManager.OnAudioFocusChangeListener, An
public void dispose() {
abandonAudioFocus();
player.removeAnalyticsListener(this);
notifyAudioSessionUpdate(false, player.getAudioSessionId());
}
/*//////////////////////////////////////////////////////////////////////////
@ -149,11 +151,21 @@ public class AudioReactor implements AudioManager.OnAudioFocusChangeListener, An
@Override
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()) {
return;
}
final Intent intent = new Intent(AudioEffect.ACTION_OPEN_AUDIO_EFFECT_CONTROL_SESSION);
final Intent intent = new Intent(active
? 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_PACKAGE_NAME, context.getPackageName());
context.sendBroadcast(intent);

View File

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

View File

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

View File

@ -239,6 +239,11 @@ public final class PlayerHelper {
.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) {
return getPreferences(context)
.getBoolean(context.getString(R.string.auto_queue_key), false);
@ -307,22 +312,6 @@ public final class PlayerHelper {
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() {
return new AdaptiveTrackSelection.Factory(
1000,

View File

@ -27,6 +27,7 @@ public class PlayQueueItem implements Serializable {
private final String thumbnailUrl;
@NonNull
private final String uploader;
private final String uploaderUrl;
@NonNull
private final StreamType streamType;
@ -37,7 +38,8 @@ public class PlayQueueItem implements Serializable {
PlayQueueItem(@NonNull final StreamInfo info) {
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) {
setRecoveryPosition(info.getStartPosition() * 1000);
@ -46,19 +48,21 @@ public class PlayQueueItem implements Serializable {
PlayQueueItem(@NonNull final StreamInfoItem item) {
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,
final int serviceId, final long duration,
@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.url = url != null ? url : EMPTY_STRING;
this.serviceId = serviceId;
this.duration = duration;
this.thumbnailUrl = thumbnailUrl != null ? thumbnailUrl : EMPTY_STRING;
this.uploader = uploader != null ? uploader : EMPTY_STRING;
this.uploaderUrl = uploaderUrl;
this.streamType = streamType;
this.recoveryPosition = RECOVERY_UNSET;
@ -92,6 +96,10 @@ public class PlayQueueItem implements Serializable {
return uploader;
}
public String getUploaderUrl() {
return uploaderUrl;
}
@NonNull
public StreamType getStreamType() {
return streamType;

View File

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

View File

@ -1,16 +1,18 @@
package org.schabi.newpipe.player.seekbarpreview;
import static org.schabi.newpipe.player.seekbarpreview.SeekbarPreviewThumbnailHelper.SeekbarPreviewThumbnailType;
import android.content.Context;
import android.graphics.Bitmap;
import android.util.Log;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.google.common.base.Stopwatch;
import com.nostra13.universalimageloader.core.ImageLoader;
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.HashMap;
@ -21,11 +23,8 @@ import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.function.Supplier;
import static org.schabi.newpipe.player.seekbarpreview.SeekbarPreviewThumbnailHelper.SeekbarPreviewThumbnailType;
public class SeekbarPreviewThumbnailHolder {
// 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) {
if (url == null) {
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;
try {
final SyncImageLoadingListener syncImageLoadingListener =
new SyncImageLoadingListener();
Log.d(TAG, "Downloading bitmap for seekbarPreview from '" + url + "'");
// Ensure that everything is running
ImageLoader.getInstance().resume();
// Load the image
// Impl-Note:
// Gets the bitmap within the timeout of 15 seconds imposed by default by OkHttpClient
// Ensure that your are not running on the main-Thread this will otherwise hang
ImageLoader.getInstance().loadImage(
url,
ImageDisplayConstants.DISPLAY_SEEKBAR_PREVIEW_OPTIONS,
syncImageLoadingListener);
// Get the bitmap within the timeout
final Bitmap bitmap =
syncImageLoadingListener.waitForBitmapOrThrow(30, TimeUnit.SECONDS);
final Bitmap bitmap = PicassoHelper.loadSeekbarThumbnailPreview(url).get();
if (sw != null) {
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.PreferenceManager;
import com.nostra13.universalimageloader.core.ImageLoader;
import org.schabi.newpipe.DownloaderImpl;
import org.schabi.newpipe.NewPipeDatabase;
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.streams.io.StoredFileHelper;
import org.schabi.newpipe.util.NavigationHelper;
import org.schabi.newpipe.util.PicassoHelper;
import org.schabi.newpipe.util.ZipHelper;
import java.io.File;
@ -50,7 +49,6 @@ public class ContentSettingsFragment extends BasePreferenceFragment {
private ContentSettingsManager manager;
private String importExportDataPathKey;
private String thumbnailLoadToggleKey;
private String youtubeRestrictedModeEnabledKey;
private Localization initialSelectedLocalization;
@ -69,7 +67,6 @@ public class ContentSettingsFragment extends BasePreferenceFragment {
manager.deleteSettingsFile();
importExportDataPathKey = getString(R.string.import_export_data_path);
thumbnailLoadToggleKey = getString(R.string.download_thumbnail_key);
youtubeRestrictedModeEnabledKey = getString(R.string.youtube_restricted_mode_enabled);
addPreferencesFromResource(R.xml.content_settings);
@ -77,7 +74,8 @@ public class ContentSettingsFragment extends BasePreferenceFragment {
final Preference importDataPreference = requirePreference(R.string.import_data);
importDataPreference.setOnPreferenceClickListener((Preference p) -> {
requestImportPathLauncher.launch(
StoredFileHelper.getPicker(requireContext(), getImportExportDataUri()));
StoredFileHelper.getPicker(requireContext(),
ZIP_MIME_TYPE, getImportExportDataUri()));
return true;
});
@ -95,8 +93,7 @@ public class ContentSettingsFragment extends BasePreferenceFragment {
.getPreferredLocalization(requireContext());
initialSelectedContentCountry = org.schabi.newpipe.util.Localization
.getPreferredContentCountry(requireContext());
initialLanguage = PreferenceManager
.getDefaultSharedPreferences(requireContext()).getString("app_language_key", "en");
initialLanguage = defaultPreferences.getString(getString(R.string.app_language_key), "en");
final Preference clearCookiePref = requirePreference(R.string.clear_cookie_key);
clearCookiePref.setOnPreferenceClickListener(preference -> {
@ -112,20 +109,24 @@ public class ContentSettingsFragment extends BasePreferenceFragment {
if (defaultPreferences.getString(getString(R.string.recaptcha_cookies_key), "").isEmpty()) {
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
public boolean onPreferenceTreeClick(final Preference preference) {
if (preference.getKey().equals(thumbnailLoadToggleKey)) {
final ImageLoader imageLoader = ImageLoader.getInstance();
imageLoader.stop();
imageLoader.clearDiskCache();
imageLoader.clearMemoryCache();
imageLoader.resume();
Toast.makeText(preference.getContext(), R.string.thumbnail_cache_wipe_complete_notice,
Toast.LENGTH_SHORT).show();
}
if (preference.getKey().equals(youtubeRestrictedModeEnabledKey)) {
final Context context = getContext();
if (context != null) {
@ -146,8 +147,8 @@ public class ContentSettingsFragment extends BasePreferenceFragment {
.getPreferredLocalization(requireContext());
final ContentCountry selectedContentCountry = org.schabi.newpipe.util.Localization
.getPreferredContentCountry(requireContext());
final String selectedLanguage = PreferenceManager
.getDefaultSharedPreferences(requireContext()).getString("app_language_key", "en");
final String selectedLanguage =
defaultPreferences.getString(getString(R.string.app_language_key), "en");
if (!selectedLocalization.equals(initialSelectedLocalization)
|| !selectedContentCountry.equals(initialSelectedContentCountry)
@ -183,7 +184,7 @@ public class ContentSettingsFragment extends BasePreferenceFragment {
new AlertDialog.Builder(requireActivity())
.setMessage(R.string.override_current_data)
.setPositiveButton(R.string.finish, (d, id) ->
.setPositiveButton(R.string.ok, (d, id) ->
importDatabase(file, lastImportDataUri))
.setNegativeButton(R.string.cancel, (d, id) ->
d.cancel())
@ -231,11 +232,11 @@ public class ContentSettingsFragment extends BasePreferenceFragment {
final AlertDialog.Builder alert = new AlertDialog.Builder(requireContext());
alert.setTitle(R.string.import_settings);
alert.setNegativeButton(android.R.string.no, (dialog, which) -> {
alert.setNegativeButton(R.string.cancel, (dialog, which) -> {
dialog.dismiss();
finishImport(importDataUri);
});
alert.setPositiveButton(getString(R.string.finish), (dialog, which) -> {
alert.setPositiveButton(R.string.ok, (dialog, which) -> {
dialog.dismiss();
manager.loadSharedPreferences(PreferenceManager
.getDefaultSharedPreferences(requireContext()));

View File

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

View File

@ -6,6 +6,7 @@ import android.os.Build;
import android.os.Environment;
import androidx.annotation.NonNull;
import androidx.annotation.StringRes;
import androidx.preference.PreferenceManager;
import org.schabi.newpipe.R;
@ -59,6 +60,10 @@ public final class NewPipeSettings {
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.video_audio_settings, true);
PreferenceManager.setDefaultValues(context, R.xml.download_settings, true);
@ -71,8 +76,6 @@ public final class NewPipeSettings {
saveDefaultVideoDownloadDirectory(context);
saveDefaultAudioDownloadDirectory(context);
SettingMigrations.initMigrations(context, isFirstRun);
}
static void saveDefaultVideoDownloadDirectory(final Context context) {
@ -124,4 +127,29 @@ public final class NewPipeSettings {
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)
.setView(dialogBinding.getRoot())
.setNegativeButton(R.string.cancel, null)
.setPositiveButton(R.string.finish, (dialog1, which) -> {
.setPositiveButton(R.string.ok, (dialog1, which) -> {
final String url = dialogBinding.dialogEditText.getText().toString();
addInstance(url);
})

View File

@ -14,13 +14,11 @@ import androidx.fragment.app.DialogFragment;
import androidx.recyclerview.widget.LinearLayoutManager;
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.database.subscription.SubscriptionEntity;
import org.schabi.newpipe.error.ErrorActivity;
import org.schabi.newpipe.local.subscription.SubscriptionManager;
import org.schabi.newpipe.util.PicassoHelper;
import org.schabi.newpipe.util.ThemeHelper;
import java.util.List;
@ -54,13 +52,6 @@ import io.reactivex.rxjava3.schedulers.Schedulers;
*/
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 OnCancelListener onCancelListener = null;
@ -199,8 +190,7 @@ public class SelectChannelFragment extends DialogFragment {
final SubscriptionEntity entry = subscriptions.get(position);
holder.titleView.setText(entry.getName());
holder.view.setOnClickListener(view -> clickedItem(position));
imageLoader.displayImage(entry.getAvatarUrl(), holder.thumbnailView,
DISPLAY_IMAGE_OPTIONS);
PicassoHelper.loadAvatar(entry.getAvatarUrl()).into(holder.thumbnailView);
}
@Override

View File

@ -14,9 +14,6 @@ import androidx.fragment.app.DialogFragment;
import androidx.recyclerview.widget.LinearLayoutManager;
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.R;
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.local.playlist.LocalPlaylistManager;
import org.schabi.newpipe.local.playlist.RemotePlaylistManager;
import org.schabi.newpipe.util.PicassoHelper;
import java.util.List;
import java.util.Vector;
@ -38,13 +36,6 @@ import io.reactivex.rxjava3.core.Flowable;
import io.reactivex.rxjava3.disposables.Disposable;
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;
@ -170,16 +161,15 @@ public class SelectPlaylistFragment extends DialogFragment {
holder.titleView.setText(entry.name);
holder.view.setOnClickListener(view -> clickedItem(position));
imageLoader.displayImage(entry.thumbnailUrl, holder.thumbnailView,
DISPLAY_IMAGE_OPTIONS);
PicassoHelper.loadPlaylistThumbnail(entry.thumbnailUrl).into(holder.thumbnailView);
} else if (selectedItem instanceof PlaylistRemoteEntity) {
final PlaylistRemoteEntity entry = ((PlaylistRemoteEntity) selectedItem);
holder.titleView.setText(entry.getName());
holder.view.setOnClickListener(view -> clickedItem(position));
imageLoader.displayImage(entry.getThumbnailUrl(), holder.thumbnailView,
DISPLAY_IMAGE_OPTIONS);
PicassoHelper.loadPlaylistThumbnail(entry.getThumbnailUrl())
.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.util.DeviceUtils;
import java.util.Collections;
import java.util.HashSet;
import java.util.Set;
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 {
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;
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.
* <p>
@ -81,9 +118,15 @@ public final class SettingMigrations {
private static final Migration[] SETTING_MIGRATIONS = {
MIGRATION_0_1,
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) {
// 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);
}
public static Intent getPicker(@NonNull final Context ctx) {
public static Intent getPicker(@NonNull final Context ctx,
@NonNull final String mimeType) {
if (NewPipeSettings.useStorageAccessFramework(ctx)) {
return new Intent(Intent.ACTION_OPEN_DOCUMENT)
.putExtra("android.content.extra.SHOW_ADVANCED", true)
.setType("*/*")
.setType(mimeType)
.addCategory(Intent.CATEGORY_OPENABLE)
.addFlags(Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION
| StoredDirectoryHelper.PERMISSION_FLAGS);
@ -477,8 +478,10 @@ public class StoredFileHelper implements Serializable {
}
}
public static Intent getPicker(@NonNull final Context ctx, @Nullable final Uri initialPath) {
return applyInitialPathToPickerIntent(ctx, getPicker(ctx), initialPath, null);
public static Intent getPicker(@NonNull final Context ctx,
@NonNull final String mimeType,
@Nullable final Uri initialPath) {
return applyInitialPathToPickerIntent(ctx, getPicker(ctx, mimeType), initialPath, null);
}
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.NonNull;
import androidx.appcompat.app.AppCompatActivity;
import androidx.core.content.ContextCompat;
import androidx.preference.PreferenceManager;
@ -130,4 +131,13 @@ public final class DeviceUtils {
&& !HI3798MV200
&& !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));
}
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,
@StringRes final int zeroCaseStringId, final long count,
final String formattedCount) {

View File

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

View File

@ -119,7 +119,7 @@ public final class PermissionHelper {
public static boolean isPopupEnabled(final Context context) {
return Build.VERSION.SDK_INT < Build.VERSION_CODES.M
|| PermissionHelper.checkSystemAlertWindowPermission(context);
|| checkSystemAlertWindowPermission(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
import android.os.Parcelable
import kotlinx.android.parcel.Parcelize
import kotlinx.parcelize.Parcelize
/**
* 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.net.Uri;
import android.widget.Toast;
import androidx.fragment.app.Fragment;
import org.schabi.newpipe.NewPipeDatabase;
import org.schabi.newpipe.R;
import org.schabi.newpipe.extractor.stream.StreamInfoItem;
import org.schabi.newpipe.local.dialog.PlaylistAppendDialog;
@ -20,7 +22,9 @@ import java.util.Collections;
import java.util.List;
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.POPUP;
@ -29,12 +33,30 @@ public enum StreamDialogEntry {
// enum values with DEFAULT actions //
//////////////////////////////////////
show_channel_details(R.string.show_channel_details, (fragment, item) ->
// For some reason `getParentFragmentManager()` doesn't work, but this does.
NavigationHelper.openChannelFragment(
fragment.requireActivity().getSupportFragmentManager(),
item.getServiceId(), item.getUploaderUrl(), item.getUploaderName())
),
show_channel_details(R.string.show_channel_details, (fragment, item) -> {
if (isNullOrEmpty(item.getUploaderUrl())) {
final int serviceId = item.getServiceId();
final String url = item.getUrl();
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>
@ -179,4 +201,17 @@ public enum StreamDialogEntry {
public interface StreamDialogEntryAction {
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;
import android.content.Context;
import android.util.Log;
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.StreamingService;
import org.schabi.newpipe.extractor.exceptions.ExtractionException;
@ -24,6 +29,9 @@ import io.reactivex.rxjava3.disposables.CompositeDisposable;
import io.reactivex.rxjava3.schedulers.Schedulers;
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 HASHTAG_TIMESTAMP_PATTERN =
Pattern.compile("(.*)#timestamp=(\\d+)");
@ -93,7 +101,12 @@ public final class InternalUrlsHandler {
return false;
}
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.LinkType linkType;
@ -146,8 +159,18 @@ public final class InternalUrlsHandler {
.observeOn(AndroidSchedulers.mainThread())
.subscribe(info -> {
final PlayQueue playQueue
= new SinglePlayQueue(info, seconds * 1000);
= new SinglePlayQueue(info, seconds * 1000L);
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;
}

View File

@ -248,6 +248,7 @@ public final class ShareUtils {
shareIntent.putExtra(Intent.EXTRA_TEXT, content);
if (!title.isEmpty()) {
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

View File

@ -32,9 +32,8 @@ import static org.schabi.newpipe.util.external_communication.InternalUrlsHandler
public final class TextLinkifier {
public static final String TAG = TextLinkifier.class.getSimpleName();
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() {
}
@ -174,33 +173,34 @@ public final class TextLinkifier {
final Info relatedInfo,
final CompositeDisposable disposables) {
final String descriptionText = spannableDescription.toString();
final Matcher timestampsMatches = TIMESTAMPS_PATTERN.matcher(descriptionText);
final Matcher timestampsMatches =
TimestampExtractor.TIMESTAMPS_PATTERN.matcher(descriptionText);
while (timestampsMatches.find()) {
final int timestampStart = timestampsMatches.start(2);
final int timestampEnd = timestampsMatches.end(3);
final String parsedTimestamp = descriptionText.substring(timestampStart, timestampEnd);
final String[] timestampParts = parsedTimestamp.split(":");
final TimestampExtractor.TimestampMatchDTO timestampMatchDTO =
TimestampExtractor.getTimestampFromMatcher(
timestampsMatches,
descriptionText);
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 {
if (timestampMatchDTO == null) {
continue;
}
spannableDescription.setSpan(new ClickableSpan() {
@Override
public void onClick(@NonNull final View view) {
playOnPopup(context, relatedInfo.getUrl(), relatedInfo.getService(), seconds,
disposables);
}
}, timestampStart, timestampEnd, 0);
spannableDescription.setSpan(
new ClickableSpan() {
@Override
public void onClick(@NonNull final View view) {
playOnPopup(
context,
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
import android.os.Parcelable
import kotlinx.android.parcel.Parcelize
import kotlinx.parcelize.Parcelize
import org.schabi.newpipe.extractor.MediaFormat
import org.schabi.newpipe.extractor.stream.AudioStream
import org.schabi.newpipe.extractor.stream.Stream

View File

@ -49,6 +49,8 @@ import us.shandian.giga.get.DownloadMission;
import us.shandian.giga.get.MissionRecoveryInfo;
import org.schabi.newpipe.streams.io.StoredDirectoryHelper;
import org.schabi.newpipe.streams.io.StoredFileHelper;
import org.schabi.newpipe.util.Localization;
import us.shandian.giga.postprocessing.Postprocessing;
import us.shandian.giga.service.DownloadManager.NetworkState;
@ -467,7 +469,8 @@ public class DownloadManagerService extends Service {
.setContentIntent(makePendingIntent(ACTION_OPEN_DOWNLOADS_FINISHED));
}
if (downloadDoneCount < 1) {
downloadDoneCount++;
if (downloadDoneCount == 1) {
downloadDoneList.append(name);
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
@ -476,9 +479,9 @@ public class DownloadManagerService extends Service {
downloadDoneNotification.setContentTitle(null);
}
downloadDoneNotification.setContentText(getString(R.string.download_finished));
downloadDoneNotification.setContentText(Localization.downloadCount(this, downloadDoneCount));
downloadDoneNotification.setStyle(new NotificationCompat.BigTextStyle()
.setBigContentTitle(getString(R.string.download_finished))
.setBigContentTitle(Localization.downloadCount(this, downloadDoneCount))
.bigText(name)
);
} else {
@ -486,12 +489,11 @@ public class DownloadManagerService extends Service {
downloadDoneList.append(name);
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);
}
mNotificationManager.notify(DOWNLOADS_NOTIFICATION_ID, downloadDoneNotification.build());
downloadDoneCount++;
}
public void notifyFailedDownload(DownloadMission mission) {

View File

@ -43,6 +43,7 @@ import org.schabi.newpipe.extractor.NewPipe;
import org.schabi.newpipe.error.ErrorActivity;
import org.schabi.newpipe.error.ErrorInfo;
import org.schabi.newpipe.error.UserAction;
import org.schabi.newpipe.util.Localization;
import org.schabi.newpipe.util.NavigationHelper;
import org.schabi.newpipe.util.external_communication.ShareUtils;
@ -554,7 +555,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())
.create()
.show();
@ -596,7 +597,7 @@ public class MissionAdapter extends Adapter<ViewHolder> implements Handler.Callb
}
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.setAction(R.string.undo, s -> {
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

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