diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index ad9f1f82f..de16c5cf0 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -57,7 +57,7 @@ Oh no, a bug! It happens. Thanks for reporting an issue with NewPipe. To make it - + ### Device info diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f4412e099..a876df8fd 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -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 diff --git a/.github/workflows/no-response.yml b/.github/workflows/no-response.yml new file mode 100644 index 000000000..54e749dc0 --- /dev/null +++ b/.github/workflows/no-response.yml @@ -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 diff --git a/app/build.gradle b/app/build.gradle index 38423ec2d..9bdb4e71e 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -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" diff --git a/app/schemas/org.schabi.newpipe.database.AppDatabase/4.json b/app/schemas/org.schabi.newpipe.database.AppDatabase/4.json new file mode 100644 index 000000000..b555260d5 --- /dev/null +++ b/app/schemas/org.schabi.newpipe.database.AppDatabase/4.json @@ -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')" + ] + } +} \ No newline at end of file diff --git a/app/src/androidTest/java/org/schabi/newpipe/database/AppDatabaseTest.kt b/app/src/androidTest/java/org/schabi/newpipe/database/AppDatabaseTest.kt deleted file mode 100644 index 239c46f7a..000000000 --- a/app/src/androidTest/java/org/schabi/newpipe/database/AppDatabaseTest.kt +++ /dev/null @@ -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 - } -} diff --git a/app/src/androidTest/java/org/schabi/newpipe/local/playlist/LocalPlaylistManagerTest.kt b/app/src/androidTest/java/org/schabi/newpipe/local/playlist/LocalPlaylistManagerTest.kt index 211312bc0..24563d1c1 100644 --- a/app/src/androidTest/java/org/schabi/newpipe/local/playlist/LocalPlaylistManagerTest.kt +++ b/app/src/androidTest/java/org/schabi/newpipe/local/playlist/LocalPlaylistManagerTest.kt @@ -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)) diff --git a/app/src/debug/java/org/schabi/newpipe/settings/DebugSettingsFragment.java b/app/src/debug/java/org/schabi/newpipe/settings/DebugSettingsFragment.java index d5d223ff2..55b2c7708 100644 --- a/app/src/debug/java/org/schabi/newpipe/settings/DebugSettingsFragment.java +++ b/app/src/debug/java/org/schabi/newpipe/settings/DebugSettingsFragment.java @@ -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(); }); diff --git a/app/src/main/java/org/schabi/newpipe/App.java b/app/src/main/java/org/schabi/newpipe/App.java index ddbac4422..766ebe834 100644 --- a/app/src/main/java/org/schabi/newpipe/App.java +++ b/app/src/main/java/org/schabi/newpipe/App.java @@ -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. diff --git a/app/src/main/java/org/schabi/newpipe/BaseFragment.java b/app/src/main/java/org/schabi/newpipe/BaseFragment.java index 0d2412778..0be427648 100644 --- a/app/src/main/java/org/schabi/newpipe/BaseFragment.java +++ b/app/src/main/java/org/schabi/newpipe/BaseFragment.java @@ -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 diff --git a/app/src/main/java/org/schabi/newpipe/CheckForNewAppVersion.java b/app/src/main/java/org/schabi/newpipe/CheckForNewAppVersion.java index 37ca0e400..c8fa02186 100644 --- a/app/src/main/java/org/schabi/newpipe/CheckForNewAppVersion.java +++ b/app/src/main/java/org/schabi/newpipe/CheckForNewAppVersion.java @@ -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 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); diff --git a/app/src/main/java/org/schabi/newpipe/DownloaderImpl.java b/app/src/main/java/org/schabi/newpipe/DownloaderImpl.java index 90bb0e825..fde991655 100644 --- a/app/src/main/java/org/schabi/newpipe/DownloaderImpl.java +++ b/app/src/main/java/org/schabi/newpipe/DownloaderImpl.java @@ -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 { diff --git a/app/src/main/java/org/schabi/newpipe/ImageDownloader.java b/app/src/main/java/org/schabi/newpipe/ImageDownloader.java deleted file mode 100644 index ceae11777..000000000 --- a/app/src/main/java/org/schabi/newpipe/ImageDownloader.java +++ /dev/null @@ -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); - } -} diff --git a/app/src/main/java/org/schabi/newpipe/NewPipeDatabase.java b/app/src/main/java/org/schabi/newpipe/NewPipeDatabase.java index 8f7732218..36bd6ee0d 100644 --- a/app/src/main/java/org/schabi/newpipe/NewPipeDatabase.java +++ b/app/src/main/java/org/schabi/newpipe/NewPipeDatabase.java @@ -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(); } diff --git a/app/src/main/java/org/schabi/newpipe/QueueItemMenuUtil.java b/app/src/main/java/org/schabi/newpipe/QueueItemMenuUtil.java new file mode 100644 index 000000000..9105ff992 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/QueueItemMenuUtil.java @@ -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() { } +} diff --git a/app/src/main/java/org/schabi/newpipe/about/AboutActivity.kt b/app/src/main/java/org/schabi/newpipe/about/AboutActivity.kt index 0199f30d8..a8fdcae26 100644 --- a/app/src/main/java/org/schabi/newpipe/about/AboutActivity.kt +++ b/app/src/main/java/org/schabi/newpipe/about/AboutActivity.kt @@ -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 diff --git a/app/src/main/java/org/schabi/newpipe/about/License.kt b/app/src/main/java/org/schabi/newpipe/about/License.kt index b2c5d44a2..117ff9bf5 100644 --- a/app/src/main/java/org/schabi/newpipe/about/License.kt +++ b/app/src/main/java/org/schabi/newpipe/about/License.kt @@ -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 /** diff --git a/app/src/main/java/org/schabi/newpipe/about/LicenseFragmentHelper.kt b/app/src/main/java/org/schabi/newpipe/about/LicenseFragmentHelper.kt index 7617ef451..a04de8abc 100644 --- a/app/src/main/java/org/schabi/newpipe/about/LicenseFragmentHelper.kt +++ b/app/src/main/java/org/schabi/newpipe/about/LicenseFragmentHelper.kt @@ -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() } diff --git a/app/src/main/java/org/schabi/newpipe/about/SoftwareComponent.kt b/app/src/main/java/org/schabi/newpipe/about/SoftwareComponent.kt index 2e967ebe8..354e8fef7 100644 --- a/app/src/main/java/org/schabi/newpipe/about/SoftwareComponent.kt +++ b/app/src/main/java/org/schabi/newpipe/about/SoftwareComponent.kt @@ -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 diff --git a/app/src/main/java/org/schabi/newpipe/database/AppDatabase.java b/app/src/main/java/org/schabi/newpipe/database/AppDatabase.java index 3b5bda155..cf52d9453 100644 --- a/app/src/main/java/org/schabi/newpipe/database/AppDatabase.java +++ b/app/src/main/java/org/schabi/newpipe/database/AppDatabase.java @@ -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"; diff --git a/app/src/main/java/org/schabi/newpipe/database/Migrations.java b/app/src/main/java/org/schabi/newpipe/database/Migrations.java index f5195ba8d..fdd38a824 100644 --- a/app/src/main/java/org/schabi/newpipe/database/Migrations.java +++ b/app/src/main/java/org/schabi/newpipe/database/Migrations.java @@ -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() { } } diff --git a/app/src/main/java/org/schabi/newpipe/database/playlist/PlaylistStreamEntry.kt b/app/src/main/java/org/schabi/newpipe/database/playlist/PlaylistStreamEntry.kt index 81409ecf0..d2543ae6d 100644 --- a/app/src/main/java/org/schabi/newpipe/database/playlist/PlaylistStreamEntry.kt +++ b/app/src/main/java/org/schabi/newpipe/database/playlist/PlaylistStreamEntry.kt @@ -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 diff --git a/app/src/main/java/org/schabi/newpipe/database/stream/StreamStatisticsEntry.kt b/app/src/main/java/org/schabi/newpipe/database/stream/StreamStatisticsEntry.kt index 9a622f643..dc0db59d8 100644 --- a/app/src/main/java/org/schabi/newpipe/database/stream/StreamStatisticsEntry.kt +++ b/app/src/main/java/org/schabi/newpipe/database/stream/StreamStatisticsEntry.kt @@ -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 diff --git a/app/src/main/java/org/schabi/newpipe/database/stream/dao/StreamDAO.kt b/app/src/main/java/org/schabi/newpipe/database/stream/dao/StreamDAO.kt index be8590382..7dc16e784 100644 --- a/app/src/main/java/org/schabi/newpipe/database/stream/dao/StreamDAO.kt +++ b/app/src/main/java/org/schabi/newpipe/database/stream/dao/StreamDAO.kt @@ -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 { @Query("SELECT * FROM streams WHERE url = :url AND service_id = :serviceId") abstract fun getStream(serviceId: Long, url: String): Flowable> + @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 diff --git a/app/src/main/java/org/schabi/newpipe/database/stream/model/StreamEntity.kt b/app/src/main/java/org/schabi/newpipe/database/stream/model/StreamEntity.kt index 0f3e18e7b..c56f91949 100644 --- a/app/src/main/java/org/schabi/newpipe/database/stream/model/StreamEntity.kt +++ b/app/src/main/java/org/schabi/newpipe/database/stream/model/StreamEntity.kt @@ -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" diff --git a/app/src/main/java/org/schabi/newpipe/download/DownloadDialog.java b/app/src/main/java/org/schabi/newpipe/download/DownloadDialog.java index 628496c01..a7f5b938f 100644 --- a/app/src/main/java/org/schabi/newpipe/download/DownloadDialog.java +++ b/app/src/main/java/org/schabi/newpipe/download/DownloadDialog.java @@ -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; diff --git a/app/src/main/java/org/schabi/newpipe/error/ErrorInfo.kt b/app/src/main/java/org/schabi/newpipe/error/ErrorInfo.kt index 487e7c7fb..6581b5752 100644 --- a/app/src/main/java/org/schabi/newpipe/error/ErrorInfo.kt +++ b/app/src/main/java/org/schabi/newpipe/error/ErrorInfo.kt @@ -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 diff --git a/app/src/main/java/org/schabi/newpipe/error/ErrorPanelHelper.kt b/app/src/main/java/org/schabi/newpipe/error/ErrorPanelHelper.kt index e790c5fc5..228c17f8c 100644 --- a/app/src/main/java/org/schabi/newpipe/error/ErrorPanelHelper.kt +++ b/app/src/main/java/org/schabi/newpipe/error/ErrorPanelHelper.kt @@ -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 + } + } + } + } } } diff --git a/app/src/main/java/org/schabi/newpipe/error/ReCaptchaActivity.java b/app/src/main/java/org/schabi/newpipe/error/ReCaptchaActivity.java index cd6a882ae..555dd709b 100644 --- a/app/src/main/java/org/schabi/newpipe/error/ReCaptchaActivity.java +++ b/app/src/main/java/org/schabi/newpipe/error/ReCaptchaActivity.java @@ -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); diff --git a/app/src/main/java/org/schabi/newpipe/fragments/detail/VideoDetailFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/detail/VideoDetailFragment.java index 14d023dc7..324ee37a8 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/detail/VideoDetailFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/detail/VideoDetailFragment.java @@ -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) { diff --git a/app/src/main/java/org/schabi/newpipe/fragments/list/channel/ChannelFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/list/channel/ChannelFragment.java index bc6718021..548ae7b2c 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/list/channel/ChannelFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/list/channel/ChannelFragment.java @@ -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 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 @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 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) { diff --git a/app/src/main/java/org/schabi/newpipe/fragments/list/playlist/PlaylistFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/list/playlist/PlaylistFragment.java index 824aa2612..513fbbc91 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/list/playlist/PlaylistFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/list/playlist/PlaylistFragment.java @@ -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 { + + 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 { 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 { 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 diff --git a/app/src/main/java/org/schabi/newpipe/fragments/list/search/SearchFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/list/search/SearchFragment.java index 478cf94f3..7de212383 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/list/search/SearchFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/list/search/SearchFragment.java @@ -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 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> getLocalSuggestionsObservable( + final String query, final int similarQueryLimit) { + return historyRecordManager + .getRelatedSearches(query, similarQueryLimit, 25) + .toObservable() + .map(searchHistoryEntries -> { + final Set result = new HashSet<>(); // remove duplicates + for (final SearchHistoryEntry entry : searchHistoryEntries) { + result.add(new SuggestionItem(true, entry.getSearch())); + } + return new ArrayList<>(result); + }); + } + + private Observable> getRemoteSuggestionsObservable(final String query) { + return ExtractorHelper + .suggestionsFor(serviceId, query) + .toObservable() + .map(strings -> { + final List 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 isSuggestionsEnabled) + .startWithItem(searchString == null ? "" : searchString) .switchMap(query -> { - final Flowable> flowable = historyRecordManager - .getRelatedSearches(query, 3, 25); - final Observable> local = flowable.toObservable() - .map(searchHistoryEntries -> { - final List 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::emptyList) + .toObservable() + .materialize(); } - - final Observable> 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 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 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 diff --git a/app/src/main/java/org/schabi/newpipe/fragments/list/search/SuggestionItem.java b/app/src/main/java/org/schabi/newpipe/fragments/list/search/SuggestionItem.java index 5aa927ed3..83f68dbb5 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/list/search/SuggestionItem.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/list/search/SuggestionItem.java @@ -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 + "]"; diff --git a/app/src/main/java/org/schabi/newpipe/fragments/list/search/SuggestionListAdapter.java b/app/src/main/java/org/schabi/newpipe/fragments/list/search/SuggestionListAdapter.java index 952316796..3cfcfd470 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/list/search/SuggestionListAdapter.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/list/search/SuggestionListAdapter.java @@ -19,7 +19,6 @@ public class SuggestionListAdapter private final ArrayList 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 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) diff --git a/app/src/main/java/org/schabi/newpipe/info_list/InfoItemBuilder.java b/app/src/main/java/org/schabi/newpipe/info_list/InfoItemBuilder.java index ac7a3f499..d78bf1076 100644 --- a/app/src/main/java/org/schabi/newpipe/info_list/InfoItemBuilder.java +++ b/app/src/main/java/org/schabi/newpipe/info_list/InfoItemBuilder.java @@ -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 onStreamSelectedListener; private OnClickGesture onChannelSelectedListener; @@ -101,10 +98,6 @@ public class InfoItemBuilder { return context; } - public ImageLoader getImageLoader() { - return imageLoader; - } - public OnClickGesture getOnStreamSelectedListener() { return onStreamSelectedListener; } diff --git a/app/src/main/java/org/schabi/newpipe/info_list/StreamSegmentItem.kt b/app/src/main/java/org/schabi/newpipe/info_list/StreamSegmentItem.kt index f6d3587d6..f233c7627 100644 --- a/app/src/main/java/org/schabi/newpipe/info_list/StreamSegmentItem.kt +++ b/app/src/main/java/org/schabi/newpipe/info_list/StreamSegmentItem.kt @@ -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(R.id.previewImage), - ImageDisplayConstants.DISPLAY_THUMBNAIL_OPTIONS - ) + PicassoHelper.loadThumbnail(it) + .into(viewHolder.root.findViewById(R.id.previewImage)) } viewHolder.root.findViewById(R.id.textViewTitle).text = item.title if (item.channelName == null) { diff --git a/app/src/main/java/org/schabi/newpipe/info_list/holder/ChannelMiniInfoItemHolder.java b/app/src/main/java/org/schabi/newpipe/info_list/holder/ChannelMiniInfoItemHolder.java index 9d93abcd9..78acb752b 100644 --- a/app/src/main/java/org/schabi/newpipe/info_list/holder/ChannelMiniInfoItemHolder.java +++ b/app/src/main/java/org/schabi/newpipe/info_list/holder/ChannelMiniInfoItemHolder.java @@ -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) { diff --git a/app/src/main/java/org/schabi/newpipe/info_list/holder/CommentsMiniInfoItemHolder.java b/app/src/main/java/org/schabi/newpipe/info_list/holder/CommentsMiniInfoItemHolder.java index 629240dc6..079efa4a8 100644 --- a/app/src/main/java/org/schabi/newpipe/info_list/holder/CommentsMiniInfoItemHolder.java +++ b/app/src/main/java/org/schabi/newpipe/info_list/holder/CommentsMiniInfoItemHolder.java @@ -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); } } diff --git a/app/src/main/java/org/schabi/newpipe/info_list/holder/PlaylistMiniInfoItemHolder.java b/app/src/main/java/org/schabi/newpipe/info_list/holder/PlaylistMiniInfoItemHolder.java index d4af63062..bf5f57db3 100644 --- a/app/src/main/java/org/schabi/newpipe/info_list/holder/PlaylistMiniInfoItemHolder.java +++ b/app/src/main/java/org/schabi/newpipe/info_list/holder/PlaylistMiniInfoItemHolder.java @@ -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) { diff --git a/app/src/main/java/org/schabi/newpipe/info_list/holder/StreamMiniInfoItemHolder.java b/app/src/main/java/org/schabi/newpipe/info_list/holder/StreamMiniInfoItemHolder.java index 98699eb95..79772a6a3 100644 --- a/app/src/main/java/org/schabi/newpipe/info_list/holder/StreamMiniInfoItemHolder.java +++ b/app/src/main/java/org/schabi/newpipe/info_list/holder/StreamMiniInfoItemHolder.java @@ -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) { diff --git a/app/src/main/java/org/schabi/newpipe/local/LocalItemBuilder.java b/app/src/main/java/org/schabi/newpipe/local/LocalItemBuilder.java index d7aaddcc4..041d16d43 100644 --- a/app/src/main/java/org/schabi/newpipe/local/LocalItemBuilder.java +++ b/app/src/main/java/org/schabi/newpipe/local/LocalItemBuilder.java @@ -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 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 getOnItemSelectedListener() { return onSelectedListener; } diff --git a/app/src/main/java/org/schabi/newpipe/local/feed/FeedFragment.kt b/app/src/main/java/org/schabi/newpipe/local/feed/FeedFragment.kt index d3f6afeb6..42fb8915d 100644 --- a/app/src/main/java/org/schabi/newpipe/local/feed/FeedFragment.kt +++ b/app/src/main/java/org/schabi/newpipe/local/feed/FeedFragment.kt @@ -206,7 +206,7 @@ class FeedFragment : BaseStateFragment() { 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() { StreamDialogEntry.mark_as_watched ) } + entries.add(StreamDialogEntry.show_channel_details) StreamDialogEntry.setEnabledEntries(entries) InfoItemDialog(activity, item, StreamDialogEntry.getCommands(context)) { _, which -> diff --git a/app/src/main/java/org/schabi/newpipe/local/feed/item/StreamItem.kt b/app/src/main/java/org/schabi/newpipe/local/feed/item/StreamItem.kt index 13ba7592b..c454f7eec 100644 --- a/app/src/main/java/org/schabi/newpipe/local/feed/item/StreamItem.kt +++ b/app/src/main/java/org/schabi/newpipe/local/feed/item/StreamItem.kt @@ -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 = diff --git a/app/src/main/java/org/schabi/newpipe/local/feed/service/FeedLoadService.kt b/app/src/main/java/org/schabi/newpipe/local/feed/service/FeedLoadService.kt index 3638b4c0e..98ff5914d 100644 --- a/app/src/main/java/org/schabi/newpipe/local/feed/service/FeedLoadService.kt +++ b/app/src/main/java/org/schabi/newpipe/local/feed/service/FeedLoadService.kt @@ -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) diff --git a/app/src/main/java/org/schabi/newpipe/local/history/StatisticsPlaylistFragment.java b/app/src/main/java/org/schabi/newpipe/local/history/StatisticsPlaylistFragment.java index 166b9c04e..6a7a300ca 100644 --- a/app/src/main/java/org/schabi/newpipe/local/history/StatisticsPlaylistFragment.java +++ b/app/src/main/java/org/schabi/newpipe/local/history/StatisticsPlaylistFragment.java @@ -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, 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); diff --git a/app/src/main/java/org/schabi/newpipe/local/holder/LocalPlaylistItemHolder.java b/app/src/main/java/org/schabi/newpipe/local/holder/LocalPlaylistItemHolder.java index 5560df3e0..f8c5176ec 100644 --- a/app/src/main/java/org/schabi/newpipe/local/holder/LocalPlaylistItemHolder.java +++ b/app/src/main/java/org/schabi/newpipe/local/holder/LocalPlaylistItemHolder.java @@ -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); } diff --git a/app/src/main/java/org/schabi/newpipe/local/holder/LocalPlaylistStreamItemHolder.java b/app/src/main/java/org/schabi/newpipe/local/holder/LocalPlaylistStreamItemHolder.java index 903f10440..561cde560 100644 --- a/app/src/main/java/org/schabi/newpipe/local/holder/LocalPlaylistStreamItemHolder.java +++ b/app/src/main/java/org/schabi/newpipe/local/holder/LocalPlaylistStreamItemHolder.java @@ -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) { diff --git a/app/src/main/java/org/schabi/newpipe/local/holder/LocalStatisticStreamItemHolder.java b/app/src/main/java/org/schabi/newpipe/local/holder/LocalStatisticStreamItemHolder.java index adf6bd5c2..d2fe8b40f 100644 --- a/app/src/main/java/org/schabi/newpipe/local/holder/LocalStatisticStreamItemHolder.java +++ b/app/src/main/java/org/schabi/newpipe/local/holder/LocalStatisticStreamItemHolder.java @@ -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) { diff --git a/app/src/main/java/org/schabi/newpipe/local/holder/RemotePlaylistItemHolder.java b/app/src/main/java/org/schabi/newpipe/local/holder/RemotePlaylistItemHolder.java index a39e3cecb..440353ac7 100644 --- a/app/src/main/java/org/schabi/newpipe/local/holder/RemotePlaylistItemHolder.java +++ b/app/src/main/java/org/schabi/newpipe/local/holder/RemotePlaylistItemHolder.java @@ -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); } diff --git a/app/src/main/java/org/schabi/newpipe/local/playlist/LocalPlaylistFragment.java b/app/src/main/java/org/schabi/newpipe/local/playlist/LocalPlaylistFragment.java index 788a4d062..40a7b26e2 100644 --- a/app/src/main/java/org/schabi/newpipe/local/playlist/LocalPlaylistFragment.java +++ b/app/src/main/java/org/schabi/newpipe/local/playlist/LocalPlaylistFragment.java @@ -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 { + .setPositiveButton(R.string.ok, (dialogInterface, i) -> { if (resultServiceIntent != null && getContext() != null) { getContext().startService(resultServiceIntent); } diff --git a/app/src/main/java/org/schabi/newpipe/local/subscription/SubscriptionFragment.kt b/app/src/main/java/org/schabi/newpipe/local/subscription/SubscriptionFragment.kt index e2248e742..57e1effbe 100644 --- a/app/src/main/java/org/schabi/newpipe/local/subscription/SubscriptionFragment.kt +++ b/app/src/main/java/org/schabi/newpipe/local/subscription/SubscriptionFragment.kt @@ -179,7 +179,7 @@ class SubscriptionFragment : BaseStateFragment() { } 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() { 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() { 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() { } } - 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() { super.hideLoading() binding.itemsList.animate(true, 200) } + + companion object { + const val JSON_MIME_TYPE = "application/json" + } } diff --git a/app/src/main/java/org/schabi/newpipe/local/subscription/SubscriptionsImportFragment.java b/app/src/main/java/org/schabi/newpipe/local/subscription/SubscriptionsImportFragment.java index 4e667f2b9..c4d088e39 100644 --- a/app/src/main/java/org/schabi/newpipe/local/subscription/SubscriptionsImportFragment.java +++ b/app/src/main/java/org/schabi/newpipe/local/subscription/SubscriptionsImportFragment.java @@ -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) { diff --git a/app/src/main/java/org/schabi/newpipe/local/subscription/dialog/FeedGroupDialog.kt b/app/src/main/java/org/schabi/newpipe/local/subscription/dialog/FeedGroupDialog.kt index cb0c5fe35..69d4c8819 100644 --- a/app/src/main/java/org/schabi/newpipe/local/subscription/dialog/FeedGroupDialog.kt +++ b/app/src/main/java/org/schabi/newpipe/local/subscription/dialog/FeedGroupDialog.kt @@ -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().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 } ) diff --git a/app/src/main/java/org/schabi/newpipe/local/subscription/item/ChannelItem.kt b/app/src/main/java/org/schabi/newpipe/local/subscription/item/ChannelItem.kt index a87ffb695..2b964779c 100644 --- a/app/src/main/java/org/schabi/newpipe/local/subscription/item/ChannelItem.kt +++ b/app/src/main/java/org/schabi/newpipe/local/subscription/item/ChannelItem.kt @@ -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) } diff --git a/app/src/main/java/org/schabi/newpipe/local/subscription/item/HeaderItem.kt b/app/src/main/java/org/schabi/newpipe/local/subscription/item/HeaderItem.kt deleted file mode 100644 index 71c1e6116..000000000 --- a/app/src/main/java/org/schabi/newpipe/local/subscription/item/HeaderItem.kt +++ /dev/null @@ -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() { - 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) -} diff --git a/app/src/main/java/org/schabi/newpipe/local/subscription/item/PickerSubscriptionItem.kt b/app/src/main/java/org/schabi/newpipe/local/subscription/item/PickerSubscriptionItem.kt index d4d4e7db1..aadb2fc73 100644 --- a/app/src/main/java/org/schabi/newpipe/local/subscription/item/PickerSubscriptionItem.kt +++ b/app/src/main/java/org/schabi/newpipe/local/subscription/item/PickerSubscriptionItem.kt @@ -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 } diff --git a/app/src/main/java/org/schabi/newpipe/local/subscription/services/SubscriptionsImportService.java b/app/src/main/java/org/schabi/newpipe/local/subscription/services/SubscriptionsImportService.java index a843ad77c..af598b106 100644 --- a/app/src/main/java/org/schabi/newpipe/local/subscription/services/SubscriptionsImportService.java +++ b/app/src/main/java/org/schabi/newpipe/local/subscription/services/SubscriptionsImportService.java @@ -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> importFromInputStream() { + Objects.requireNonNull(inputStream); + Objects.requireNonNull(inputStreamType); + return Flowable.fromCallable(() -> NewPipe.getService(currentServiceId) .getSubscriptionExtractor() - .fromInputStream(inputStream)); + .fromInputStream(inputStream, inputStreamType)); } private Flowable> importFromPreviousExport() { diff --git a/app/src/main/java/org/schabi/newpipe/player/MainPlayer.java b/app/src/main/java/org/schabi/newpipe/player/MainPlayer.java index 7a04ec22e..a9b9f4c87 100644 --- a/app/src/main/java/org/schabi/newpipe/player/MainPlayer.java +++ b/app/src/main/java/org/schabi/newpipe/player/MainPlayer.java @@ -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 diff --git a/app/src/main/java/org/schabi/newpipe/player/PlayQueueActivity.java b/app/src/main/java/org/schabi/newpipe/player/PlayQueueActivity.java index ce7b82de4..0976aa4fb 100644 --- a/app/src/main/java/org/schabi/newpipe/player/PlayQueueActivity.java +++ b/app/src/main/java/org/schabi/newpipe/player/PlayQueueActivity.java @@ -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); } } diff --git a/app/src/main/java/org/schabi/newpipe/player/Player.java b/app/src/main/java/org/schabi/newpipe/player/Player.java index 935dcca35..c10307b98 100644 --- a/app/src/main/java/org/schabi/newpipe/player/Player.java +++ b/app/src/main/java/org/schabi/newpipe/player/Player.java @@ -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}. *

There are multiple types of errors:

@@ -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 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 } diff --git a/app/src/main/java/org/schabi/newpipe/player/helper/AudioReactor.java b/app/src/main/java/org/schabi/newpipe/player/helper/AudioReactor.java index 45b593328..2e2fda86c 100644 --- a/app/src/main/java/org/schabi/newpipe/player/helper/AudioReactor.java +++ b/app/src/main/java/org/schabi/newpipe/player/helper/AudioReactor.java @@ -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); diff --git a/app/src/main/java/org/schabi/newpipe/player/helper/LoadController.java b/app/src/main/java/org/schabi/newpipe/player/helper/LoadController.java index e4ae27750..71cfcc818 100644 --- a/app/src/main/java/org/schabi/newpipe/player/helper/LoadController.java +++ b/app/src/main/java/org/schabi/newpipe/player/helper/LoadController.java @@ -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(); diff --git a/app/src/main/java/org/schabi/newpipe/player/helper/PlaybackParameterDialog.java b/app/src/main/java/org/schabi/newpipe/player/helper/PlaybackParameterDialog.java index a9a36e2f5..bbe281921 100644 --- a/app/src/main/java/org/schabi/newpipe/player/helper/PlaybackParameterDialog.java +++ b/app/src/main/java/org/schabi/newpipe/player/helper/PlaybackParameterDialog.java @@ -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(); diff --git a/app/src/main/java/org/schabi/newpipe/player/helper/PlayerHelper.java b/app/src/main/java/org/schabi/newpipe/player/helper/PlayerHelper.java index d60a14381..828833a8d 100644 --- a/app/src/main/java/org/schabi/newpipe/player/helper/PlayerHelper.java +++ b/app/src/main/java/org/schabi/newpipe/player/helper/PlayerHelper.java @@ -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, diff --git a/app/src/main/java/org/schabi/newpipe/player/playqueue/PlayQueueItem.java b/app/src/main/java/org/schabi/newpipe/player/playqueue/PlayQueueItem.java index 182a91e59..f7dfc562e 100644 --- a/app/src/main/java/org/schabi/newpipe/player/playqueue/PlayQueueItem.java +++ b/app/src/main/java/org/schabi/newpipe/player/playqueue/PlayQueueItem.java @@ -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; diff --git a/app/src/main/java/org/schabi/newpipe/player/playqueue/PlayQueueItemBuilder.java b/app/src/main/java/org/schabi/newpipe/player/playqueue/PlayQueueItemBuilder.java index 3e0865a3e..f2e98d866 100644 --- a/app/src/main/java/org/schabi/newpipe/player/playqueue/PlayQueueItemBuilder.java +++ b/app/src/main/java/org/schabi/newpipe/player/playqueue/PlayQueueItemBuilder.java @@ -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) { diff --git a/app/src/main/java/org/schabi/newpipe/player/seekbarpreview/SeekbarPreviewThumbnailHolder.java b/app/src/main/java/org/schabi/newpipe/player/seekbarpreview/SeekbarPreviewThumbnailHolder.java index 30c5ce910..08c6366c8 100644 --- a/app/src/main/java/org/schabi/newpipe/player/seekbarpreview/SeekbarPreviewThumbnailHolder.java +++ b/app/src/main/java/org/schabi/newpipe/player/seekbarpreview/SeekbarPreviewThumbnailHolder.java @@ -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, diff --git a/app/src/main/java/org/schabi/newpipe/player/seekbarpreview/SyncImageLoadingListener.java b/app/src/main/java/org/schabi/newpipe/player/seekbarpreview/SyncImageLoadingListener.java deleted file mode 100644 index 46c278bf2..000000000 --- a/app/src/main/java/org/schabi/newpipe/player/seekbarpreview/SyncImageLoadingListener.java +++ /dev/null @@ -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; - } -} diff --git a/app/src/main/java/org/schabi/newpipe/settings/ContentSettingsFragment.java b/app/src/main/java/org/schabi/newpipe/settings/ContentSettingsFragment.java index f1e19af94..6e7e75932 100644 --- a/app/src/main/java/org/schabi/newpipe/settings/ContentSettingsFragment.java +++ b/app/src/main/java/org/schabi/newpipe/settings/ContentSettingsFragment.java @@ -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())); diff --git a/app/src/main/java/org/schabi/newpipe/settings/DownloadSettingsFragment.java b/app/src/main/java/org/schabi/newpipe/settings/DownloadSettingsFragment.java index b4af0e43b..dfd77f049 100644 --- a/app/src/main/java/org/schabi/newpipe/settings/DownloadSettingsFragment.java +++ b/app/src/main/java/org/schabi/newpipe/settings/DownloadSettingsFragment.java @@ -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(); } diff --git a/app/src/main/java/org/schabi/newpipe/settings/NewPipeSettings.java b/app/src/main/java/org/schabi/newpipe/settings/NewPipeSettings.java index 33f00ec1a..aa21c4422 100644 --- a/app/src/main/java/org/schabi/newpipe/settings/NewPipeSettings.java +++ b/app/src/main/java/org/schabi/newpipe/settings/NewPipeSettings.java @@ -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 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); + } } diff --git a/app/src/main/java/org/schabi/newpipe/settings/PeertubeInstanceListFragment.java b/app/src/main/java/org/schabi/newpipe/settings/PeertubeInstanceListFragment.java index 207d1ffc6..5f388efb7 100644 --- a/app/src/main/java/org/schabi/newpipe/settings/PeertubeInstanceListFragment.java +++ b/app/src/main/java/org/schabi/newpipe/settings/PeertubeInstanceListFragment.java @@ -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); }) diff --git a/app/src/main/java/org/schabi/newpipe/settings/SelectChannelFragment.java b/app/src/main/java/org/schabi/newpipe/settings/SelectChannelFragment.java index 7f706be77..a0105a11f 100644 --- a/app/src/main/java/org/schabi/newpipe/settings/SelectChannelFragment.java +++ b/app/src/main/java/org/schabi/newpipe/settings/SelectChannelFragment.java @@ -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 diff --git a/app/src/main/java/org/schabi/newpipe/settings/SelectPlaylistFragment.java b/app/src/main/java/org/schabi/newpipe/settings/SelectPlaylistFragment.java index 63da3274f..f94e391ba 100644 --- a/app/src/main/java/org/schabi/newpipe/settings/SelectPlaylistFragment.java +++ b/app/src/main/java/org/schabi/newpipe/settings/SelectPlaylistFragment.java @@ -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); } } diff --git a/app/src/main/java/org/schabi/newpipe/settings/SettingMigrations.java b/app/src/main/java/org/schabi/newpipe/settings/SettingMigrations.java index 2d5fedec0..b0b9567d8 100644 --- a/app/src/main/java/org/schabi/newpipe/settings/SettingMigrations.java +++ b/app/src/main/java/org/schabi/newpipe/settings/SettingMigrations.java @@ -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:
+ * - 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
+ * - add {@code MIGRATION_P_P+1} at the end of {@link SettingMigrations#SETTING_MIGRATIONS}
+ * - 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 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. *

@@ -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 diff --git a/app/src/main/java/org/schabi/newpipe/streams/io/StoredFileHelper.java b/app/src/main/java/org/schabi/newpipe/streams/io/StoredFileHelper.java index dd379b730..9fe4a9340 100644 --- a/app/src/main/java/org/schabi/newpipe/streams/io/StoredFileHelper.java +++ b/app/src/main/java/org/schabi/newpipe/streams/io/StoredFileHelper.java @@ -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, diff --git a/app/src/main/java/org/schabi/newpipe/util/DeviceUtils.java b/app/src/main/java/org/schabi/newpipe/util/DeviceUtils.java index 8d918c162..73bc4d6bb 100644 --- a/app/src/main/java/org/schabi/newpipe/util/DeviceUtils.java +++ b/app/src/main/java/org/schabi/newpipe/util/DeviceUtils.java @@ -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(); + } } diff --git a/app/src/main/java/org/schabi/newpipe/util/ImageDisplayConstants.java b/app/src/main/java/org/schabi/newpipe/util/ImageDisplayConstants.java deleted file mode 100644 index 62e80275e..000000000 --- a/app/src/main/java/org/schabi/newpipe/util/ImageDisplayConstants.java +++ /dev/null @@ -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() { } -} diff --git a/app/src/main/java/org/schabi/newpipe/util/Localization.java b/app/src/main/java/org/schabi/newpipe/util/Localization.java index 674c7844f..b222f6abf 100644 --- a/app/src/main/java/org/schabi/newpipe/util/Localization.java +++ b/app/src/main/java/org/schabi/newpipe/util/Localization.java @@ -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) { diff --git a/app/src/main/java/org/schabi/newpipe/util/NavigationHelper.java b/app/src/main/java/org/schabi/newpipe/util/NavigationHelper.java index 27db9a1f9..ae1e6cede 100644 --- a/app/src/main/java/org/schabi/newpipe/util/NavigationHelper.java +++ b/app/src/main/java/org/schabi/newpipe/util/NavigationHelper.java @@ -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()); } } diff --git a/app/src/main/java/org/schabi/newpipe/util/PermissionHelper.java b/app/src/main/java/org/schabi/newpipe/util/PermissionHelper.java index c64631b72..160eb59cd 100644 --- a/app/src/main/java/org/schabi/newpipe/util/PermissionHelper.java +++ b/app/src/main/java/org/schabi/newpipe/util/PermissionHelper.java @@ -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) { diff --git a/app/src/main/java/org/schabi/newpipe/util/PicassoHelper.java b/app/src/main/java/org/schabi/newpipe/util/PicassoHelper.java new file mode 100644 index 000000000..e15ecd277 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/util/PicassoHelper.java @@ -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 + } + } +} diff --git a/app/src/main/java/org/schabi/newpipe/util/SavedState.kt b/app/src/main/java/org/schabi/newpipe/util/SavedState.kt index 313d56192..c556b59ff 100644 --- a/app/src/main/java/org/schabi/newpipe/util/SavedState.kt +++ b/app/src/main/java/org/schabi/newpipe/util/SavedState.kt @@ -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. diff --git a/app/src/main/java/org/schabi/newpipe/util/StreamDialogEntry.java b/app/src/main/java/org/schabi/newpipe/util/StreamDialogEntry.java index 89b48c9a7..6245d6f14 100644 --- a/app/src/main/java/org/schabi/newpipe/util/StreamDialogEntry.java +++ b/app/src/main/java/org/schabi/newpipe/util/StreamDialogEntry.java @@ -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.
@@ -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()); + } } diff --git a/app/src/main/java/org/schabi/newpipe/util/external_communication/InternalUrlsHandler.java b/app/src/main/java/org/schabi/newpipe/util/external_communication/InternalUrlsHandler.java index 39ec51ce4..240341ab0 100644 --- a/app/src/main/java/org/schabi/newpipe/util/external_communication/InternalUrlsHandler.java +++ b/app/src/main/java/org/schabi/newpipe/util/external_communication/InternalUrlsHandler.java @@ -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; } diff --git a/app/src/main/java/org/schabi/newpipe/util/external_communication/ShareUtils.java b/app/src/main/java/org/schabi/newpipe/util/external_communication/ShareUtils.java index e49cd6ea2..22ab6cf2b 100644 --- a/app/src/main/java/org/schabi/newpipe/util/external_communication/ShareUtils.java +++ b/app/src/main/java/org/schabi/newpipe/util/external_communication/ShareUtils.java @@ -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 diff --git a/app/src/main/java/org/schabi/newpipe/util/external_communication/TextLinkifier.java b/app/src/main/java/org/schabi/newpipe/util/external_communication/TextLinkifier.java index 76da09609..f435653b5 100644 --- a/app/src/main/java/org/schabi/newpipe/util/external_communication/TextLinkifier.java +++ b/app/src/main/java/org/schabi/newpipe/util/external_communication/TextLinkifier.java @@ -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); } } diff --git a/app/src/main/java/org/schabi/newpipe/util/external_communication/TimestampExtractor.java b/app/src/main/java/org/schabi/newpipe/util/external_communication/TimestampExtractor.java new file mode 100644 index 000000000..a13c66402 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/util/external_communication/TimestampExtractor.java @@ -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.
+ * If not null. + */ + 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; + } + } +} diff --git a/app/src/main/java/us/shandian/giga/get/MissionRecoveryInfo.kt b/app/src/main/java/us/shandian/giga/get/MissionRecoveryInfo.kt index b2364f058..403eee0c7 100644 --- a/app/src/main/java/us/shandian/giga/get/MissionRecoveryInfo.kt +++ b/app/src/main/java/us/shandian/giga/get/MissionRecoveryInfo.kt @@ -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 diff --git a/app/src/main/java/us/shandian/giga/service/DownloadManagerService.java b/app/src/main/java/us/shandian/giga/service/DownloadManagerService.java index 52c28828d..d96b4fc5b 100755 --- a/app/src/main/java/us/shandian/giga/service/DownloadManagerService.java +++ b/app/src/main/java/us/shandian/giga/service/DownloadManagerService.java @@ -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) { diff --git a/app/src/main/java/us/shandian/giga/ui/adapter/MissionAdapter.java b/app/src/main/java/us/shandian/giga/ui/adapter/MissionAdapter.java index e06485fdf..057b9cb09 100644 --- a/app/src/main/java/us/shandian/giga/ui/adapter/MissionAdapter.java +++ b/app/src/main/java/us/shandian/giga/ui/adapter/MissionAdapter.java @@ -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 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 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 i = mHidden.iterator(); diff --git a/app/src/main/res/anim/switch_service_in.xml b/app/src/main/res/anim/switch_service_in.xml deleted file mode 100644 index b15e57999..000000000 --- a/app/src/main/res/anim/switch_service_in.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - - - diff --git a/app/src/main/res/anim/switch_service_out.xml b/app/src/main/res/anim/switch_service_out.xml deleted file mode 100644 index 824efc9a8..000000000 --- a/app/src/main/res/anim/switch_service_out.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - - - diff --git a/app/src/main/res/drawable-hdpi/ic_close_white.png b/app/src/main/res/drawable-hdpi/ic_close_white.png index 9af50602d..5546fb0ff 100644 Binary files a/app/src/main/res/drawable-hdpi/ic_close_white.png and b/app/src/main/res/drawable-hdpi/ic_close_white.png differ diff --git a/app/src/main/res/drawable-hdpi/ic_hourglass_top_white.png b/app/src/main/res/drawable-hdpi/ic_hourglass_top_white.png index dc2f5122a..1f1f9046c 100644 Binary files a/app/src/main/res/drawable-hdpi/ic_hourglass_top_white.png and b/app/src/main/res/drawable-hdpi/ic_hourglass_top_white.png differ diff --git a/app/src/main/res/drawable-hdpi/ic_newpipe_triangle_white.png b/app/src/main/res/drawable-hdpi/ic_newpipe_triangle_white.png index 6c05313dd..cd3b6d182 100644 Binary files a/app/src/main/res/drawable-hdpi/ic_newpipe_triangle_white.png and b/app/src/main/res/drawable-hdpi/ic_newpipe_triangle_white.png differ diff --git a/app/src/main/res/drawable-hdpi/ic_newpipe_update.png b/app/src/main/res/drawable-hdpi/ic_newpipe_update.png index f8e0fc597..047d2f798 100755 Binary files a/app/src/main/res/drawable-hdpi/ic_newpipe_update.png and b/app/src/main/res/drawable-hdpi/ic_newpipe_update.png differ diff --git a/app/src/main/res/drawable-hdpi/ic_replay_white.png b/app/src/main/res/drawable-hdpi/ic_replay_white.png index 01b248180..c706f8097 100644 Binary files a/app/src/main/res/drawable-hdpi/ic_replay_white.png and b/app/src/main/res/drawable-hdpi/ic_replay_white.png differ diff --git a/app/src/main/res/drawable-mdpi/ic_close_white.png b/app/src/main/res/drawable-mdpi/ic_close_white.png index 199af1303..1037ea613 100644 Binary files a/app/src/main/res/drawable-mdpi/ic_close_white.png and b/app/src/main/res/drawable-mdpi/ic_close_white.png differ diff --git a/app/src/main/res/drawable-mdpi/ic_hourglass_top_white.png b/app/src/main/res/drawable-mdpi/ic_hourglass_top_white.png index 8df1a61ec..734e8eca3 100644 Binary files a/app/src/main/res/drawable-mdpi/ic_hourglass_top_white.png and b/app/src/main/res/drawable-mdpi/ic_hourglass_top_white.png differ diff --git a/app/src/main/res/drawable-mdpi/ic_newpipe_triangle_white.png b/app/src/main/res/drawable-mdpi/ic_newpipe_triangle_white.png index 97c60c91c..f967011b0 100644 Binary files a/app/src/main/res/drawable-mdpi/ic_newpipe_triangle_white.png and b/app/src/main/res/drawable-mdpi/ic_newpipe_triangle_white.png differ diff --git a/app/src/main/res/drawable-mdpi/ic_newpipe_update.png b/app/src/main/res/drawable-mdpi/ic_newpipe_update.png index 23b1dbfa3..bec3631ab 100755 Binary files a/app/src/main/res/drawable-mdpi/ic_newpipe_update.png and b/app/src/main/res/drawable-mdpi/ic_newpipe_update.png differ diff --git a/app/src/main/res/drawable-mdpi/ic_replay_white.png b/app/src/main/res/drawable-mdpi/ic_replay_white.png index f351cf709..24558a423 100644 Binary files a/app/src/main/res/drawable-mdpi/ic_replay_white.png and b/app/src/main/res/drawable-mdpi/ic_replay_white.png differ diff --git a/app/src/main/res/drawable-night-v23/splash_background.xml b/app/src/main/res/drawable-night-v23/splash_background.xml new file mode 100644 index 000000000..76f5bada6 --- /dev/null +++ b/app/src/main/res/drawable-night-v23/splash_background.xml @@ -0,0 +1,12 @@ + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable-night/splash_background.xml b/app/src/main/res/drawable-night/splash_background.xml new file mode 100644 index 000000000..237f4cdae --- /dev/null +++ b/app/src/main/res/drawable-night/splash_background.xml @@ -0,0 +1,12 @@ + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable-nodpi/background_header.png b/app/src/main/res/drawable-nodpi/background_header.png index 04032b55d..e00e9a21f 100644 Binary files a/app/src/main/res/drawable-nodpi/background_header.png and b/app/src/main/res/drawable-nodpi/background_header.png differ diff --git a/app/src/main/res/drawable-nodpi/buddy.png b/app/src/main/res/drawable-nodpi/buddy.png index 878c5dff3..8713ee02b 100644 Binary files a/app/src/main/res/drawable-nodpi/buddy.png and b/app/src/main/res/drawable-nodpi/buddy.png differ diff --git a/app/src/main/res/drawable-nodpi/buddy_channel_item.png b/app/src/main/res/drawable-nodpi/buddy_channel_item.png index 3c5f8f994..64d4cb1a0 100644 Binary files a/app/src/main/res/drawable-nodpi/buddy_channel_item.png and b/app/src/main/res/drawable-nodpi/buddy_channel_item.png differ diff --git a/app/src/main/res/drawable-nodpi/channel_banner.png b/app/src/main/res/drawable-nodpi/channel_banner.png index 7532bd3a2..12e70bb6d 100644 Binary files a/app/src/main/res/drawable-nodpi/channel_banner.png and b/app/src/main/res/drawable-nodpi/channel_banner.png differ diff --git a/app/src/main/res/drawable-nodpi/dummy_thumbnail.png b/app/src/main/res/drawable-nodpi/dummy_thumbnail.png index 49d6e5110..86f454186 100644 Binary files a/app/src/main/res/drawable-nodpi/dummy_thumbnail.png and b/app/src/main/res/drawable-nodpi/dummy_thumbnail.png differ diff --git a/app/src/main/res/drawable-nodpi/dummy_thumbnail_dark.png b/app/src/main/res/drawable-nodpi/dummy_thumbnail_dark.png index d6ab854c3..02f698918 100644 Binary files a/app/src/main/res/drawable-nodpi/dummy_thumbnail_dark.png and b/app/src/main/res/drawable-nodpi/dummy_thumbnail_dark.png differ diff --git a/app/src/main/res/drawable-nodpi/dummy_thumbnail_playlist.png b/app/src/main/res/drawable-nodpi/dummy_thumbnail_playlist.png index 3873b83cc..9ba84fdb4 100644 Binary files a/app/src/main/res/drawable-nodpi/dummy_thumbnail_playlist.png and b/app/src/main/res/drawable-nodpi/dummy_thumbnail_playlist.png differ diff --git a/app/src/main/res/drawable-nodpi/newpipe_logo_nude_shadow.png b/app/src/main/res/drawable-nodpi/newpipe_logo_nude_shadow.png index 55c5c105d..49c12af83 100644 Binary files a/app/src/main/res/drawable-nodpi/newpipe_logo_nude_shadow.png and b/app/src/main/res/drawable-nodpi/newpipe_logo_nude_shadow.png differ diff --git a/app/src/main/res/drawable-nodpi/not_available_monkey.png b/app/src/main/res/drawable-nodpi/not_available_monkey.png deleted file mode 100644 index babd53602..000000000 Binary files a/app/src/main/res/drawable-nodpi/not_available_monkey.png and /dev/null differ diff --git a/app/src/main/res/drawable-nodpi/place_holder_bandcamp.png b/app/src/main/res/drawable-nodpi/place_holder_bandcamp.png index 848e109c2..13c44b649 100644 Binary files a/app/src/main/res/drawable-nodpi/place_holder_bandcamp.png and b/app/src/main/res/drawable-nodpi/place_holder_bandcamp.png differ diff --git a/app/src/main/res/drawable-nodpi/place_holder_cloud.png b/app/src/main/res/drawable-nodpi/place_holder_cloud.png index 0f9bd26c2..c4ba2a6f4 100644 Binary files a/app/src/main/res/drawable-nodpi/place_holder_cloud.png and b/app/src/main/res/drawable-nodpi/place_holder_cloud.png differ diff --git a/app/src/main/res/drawable-nodpi/place_holder_gadse.png b/app/src/main/res/drawable-nodpi/place_holder_gadse.png index 7e3d22e81..9b479ed4f 100644 Binary files a/app/src/main/res/drawable-nodpi/place_holder_gadse.png and b/app/src/main/res/drawable-nodpi/place_holder_gadse.png differ diff --git a/app/src/main/res/drawable-nodpi/place_holder_peertube.png b/app/src/main/res/drawable-nodpi/place_holder_peertube.png index 331bf94f6..81dfdb8cc 100644 Binary files a/app/src/main/res/drawable-nodpi/place_holder_peertube.png and b/app/src/main/res/drawable-nodpi/place_holder_peertube.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_close_white.png b/app/src/main/res/drawable-xhdpi/ic_close_white.png index fc69b5bb5..568663ed0 100644 Binary files a/app/src/main/res/drawable-xhdpi/ic_close_white.png and b/app/src/main/res/drawable-xhdpi/ic_close_white.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_hourglass_top_white.png b/app/src/main/res/drawable-xhdpi/ic_hourglass_top_white.png index 29a36f543..e53c699db 100644 Binary files a/app/src/main/res/drawable-xhdpi/ic_hourglass_top_white.png and b/app/src/main/res/drawable-xhdpi/ic_hourglass_top_white.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_newpipe_triangle_white.png b/app/src/main/res/drawable-xhdpi/ic_newpipe_triangle_white.png index d4e94d0d1..5fe229a96 100644 Binary files a/app/src/main/res/drawable-xhdpi/ic_newpipe_triangle_white.png and b/app/src/main/res/drawable-xhdpi/ic_newpipe_triangle_white.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_newpipe_update.png b/app/src/main/res/drawable-xhdpi/ic_newpipe_update.png index b9a296064..31eba305c 100755 Binary files a/app/src/main/res/drawable-xhdpi/ic_newpipe_update.png and b/app/src/main/res/drawable-xhdpi/ic_newpipe_update.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_replay_white.png b/app/src/main/res/drawable-xhdpi/ic_replay_white.png index 153e3dbf3..47b75ceb9 100644 Binary files a/app/src/main/res/drawable-xhdpi/ic_replay_white.png and b/app/src/main/res/drawable-xhdpi/ic_replay_white.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_close_white.png b/app/src/main/res/drawable-xxhdpi/ic_close_white.png index 9ec308cef..990895143 100644 Binary files a/app/src/main/res/drawable-xxhdpi/ic_close_white.png and b/app/src/main/res/drawable-xxhdpi/ic_close_white.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_hourglass_top_white.png b/app/src/main/res/drawable-xxhdpi/ic_hourglass_top_white.png index 9d214c497..b8b98737f 100644 Binary files a/app/src/main/res/drawable-xxhdpi/ic_hourglass_top_white.png and b/app/src/main/res/drawable-xxhdpi/ic_hourglass_top_white.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_newpipe_triangle_white.png b/app/src/main/res/drawable-xxhdpi/ic_newpipe_triangle_white.png index fa554585f..595d5ab11 100644 Binary files a/app/src/main/res/drawable-xxhdpi/ic_newpipe_triangle_white.png and b/app/src/main/res/drawable-xxhdpi/ic_newpipe_triangle_white.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_newpipe_update.png b/app/src/main/res/drawable-xxhdpi/ic_newpipe_update.png index 5d348e6e3..eda411234 100755 Binary files a/app/src/main/res/drawable-xxhdpi/ic_newpipe_update.png and b/app/src/main/res/drawable-xxhdpi/ic_newpipe_update.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_replay_white.png b/app/src/main/res/drawable-xxhdpi/ic_replay_white.png index dc60f4ecd..9a8e1507d 100644 Binary files a/app/src/main/res/drawable-xxhdpi/ic_replay_white.png and b/app/src/main/res/drawable-xxhdpi/ic_replay_white.png differ diff --git a/app/src/main/res/drawable-xxxhdpi/ic_close_white.png b/app/src/main/res/drawable-xxxhdpi/ic_close_white.png index 535d1df0c..06854ca49 100644 Binary files a/app/src/main/res/drawable-xxxhdpi/ic_close_white.png and b/app/src/main/res/drawable-xxxhdpi/ic_close_white.png differ diff --git a/app/src/main/res/drawable-xxxhdpi/ic_newpipe_triangle_white.png b/app/src/main/res/drawable-xxxhdpi/ic_newpipe_triangle_white.png index 26e134fac..699e0c158 100644 Binary files a/app/src/main/res/drawable-xxxhdpi/ic_newpipe_triangle_white.png and b/app/src/main/res/drawable-xxxhdpi/ic_newpipe_triangle_white.png differ diff --git a/app/src/main/res/drawable-xxxhdpi/ic_newpipe_update.png b/app/src/main/res/drawable-xxxhdpi/ic_newpipe_update.png index bc06d3953..0771140c1 100755 Binary files a/app/src/main/res/drawable-xxxhdpi/ic_newpipe_update.png and b/app/src/main/res/drawable-xxxhdpi/ic_newpipe_update.png differ diff --git a/app/src/main/res/drawable-xxxhdpi/ic_replay_white.png b/app/src/main/res/drawable-xxxhdpi/ic_replay_white.png index 372bc8bd1..6a9092761 100644 Binary files a/app/src/main/res/drawable-xxxhdpi/ic_replay_white.png and b/app/src/main/res/drawable-xxxhdpi/ic_replay_white.png differ diff --git a/app/src/main/res/drawable/custom_progress_bar.xml b/app/src/main/res/drawable/custom_progress_bar.xml deleted file mode 100644 index 0ead1c4be..000000000 --- a/app/src/main/res/drawable/custom_progress_bar.xml +++ /dev/null @@ -1,17 +0,0 @@ - - - - - - - - - - - - - - - - - diff --git a/app/src/main/res/drawable/ic_edit.xml b/app/src/main/res/drawable/ic_edit.xml deleted file mode 100644 index 5505cc775..000000000 --- a/app/src/main/res/drawable/ic_edit.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/ic_expand_less.xml b/app/src/main/res/drawable/ic_expand_less.xml deleted file mode 100644 index 7b6a65628..000000000 --- a/app/src/main/res/drawable/ic_expand_less.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/ic_import_export.xml b/app/src/main/res/drawable/ic_import_export.xml deleted file mode 100644 index d826451b4..000000000 --- a/app/src/main/res/drawable/ic_import_export.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/ic_screen_rotation.xml b/app/src/main/res/drawable/ic_screen_rotation.xml deleted file mode 100644 index cc851bd83..000000000 --- a/app/src/main/res/drawable/ic_screen_rotation.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/item_in_history_indicator_background.xml b/app/src/main/res/drawable/item_in_history_indicator_background.xml deleted file mode 100644 index 1c3a9a56b..000000000 --- a/app/src/main/res/drawable/item_in_history_indicator_background.xml +++ /dev/null @@ -1,7 +0,0 @@ - - - - diff --git a/app/src/main/res/drawable/not_available_monkey.xml b/app/src/main/res/drawable/not_available_monkey.xml new file mode 100644 index 000000000..b15a381c5 --- /dev/null +++ b/app/src/main/res/drawable/not_available_monkey.xml @@ -0,0 +1,27 @@ + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout-large-land/fragment_video_detail.xml b/app/src/main/res/layout-large-land/fragment_video_detail.xml index d4f1ccc3d..7f664f5d4 100644 --- a/app/src/main/res/layout-large-land/fragment_video_detail.xml +++ b/app/src/main/res/layout-large-land/fragment_video_detail.xml @@ -52,7 +52,7 @@ android:id="@+id/detail_thumbnail_image_view" android:layout_width="match_parent" android:layout_height="wrap_content" - android:background="@android:color/transparent" + android:background="?windowBackground" android:contentDescription="@string/detail_thumbnail_view_description" android:scaleType="fitCenter" tools:ignore="RtlHardcoded" diff --git a/app/src/main/res/layout/activity_error.xml b/app/src/main/res/layout/activity_error.xml index 2dc668df1..282d2d047 100644 --- a/app/src/main/res/layout/activity_error.xml +++ b/app/src/main/res/layout/activity_error.xml @@ -36,7 +36,6 @@ android:textStyle="bold" /> @@ -86,7 +83,6 @@ @@ -108,7 +103,6 @@ diff --git a/app/src/main/res/layout/dialog_feed_group_create.xml b/app/src/main/res/layout/dialog_feed_group_create.xml index e849524b0..5b62c0dd8 100644 --- a/app/src/main/res/layout/dialog_feed_group_create.xml +++ b/app/src/main/res/layout/dialog_feed_group_create.xml @@ -7,7 +7,6 @@ android:orientation="vertical"> @@ -205,7 +204,7 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_toStartOf="@+id/confirm_button" - android:text="@android:string/cancel" /> + android:text="@string/cancel" />