diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 000000000..82025c3c5 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,112 @@ +name: CI + +on: + workflow_dispatch: + pull_request: + branches: + - legacy + paths-ignore: + - 'README.md' + - 'fastlane/**' + - 'assets/**' + - '.github/**/*.md' + - '.github/FUNDING.yml' + - '.github/ISSUE_TEMPLATE/**' + push: + branches: + - legacy + paths-ignore: + - 'README.md' + - 'fastlane/**' + - 'assets/**' + - '.github/**/*.md' + - '.github/FUNDING.yml' + - '.github/ISSUE_TEMPLATE/**' + +jobs: + build-and-test-jvm: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v3 + - uses: gradle/wrapper-validation-action@v1 + + - name: create and checkout branch + # push events already checked out the branch + if: github.event_name == 'pull_request' + run: git checkout -B ${{ github.head_ref }} + + - name: set up JDK 11 + uses: actions/setup-java@v3 + with: + java-version: 11 + distribution: "temurin" + cache: 'gradle' + + - name: Build debug APK + run: ./gradlew assembleDebug lintDebug --stacktrace -DskipFormatKtlint + + - name: Upload APK + uses: actions/upload-artifact@v3 + with: + name: app + path: app/build/outputs/apk/debug/*.apk + +# test-android: +# # macos has hardware acceleration. See android-emulator-runner action +# runs-on: macos-latest +# timeout-minutes: 20 +# strategy: +# matrix: +# # api-level 16 is min sdk, but throws errors related to desugaring +# api-level: [ 21, 29 ] +# steps: +# - uses: actions/checkout@v3 +# +# - name: set up JDK 11 +# uses: actions/setup-java@v3 +# with: +# java-version: 11 +# distribution: "temurin" +# cache: 'gradle' +# +# - name: Run android tests +# uses: reactivecircus/android-emulator-runner@v2 +# with: +# api-level: ${{ matrix.api-level }} +# # workaround to emulator bug: https://github.com/ReactiveCircus/android-emulator-runner/issues/160 +# emulator-build: 7425822 +# script: ./gradlew connectedCheck +# +# sonar: +# runs-on: ubuntu-latest +# steps: +# - uses: actions/checkout@v3 +# with: +# fetch-depth: 0 # Shallow clones should be disabled for a better relevancy of analysis + +# - name: Set up JDK 11 +# uses: actions/setup-java@v3 +# with: +# java-version: 11 # Sonar requires JDK 11 +# distribution: "temurin" + +# - name: Cache SonarCloud packages +# uses: actions/cache@v3 +# with: +# path: ~/.sonar/cache +# key: ${{ runner.os }}-sonar +# restore-keys: ${{ runner.os }}-sonar + +# - name: Cache Gradle packages +# uses: actions/cache@v3 +# 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 +# SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} +# run: ./gradlew build sonarqube --info diff --git a/.github/workflows/release-legacy.yml b/.github/workflows/release-legacy.yml new file mode 100644 index 000000000..b6f5f2acf --- /dev/null +++ b/.github/workflows/release-legacy.yml @@ -0,0 +1,40 @@ +name: release legacy + +on: + push: + tags: + - "v*-legacy" + +jobs: + release: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + with: + fetch-depth: '0' + + - uses: actions/setup-java@v1 + with: + java-version: '8' + + - name: "Build release apk" + run: ./gradlew assembleRelease --stacktrace + + - name: "Sign release" + uses: r0adkll/sign-android-release@v1 + id: sign_app + with: + releaseDirectory: app/build/outputs/apk/release + signingKeyBase64: ${{ secrets.SIGNING_KEY }} + alias: ${{ secrets.ALIAS }} + keyStorePassword: ${{ secrets.KEY_STORE_PASSWORD }} + keyPassword: ${{ secrets.KEY_PASSWORD }} + + - name: "Rename archive" + run: mv ${{ steps.sign_app.outputs.signedReleaseFile }} app-release.apk + + - name: "Create GitHub release" + uses: softprops/action-gh-release@v1 + with: + files: app-release.apk + fail_on_unmatched_files: true \ No newline at end of file diff --git a/README.md b/README.md index 219cda826..db2866755 100644 --- a/README.md +++ b/README.md @@ -1,18 +1,18 @@ -# FoxPipe - -A NewPipe fork with the old UI having separate Players handling 3 seperate Queues -

-

FoxPipe

-

A libre lightweight streaming frontend for Android.

+

+

NewPipe_Legacy_Revo

+

NewPipeLegacy preunified & unified version with working NewPipeExtractor dependency.

- + - + + + +


-

ScreenshotsDescriptionFeaturesInstallation and updatesContributionLicense

-

WebsiteFAQPress

+

ScreenshotsDescriptionFeaturesInstallation and updatesContributionDonateLicense

+

WebsiteBlogFAQPress


WARNING: THIS IS A BETA VERSION, THEREFORE YOU MAY ENCOUNTER BUGS. IF YOU DO, OPEN AN ISSUE VIA OUR GITHUB REPOSITORY. @@ -26,21 +26,24 @@ A NewPipe fork with the old UI having separate Players handling 3 seperate Queue [](fastlane/metadata/android/en-US/images/phoneScreenshots/shot_03.png) [](fastlane/metadata/android/en-US/images/phoneScreenshots/shot_04.png) [](fastlane/metadata/android/en-US/images/phoneScreenshots/shot_05.png) +[](fastlane/metadata/android/en-US/images/phoneScreenshots/shot_06.png) [](fastlane/metadata/android/en-US/images/phoneScreenshots/shot_07.png) [](fastlane/metadata/android/en-US/images/phoneScreenshots/shot_10.png) [](fastlane/metadata/android/en-US/images/tenInchScreenshots/shot_11.png) [](fastlane/metadata/android/en-US/images/tenInchScreenshots/shot_12.png) - ## Description -This repository contains old preunified version 0.19.8 of [NewPipe](https://github.com/TeamNewPipe/NewPipe/releases/tag/v0.19.8) with up-to-date [NewPipeExtractor](https://github.com/ShareASmile/NewPipeExtractor/tree/ify) dependency. + This repository contains updated preunified [NewPipeLegacy-v0.19.8](https://github.com/TeamNewPipe/NewPipe-legacy/releases/tag/v0.19.8) & revived unified v0.21.2 of [tossj fork of NewPipeLegacy](https://github.com/tossj/NewPipe-legacy/tree/update-newpipe-extractor-0.21.1) with updated [NewPipeExtractor for Legacy Devices](https://github.com/ShareASmile/NewPipeExtractor) dependency. + +The application itself heavily relies on the extractor component which is responsible for proper parsing of various video/audio streams, including Youtube site. The old NewPipe Legacy version 0.19.8 depends on old extractor version which is practically deprecated and can't handle current Youtube (and similar?) streams, thus rendering the application useless for daily use. + +NewPipe Legacy version 0.19.8+ in this repository uses the updated version of NewPipeExtractor for legacy devices and resolves the forementioned issue, thus making it possible to use old NewPipe Legacy updated version 0.19.8 with bug fixes, features & support for SoundCloud, Bandcamp, media.ccc.de sites added for legacy devices along with updated extractor version. However v0.21.2+ is also available with Unified Player in toss branch. APK's for legacy devices on Ice Cream Sandwich (Android 4.0.1 – 4.0.4) has been available from Releases, but it has a very basic support, can be buggy. -The application itself heavily relies on the extractor component which is responsible for proper parsing of various video/audio streams, including Youtube site. The old NewPipe version 0.19.8 depends on old extractor version which is practically deprecated and can't handle current Youtube (and similar?) streams,thus rendering the application useless for daily use. -FoxPipe in this repository uses the updated version of NewPipeExtractor for legacy devices and resolves the forementioned issue, thus making it possible to use old NewPipe version 0.19.8 based FoxPipe with some bug fixes & features added along with updated extractor version. You don't need a YouTube account to use NewPipe, it is a copylefted libre software. +You don't need a YouTube account to use NewPipe, it is a copylefted libre software. ## Motivation -Not so long ago, NewPipe project implemented a new UI elements for video streams. Personally, I didn't like that change. I wanted to keep using the old UI having separate Players(Video, Popup & Background) handling three seperate Queues simultaneously instead. +Not so long ago, NewPipe project implemented a new UI elements for video streams. Personally, I didn't like that change. I wanted to keep using the old UI instead. ### Features @@ -50,7 +53,7 @@ Not so long ago, NewPipe project implemented a new UI elements for video streams * Listen to YouTube videos * Popup mode (floating player) * Select streaming player to watch video with -* Make Personalised Watch Later Playlists +* Make Personalised Playlists Locally on device * Open a video in Kodi * Show next/related videos * Search YouTube in a specific language @@ -85,11 +88,10 @@ NewPipe supports multiple services. Our [docs](https://teamnewpipe.github.io/doc ## Installation and updates -You can install NewPipe using one of the following methods: - 1. Download the APK from [Github Releases](https://github.com/ShareASmile/FoxPipe/releases) and install it. - 2. Build a debug APK yourself or install from [actions](https://github.com/ShareASmile/FoxPipe/actions) This is the fastest way to get new features on your device. +You can install NewPipe Legacy Revo using one of the following methods: -We recommend method 1 for most users. 2. Building a debug APK using method 2 excludes a key entirely. Signing keys help ensure that a user isn't tricked into installing a malicious update to an app. + 1. Download the APK from [Github Releases](https://github.com/ShareASmile/NewPipe-Legacy-Revo/releases) and install it. + 2. Build a debug APK yourself. This is the fastest way to get new features on your device, but is much more complicated, so we recommend using one of the other methods. ## Contribution Whether you have ideas, translations, design changes, code cleaning, or real heavy code changes, help is always welcome. @@ -97,11 +99,35 @@ The more is done the better it gets! If you'd like to get involved, check our [contribution notes](.github/CONTRIBUTING.md). + +Translation status + + +## Donate +If you like NewPipe we'd be happy about a donation. You can either send bitcoin or donate via Bountysource or Liberapay. For further info on donating to NewPipe, please visit our [upstream](https://newpipe.net/donate). + + + + + + + + + + + + + + + + + +
BitcoinBitcoin QR code16A9J59ahMRqkLSZjhYj33n9j3fMztFxnh
LiberapayVisit NewPipe at liberapay.comDonate via Liberapay
BountysourceVisit NewPipe at bountysource.comCheck out how many bounties you can earn.
## Privacy Policy -The FoxPipe project aims to provide a private, anonymous experience for using media web services. -Therefore, the app does not collect any data without your consent. +The NewPipe project aims to provide a private, anonymous experience for using media web services. +Therefore, the app does not collect any data without your consent. NewPipe's privacy policy explains in detail what data is sent and stored when you send a crash report, or comment in our blog. You can find the document [here](https://newpipe.net/legal/privacy/). ## License [![GNU GPLv3 Image](https://www.gnu.org/graphics/gplv3-127x51.png)](https://www.gnu.org/licenses/gpl-3.0.en.html) @@ -110,4 +136,4 @@ NewPipe is Free Software: You can use, study share and improve it at your will. Specifically you can redistribute and/or modify it under the terms of the [GNU General Public License](https://www.gnu.org/licenses/gpl.html) as published by the Free Software Foundation, either version 3 of the License, or -(at your option) any later version. \ No newline at end of file +(at your option) any later version. diff --git a/app/build.gradle b/app/build.gradle index 7827edd23..2bb1b2402 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -2,19 +2,21 @@ apply plugin: 'com.android.application' apply plugin: 'kotlin-android' apply plugin: 'kotlin-android-extensions' apply plugin: 'kotlin-kapt' -apply plugin: 'checkstyle' android { compileSdkVersion 29 - buildToolsVersion '29.0.3' + buildToolsVersion '30.0.3' defaultConfig { applicationId 'org.schabi.newpipelegacy' resValue "string", "app_name", "NewPipe Legacy" minSdkVersion 16 + //noinspection ExpiredTargetSdkVersion targetSdkVersion 29 - versionCode 90 - versionName "0.19.8" + versionCode 110 + versionName "0.19.9.4" + + multiDexEnabled true testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" vectorDrawables.useSupportLibrary = true @@ -28,12 +30,11 @@ android { buildTypes { debug { - multiDexEnabled true debuggable true // suffix the app id and the app name with git branch name def workingBranch = getGitWorkingBranch() - def normalizedWorkingBranch = workingBranch.replaceAll("[^A-Za-z]+", "").toLowerCase() + def normalizedWorkingBranch = workingBranch.replaceFirst("^[^A-Za-z]+", "").replaceAll("[^0-9A-Za-z]+", "") if (normalizedWorkingBranch.isEmpty() || workingBranch == "master" || workingBranch == "dev") { // default values when branch name could not be determined or is master or dev applicationIdSuffix ".debug" @@ -50,7 +51,7 @@ android { // TODO: update Gradle version release { minifyEnabled true - shrinkResources false // disabled to fix F-Droid's reproducible build + shrinkResources true // could be disabled to fix F-Droid's reproducible build proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' archivesBaseName = 'app' } @@ -61,14 +62,24 @@ android { // Or, if you prefer, you can continue to check for errors in release builds, // but continue the build even when errors are found: abortOnError false + // suppress false warning ("Resource IDs will be non-final in Android Gradle Plugin version + // 5.0, avoid using them in switch case statements"), which affects only library projects + disable 'NonConstantResourceId' } compileOptions { + // Flag to enable support for the new language APIs + coreLibraryDesugaringEnabled true + sourceCompatibility JavaVersion.VERSION_1_8 targetCompatibility JavaVersion.VERSION_1_8 encoding 'utf-8' } + kotlinOptions { + jvmTarget = JavaVersion.VERSION_1_8 + } + // Required and used only by groupie androidExtensions { experimental = true @@ -81,132 +92,146 @@ android { ext { icepickVersion = '3.2.0' - checkstyleVersion = '8.32' stethoVersion = '1.5.1' - leakCanaryVersion = '2.2' - exoPlayerVersion = '2.11.6' + leakCanaryVersion = '2.5' + exoPlayerVersion = '2.11.8' androidxLifecycleVersion = '2.2.0' androidxRoomVersion = '2.2.5' - groupieVersion = '2.8.0' - markwonVersion = '4.3.1' + groupieVersion = '2.8.1' + markwonVersion = '4.6.0' + googleAutoServiceVersion = '1.0-rc7' } configurations { - checkstyle ktlint } -checkstyle { - configFile rootProject.file('checkstyle.xml') - ignoreFailures false - showViolations true - toolVersion = checkstyleVersion -} - -task runCheckstyle(type: Checkstyle) { - source 'src' - include '**/*.java' - exclude '**/gen/**' - exclude '**/R.java' - exclude '**/BuildConfig.java' - exclude 'main/java/us/shandian/giga/**' - - classpath = configurations.checkstyle - - showViolations true - - reports { - xml.enabled true - html.enabled true - } -} +def outputDir = "${project.buildDir}/reports/ktlint/" +def inputFiles = project.fileTree(dir: "src", include: "**/*.kt") task runKtlint(type: JavaExec) { + inputs.files(inputFiles) + outputs.dir(outputDir) main = "com.pinterest.ktlint.Main" classpath = configurations.ktlint args "src/**/*.kt" } task formatKtlint(type: JavaExec) { + inputs.files(inputFiles) + outputs.dir(outputDir) main = "com.pinterest.ktlint.Main" classpath = configurations.ktlint args "-F", "src/**/*.kt" } afterEvaluate { - preDebugBuild.dependsOn runCheckstyle, runKtlint + if (!System.properties.containsKey('skipFormatKtlint')) { + preDebugBuild.dependsOn formatKtlint + } + preDebugBuild.dependsOn runKtlint } dependencies { - implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" - - implementation "frankiesardo:icepick:${icepickVersion}" - kapt "frankiesardo:icepick-processor:${icepickVersion}" +/** Desugaring **/ + coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.1.5' - checkstyle "com.puppycrawl.tools:checkstyle:${checkstyleVersion}" - ktlint "com.pinterest:ktlint:0.35.0" - - debugImplementation "com.facebook.stetho:stetho:${stethoVersion}" - debugImplementation "com.facebook.stetho:stetho-okhttp3:${stethoVersion}" - - debugImplementation "com.squareup.leakcanary:leakcanary-android:${leakCanaryVersion}" - implementation "com.squareup.leakcanary:leakcanary-object-watcher-android:${leakCanaryVersion}" - - debugImplementation "androidx.multidex:multidex:2.0.1" - - testImplementation 'junit:junit:4.13' - testImplementation 'org.mockito:mockito-core:3.3.3' - - androidTestImplementation "androidx.test.ext:junit:1.1.1" - androidTestImplementation "androidx.room:room-testing:${androidxRoomVersion}" - androidTestImplementation "androidx.test.espresso:espresso-core:3.2.0", { - exclude module: 'support-annotations' - } - - implementation 'com.github.TeamNewPipe:NewPipeExtractor:5ac80624a40f4c600ae493e66881b5bf008f0ddb' - - implementation "com.github.TeamNewPipe:nanojson:1d9e1aea9049fc9f85e68b43ba39fe7be1c1f751" - implementation "org.jsoup:jsoup:1.13.1" +/** Ktlint **/ + ktlint "com.pinterest:ktlint:0.40.0" - implementation "com.squareup.okhttp3:okhttp:3.12.11" +/** Kotlin **/ + implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version" - implementation "com.google.android.exoplayer:exoplayer:${exoPlayerVersion}" - implementation "com.google.android.exoplayer:extension-mediasession:${exoPlayerVersion}" - - implementation "com.google.android.material:material:1.1.0" - - implementation "androidx.appcompat:appcompat:1.1.0" - implementation "androidx.preference:preference:1.1.1" - implementation "androidx.recyclerview:recyclerview:1.1.0" +/** AndroidX **/ + implementation "androidx.appcompat:appcompat:1.1.0" //no change implementation "androidx.cardview:cardview:1.0.0" implementation "androidx.constraintlayout:constraintlayout:1.1.3" - implementation "androidx.lifecycle:lifecycle-livedata:${androidxLifecycleVersion}" implementation "androidx.lifecycle:lifecycle-viewmodel:${androidxLifecycleVersion}" implementation "androidx.lifecycle:lifecycle-extensions:${androidxLifecycleVersion}" - + implementation 'androidx.media:media:1.2.1' + implementation "androidx.multidex:multidex:2.0.1" + implementation "androidx.preference:preference:1.1.1" + implementation "androidx.recyclerview:recyclerview:1.1.0" implementation "androidx.room:room-runtime:${androidxRoomVersion}" implementation "androidx.room:room-rxjava2:${androidxRoomVersion}" kapt "androidx.room:room-compiler:${androidxRoomVersion}" + implementation "androidx.swiperefreshlayout:swiperefreshlayout:1.1.0" + implementation "com.google.android.material:material:1.2.1" + +/** NewPipe libraries **/ + // You can use a local version by uncommenting a few lines in settings.gradle + // Or you can use a commit you pushed to GitHub by just replacing TeamNewPipe with your GitHub + // 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.ShareASmile:NewPipeExtractor:4aea925d97' + +/** Third-party libraries **/ + // Instance state boilerplate elimination + implementation "frankiesardo:icepick:${icepickVersion}" + kapt "frankiesardo:icepick-processor:${icepickVersion}" + + // HTML parser + implementation "org.jsoup:jsoup:1.13.1" + + // HTTP client + //noinspection GradleDependency --> do not update okhttp beyond 3.12.x to keep supporting Android 4.4 & Lower users + implementation "com.squareup.okhttp3:okhttp:3.12.13" + // Media player + implementation "com.google.android.exoplayer:exoplayer:${exoPlayerVersion}" + implementation "com.google.android.exoplayer:extension-mediasession:${exoPlayerVersion}" + + // Metadata generator for service descriptors + compileOnly "com.google.auto.service:auto-service-annotations:${googleAutoServiceVersion}" + kapt "com.google.auto.service:auto-service:${googleAutoServiceVersion}" + + // Manager for complex RecyclerView layouts implementation "com.xwray:groupie:${groupieVersion}" implementation "com.xwray:groupie-kotlin-android-extensions:${groupieVersion}" + // Circular ImageView implementation "de.hdodenhof:circleimageview:3.1.0" + // Image loading implementation "com.nostra13.universalimageloader:universal-image-loader:1.9.5" + // Markdown library for Android implementation "io.noties.markwon:core:${markwonVersion}" implementation "io.noties.markwon:linkify:${markwonVersion}" + // File picker implementation "com.nononsenseapps:filepicker:4.2.1" + // Crash reporting implementation "ch.acra:acra-core:5.5.0" + // Reactive extensions for Java VM implementation "io.reactivex.rxjava2:rxjava:2.2.19" implementation "io.reactivex.rxjava2:rxandroid:2.1.1" + // RxJava binding APIs for Android UI widgets implementation "com.jakewharton.rxbinding2:rxbinding:2.2.0" - implementation "org.ocpsoft.prettytime:prettytime:4.0.5.Final" + // Date and time formatting + implementation "org.ocpsoft.prettytime:prettytime:5.0.7.Final" + +/** Debugging **/ + // Memory leak detection + debugImplementation "com.squareup.leakcanary:leakcanary-android:${leakCanaryVersion}" + implementation "com.squareup.leakcanary:leakcanary-object-watcher-android:${leakCanaryVersion}" + // Debug bridge for Android + debugImplementation "com.facebook.stetho:stetho:${stethoVersion}" + debugImplementation "com.facebook.stetho:stetho-okhttp3:${stethoVersion}" + +/** Testing **/ + testImplementation 'junit:junit:4.13.2' + testImplementation 'org.mockito:mockito-core:3.6.0' + + androidTestImplementation "androidx.test.ext:junit:1.1.2" + androidTestImplementation "androidx.room:room-testing:${androidxRoomVersion}" + androidTestImplementation "androidx.test.espresso:espresso-core:3.3.0", { + exclude module: 'support-annotations' + } } static String getGitWorkingBranch() { diff --git a/app/src/androidTest/java/org/schabi/newpipelegacy/database/AppDatabaseTest.kt b/app/src/androidTest/java/org/schabi/newpipelegacy/database/AppDatabaseTest.kt index 442257fa7..ed3a0f2e0 100644 --- a/app/src/androidTest/java/org/schabi/newpipelegacy/database/AppDatabaseTest.kt +++ b/app/src/androidTest/java/org/schabi/newpipelegacy/database/AppDatabaseTest.kt @@ -31,49 +31,62 @@ class AppDatabaseTest { } @get:Rule - val testHelper = MigrationTestHelper(InstrumentationRegistry.getInstrumentation(), - AppDatabase::class.java.canonicalName, FrameworkSQLiteOpenHelperFactory()) + 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) - }) + 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) + testHelper.runMigrationsAndValidate( + AppDatabase.DATABASE_NAME, Migrations.DB_VER_3, + true, Migrations.MIGRATION_2_3 + ) val migratedDatabaseV3 = getMigratedDatabase() val listFromDB = migratedDatabaseV3.streamDAO().all.blockingFirst() @@ -110,9 +123,11 @@ class AppDatabaseTest { } private fun getMigratedDatabase(): AppDatabase { - val database: AppDatabase = Room.databaseBuilder(ApplicationProvider.getApplicationContext(), - AppDatabase::class.java, AppDatabase.DATABASE_NAME) - .build() + val database: AppDatabase = Room.databaseBuilder( + ApplicationProvider.getApplicationContext(), + AppDatabase::class.java, AppDatabase.DATABASE_NAME + ) + .build() testHelper.closeWhenFinished(database) return database } diff --git a/app/src/debug/java/org/schabi/newpipelegacy/DebugApp.kt b/app/src/debug/java/org/schabi/newpipelegacy/DebugApp.kt index 9ae06a1f4..eaf3cf0dc 100644 --- a/app/src/debug/java/org/schabi/newpipelegacy/DebugApp.kt +++ b/app/src/debug/java/org/schabi/newpipelegacy/DebugApp.kt @@ -1,6 +1,5 @@ package org.schabi.newpipelegacy -import android.content.Context import androidx.multidex.MultiDex import androidx.preference.PreferenceManager import com.facebook.stetho.Stetho @@ -11,29 +10,38 @@ import okhttp3.OkHttpClient import org.schabi.newpipe.extractor.downloader.Downloader class DebugApp : App() { - override fun attachBaseContext(base: Context) { - super.attachBaseContext(base) - MultiDex.install(this) - } - override fun onCreate() { super.onCreate() initStetho() // Give each object 10 seconds to be GC'ed, before LeakCanary gets nosy on it AppWatcher.config = AppWatcher.config.copy(watchDurationMillis = 10000) - LeakCanary.config = LeakCanary.config.copy(dumpHeap = PreferenceManager - .getDefaultSharedPreferences(this).getBoolean(getString( - R.string.allow_heap_dumping_key), false)) + LeakCanary.config = LeakCanary.config.copy( + dumpHeap = PreferenceManager + .getDefaultSharedPreferences(this).getBoolean( + getString( + R.string.allow_heap_dumping_key + ), + false + ) + ) } override fun getDownloader(): Downloader { - val downloader = DownloaderImpl.init(OkHttpClient.Builder() - .addNetworkInterceptor(StethoInterceptor())) + val downloader = DownloaderImpl.init( + OkHttpClient.Builder() + .addNetworkInterceptor(StethoInterceptor()) + ) setCookiesToDownloader(downloader) return downloader } + override fun initACRA() { + // install MultiDex before initializing ACRA + MultiDex.install(this) + super.initACRA() + } + private fun initStetho() { // Create an InitializerBuilder val initializerBuilder = Stetho.newInitializerBuilder(this) @@ -43,7 +51,8 @@ class DebugApp : App() { // Enable command line interface initializerBuilder.enableDumpapp( - Stetho.defaultDumperPluginsProvider(applicationContext)) + Stetho.defaultDumperPluginsProvider(applicationContext) + ) // Use the InitializerBuilder to generate an Initializer val initializer = initializerBuilder.build() @@ -54,6 +63,6 @@ class DebugApp : App() { override fun isDisposedRxExceptionsReported(): Boolean { return PreferenceManager.getDefaultSharedPreferences(this) - .getBoolean(getString(R.string.allow_disposed_exceptions_key), false) + .getBoolean(getString(R.string.allow_disposed_exceptions_key), false) } } diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 7306e2348..f27ea136d 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -1,7 +1,8 @@ + package="org.schabi.newpipelegacy" + android:installLocation="auto"> @@ -20,17 +21,20 @@ android:banner="@mipmap/newpipe_tv_banner" android:icon="@mipmap/ic_launcher" android:label="@string/app_name" + android:largeHeap="true" android:logo="@mipmap/ic_launcher" + android:networkSecurityConfig="@xml/network_security_config" android:requestLegacyExternalStorage="true" android:theme="@style/OpeningTheme" + android:resizeableActivity="true" tools:ignore="AllowBackup"> - @@ -160,6 +164,8 @@ + + @@ -238,23 +244,13 @@ - - - - - - - - - - - - - - - + + + + + @@ -282,7 +278,7 @@ - + @@ -313,26 +309,62 @@ + - - - - - + - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/assets/gpl_2.html b/app/src/main/assets/gpl_2.html deleted file mode 100644 index 0e1b8827e..000000000 --- a/app/src/main/assets/gpl_2.html +++ /dev/null @@ -1,400 +0,0 @@ - - - - - - GNU General Public License v2.0 - GNU Project - Free Software Foundation (FSF) - - - -

GNU GENERAL PUBLIC LICENSE

-

-Version 2, June 1991 -

- -
-Copyright (C) 1989, 1991 Free Software Foundation, Inc.
-51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
-
-Everyone is permitted to copy and distribute verbatim copies -of this license document, but changing it is not allowed. -
- -

Preamble

- -

- The licenses for most software are designed to take away your -freedom to share and change it. By contrast, the GNU General Public -License is intended to guarantee your freedom to share and change free -software--to make sure the software is free for all its users. This -General Public License applies to most of the Free Software -Foundation's software and to any other program whose authors commit to -using it. (Some other Free Software Foundation software is covered by -the GNU Lesser General Public License instead.) You can apply it to -your programs, too. -

- -

- When we speak of free software, we are referring to freedom, not -price. Our General Public Licenses are designed to make sure that you -have the freedom to distribute copies of free software (and charge for -this service if you wish), that you receive source code or can get it -if you want it, that you can change the software or use pieces of it -in new free programs; and that you know you can do these things. -

- -

- To protect your rights, we need to make restrictions that forbid -anyone to deny you these rights or to ask you to surrender the rights. -These restrictions translate to certain responsibilities for you if you -distribute copies of the software, or if you modify it. -

- -

- For example, if you distribute copies of such a program, whether -gratis or for a fee, you must give the recipients all the rights that -you have. You must make sure that they, too, receive or can get the -source code. And you must show them these terms so they know their -rights. -

- -

- We protect your rights with two steps: (1) copyright the software, and -(2) offer you this license which gives you legal permission to copy, -distribute and/or modify the software. -

- -

- Also, for each author's protection and ours, we want to make certain -that everyone understands that there is no warranty for this free -software. If the software is modified by someone else and passed on, we -want its recipients to know that what they have is not the original, so -that any problems introduced by others will not reflect on the original -authors' reputations. -

- -

- Finally, any free program is threatened constantly by software -patents. We wish to avoid the danger that redistributors of a free -program will individually obtain patent licenses, in effect making the -program proprietary. To prevent this, we have made it clear that any -patent must be licensed for everyone's free use or not licensed at all. -

- -

- The precise terms and conditions for copying, distribution and -modification follow. -

- - -

TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION

- - -

-0. - This License applies to any program or other work which contains -a notice placed by the copyright holder saying it may be distributed -under the terms of this General Public License. The "Program", below, -refers to any such program or work, and a "work based on the Program" -means either the Program or any derivative work under copyright law: -that is to say, a work containing the Program or a portion of it, -either verbatim or with modifications and/or translated into another -language. (Hereinafter, translation is included without limitation in -the term "modification".) Each licensee is addressed as "you". -

- -

-Activities other than copying, distribution and modification are not -covered by this License; they are outside its scope. The act of -running the Program is not restricted, and the output from the Program -is covered only if its contents constitute a work based on the -Program (independent of having been made by running the Program). -Whether that is true depends on what the Program does. -

- -

-1. - You may copy and distribute verbatim copies of the Program's -source code as you receive it, in any medium, provided that you -conspicuously and appropriately publish on each copy an appropriate -copyright notice and disclaimer of warranty; keep intact all the -notices that refer to this License and to the absence of any warranty; -and give any other recipients of the Program a copy of this License -along with the Program. -

- -

-You may charge a fee for the physical act of transferring a copy, and -you may at your option offer warranty protection in exchange for a fee. -

- -

-2. - You may modify your copy or copies of the Program or any portion -of it, thus forming a work based on the Program, and copy and -distribute such modifications or work under the terms of Section 1 -above, provided that you also meet all of these conditions: -

- -
-
-
- a) - You must cause the modified files to carry prominent notices - stating that you changed the files and the date of any change. -
-
-
- b) - You must cause any work that you distribute or publish, that in - whole or in part contains or is derived from the Program or any - part thereof, to be licensed as a whole at no charge to all third - parties under the terms of this License. -
-
-
- c) - If the modified program normally reads commands interactively - when run, you must cause it, when started running for such - interactive use in the most ordinary way, to print or display an - announcement including an appropriate copyright notice and a - notice that there is no warranty (or else, saying that you provide - a warranty) and that users may redistribute the program under - these conditions, and telling the user how to view a copy of this - License. (Exception: if the Program itself is interactive but - does not normally print such an announcement, your work based on - the Program is not required to print an announcement.) -
-
- -

-These requirements apply to the modified work as a whole. If -identifiable sections of that work are not derived from the Program, -and can be reasonably considered independent and separate works in -themselves, then this License, and its terms, do not apply to those -sections when you distribute them as separate works. But when you -distribute the same sections as part of a whole which is a work based -on the Program, the distribution of the whole must be on the terms of -this License, whose permissions for other licensees extend to the -entire whole, and thus to each and every part regardless of who wrote it. -

- -

-Thus, it is not the intent of this section to claim rights or contest -your rights to work written entirely by you; rather, the intent is to -exercise the right to control the distribution of derivative or -collective works based on the Program. -

- -

-In addition, mere aggregation of another work not based on the Program -with the Program (or with a work based on the Program) on a volume of -a storage or distribution medium does not bring the other work under -the scope of this License. -

- -

-3. - You may copy and distribute the Program (or a work based on it, -under Section 2) in object code or executable form under the terms of -Sections 1 and 2 above provided that you also do one of the following: -

- - - - -
-
-
- a) - Accompany it with the complete corresponding machine-readable - source code, which must be distributed under the terms of Sections - 1 and 2 above on a medium customarily used for software interchange; or, -
-
-
- b) - Accompany it with a written offer, valid for at least three - years, to give any third party, for a charge no more than your - cost of physically performing source distribution, a complete - machine-readable copy of the corresponding source code, to be - distributed under the terms of Sections 1 and 2 above on a medium - customarily used for software interchange; or, -
-
-
- c) - Accompany it with the information you received as to the offer - to distribute corresponding source code. (This alternative is - allowed only for noncommercial distribution and only if you - received the program in object code or executable form with such - an offer, in accord with Subsection b above.) -
-
- -

-The source code for a work means the preferred form of the work for -making modifications to it. For an executable work, complete source -code means all the source code for all modules it contains, plus any -associated interface definition files, plus the scripts used to -control compilation and installation of the executable. However, as a -special exception, the source code distributed need not include -anything that is normally distributed (in either source or binary -form) with the major softwareComponents (compiler, kernel, and so on) of the -operating system on which the executable runs, unless that component -itself accompanies the executable. -

- -

-If distribution of executable or object code is made by offering -access to copy from a designated place, then offering equivalent -access to copy the source code from the same place counts as -distribution of the source code, even though third parties are not -compelled to copy the source along with the object code. -

- -

-4. - You may not copy, modify, sublicense, or distribute the Program -except as expressly provided under this License. Any attempt -otherwise to copy, modify, sublicense or distribute the Program is -void, and will automatically terminate your rights under this License. -However, parties who have received copies, or rights, from you under -this License will not have their licenses terminated so long as such -parties remain in full compliance. -

- -

-5. - You are not required to accept this License, since you have not -signed it. However, nothing else grants you permission to modify or -distribute the Program or its derivative works. These actions are -prohibited by law if you do not accept this License. Therefore, by -modifying or distributing the Program (or any work based on the -Program), you indicate your acceptance of this License to do so, and -all its terms and conditions for copying, distributing or modifying -the Program or works based on it. -

- -

-6. - Each time you redistribute the Program (or any work based on the -Program), the recipient automatically receives a license from the -original licensor to copy, distribute or modify the Program subject to -these terms and conditions. You may not impose any further -restrictions on the recipients' exercise of the rights granted herein. -You are not responsible for enforcing compliance by third parties to -this License. -

- -

-7. - If, as a consequence of a court judgment or allegation of patent -infringement or for any other reason (not limited to patent issues), -conditions are imposed on you (whether by court order, agreement or -otherwise) that contradict the conditions of this License, they do not -excuse you from the conditions of this License. If you cannot -distribute so as to satisfy simultaneously your obligations under this -License and any other pertinent obligations, then as a consequence you -may not distribute the Program at all. For example, if a patent -license would not permit royalty-free redistribution of the Program by -all those who receive copies directly or indirectly through you, then -the only way you could satisfy both it and this License would be to -refrain entirely from distribution of the Program. -

- -

-If any portion of this section is held invalid or unenforceable under -any particular circumstance, the balance of the section is intended to -apply and the section as a whole is intended to apply in other -circumstances. -

- -

-It is not the purpose of this section to induce you to infringe any -patents or other property right claims or to contest validity of any -such claims; this section has the sole purpose of protecting the -integrity of the free software distribution system, which is -implemented by public license practices. Many people have made -generous contributions to the wide range of software distributed -through that system in reliance on consistent application of that -system; it is up to the author/donor to decide if he or she is willing -to distribute software through any other system and a licensee cannot -impose that choice. -

- -

-This section is intended to make thoroughly clear what is believed to -be a consequence of the rest of this License. -

- -

-8. - If the distribution and/or use of the Program is restricted in -certain countries either by patents or by copyrighted interfaces, the -original copyright holder who places the Program under this License -may add an explicit geographical distribution limitation excluding -those countries, so that distribution is permitted only in or among -countries not thus excluded. In such case, this License incorporates -the limitation as if written in the body of this License. -

- -

-9. - The Free Software Foundation may publish revised and/or new versions -of the General Public License from time to time. Such new versions will -be similar in spirit to the present version, but may differ in detail to -address new problems or concerns. -

- -

-Each version is given a distinguishing version number. If the Program -specifies a version number of this License which applies to it and "any -later version", you have the option of following the terms and conditions -either of that version or of any later version published by the Free -Software Foundation. If the Program does not specify a version number of -this License, you may choose any version ever published by the Free Software -Foundation. -

- -

-10. - If you wish to incorporate parts of the Program into other free -programs whose distribution conditions are different, write to the author -to ask for permission. For software which is copyrighted by the Free -Software Foundation, write to the Free Software Foundation; we sometimes -make exceptions for this. Our decision will be guided by the two goals -of preserving the free status of all derivatives of our free software and -of promoting the sharing and reuse of software generally. -

- -

NO WARRANTY

- -

-11. - BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY -FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN -OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES -PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED -OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF -MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS -TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE -PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, -REPAIR OR CORRECTION. -

- -

-12. - IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING -WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR -REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, -INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING -OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED -TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY -YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER -PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE -POSSIBILITY OF SUCH DAMAGES. -

- diff --git a/app/src/main/java/org/schabi/newpipelegacy/App.java b/app/src/main/java/org/schabi/newpipelegacy/App.java index ef5fc504c..7ee2005e0 100644 --- a/app/src/main/java/org/schabi/newpipelegacy/App.java +++ b/app/src/main/java/org/schabi/newpipelegacy/App.java @@ -1,7 +1,6 @@ package org.schabi.newpipelegacy; import android.annotation.TargetApi; -import android.app.Application; import android.app.NotificationChannel; import android.app.NotificationManager; import android.content.Context; @@ -10,6 +9,7 @@ import android.util.Log; import androidx.annotation.NonNull; +import androidx.multidex.MultiDexApplication; import androidx.preference.PreferenceManager; import com.nostra13.universalimageloader.cache.memory.impl.LRULimitedMemoryCache; @@ -63,12 +63,10 @@ * along with NewPipe. If not, see . */ -public class App extends Application { +public class App extends MultiDexApplication { protected static final String TAG = App.class.toString(); - @SuppressWarnings("unchecked") - private static final Class[] - REPORT_SENDER_FACTORY_CLASSES = new Class[]{AcraReportSenderFactory.class}; private static App app; + public static final String PACKAGE_NAME = BuildConfig.APPLICATION_ID; public static App getApp() { return app; @@ -77,7 +75,6 @@ public static App getApp() { @Override protected void attachBaseContext(final Context base) { super.attachBaseContext(base); - initACRA(); } @@ -106,7 +103,7 @@ public void onCreate() { configureRxJavaErrorHandler(); // Check for new version - new CheckForNewAppVersionTask().execute(); + //new CheckForNewAppVersionTask().execute(); } protected Downloader getDownloader() { @@ -119,7 +116,7 @@ protected void setCookiesToDownloader(final DownloaderImpl downloader) { final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences( getApplicationContext()); final String key = getApplicationContext().getString(R.string.recaptcha_cookies_key); - downloader.setCookie(ReCaptchaActivity.RECAPTCHA_COOKIES_KEY, prefs.getString(key, "")); + downloader.setCookie(ReCaptchaActivity.RECAPTCHA_COOKIES_KEY, prefs.getString(key, null)); downloader.updateYoutubeRestrictedModeCookies(getApplicationContext()); } @@ -200,10 +197,13 @@ private ImageLoaderConfiguration getImageLoaderConfigurations(final int memoryCa .build(); } - private void initACRA() { + /** + * Called in {@link #attachBaseContext(Context)} after calling the {@code super} method. + * Should be overridden if MultiDex is enabled, since it has to be initialized before ACRA. + */ + protected void initACRA() { try { final CoreConfiguration acraConfig = new CoreConfigurationBuilder(this) - .setReportSenderFactoryClasses(REPORT_SENDER_FACTORY_CLASSES) .setBuildConfigClass(BuildConfig.class) .build(); ACRA.init(this, acraConfig); diff --git a/app/src/main/java/org/schabi/newpipelegacy/DownloaderImpl.java b/app/src/main/java/org/schabi/newpipelegacy/DownloaderImpl.java index 4459f88e0..41774fa0c 100644 --- a/app/src/main/java/org/schabi/newpipelegacy/DownloaderImpl.java +++ b/app/src/main/java/org/schabi/newpipelegacy/DownloaderImpl.java @@ -43,7 +43,7 @@ public final class DownloaderImpl extends Downloader { public static final String USER_AGENT - = "Mozilla/5.0 (Windows NT 10.0; WOW64; rv:68.0) Gecko/20100101 Firefox/68.0"; + = "Mozilla/5.0 (Windows NT 10.0; rv:91.0) Gecko/20100101 Firefox/91.0"; public static final String YOUTUBE_RESTRICTED_MODE_COOKIE_KEY = "youtube_restricted_mode_key"; public static final String YOUTUBE_RESTRICTED_MODE_COOKIE = "PREF=f2=8000000"; diff --git a/app/src/main/java/org/schabi/newpipelegacy/MainActivity.java b/app/src/main/java/org/schabi/newpipelegacy/MainActivity.java index ca8480f08..7c7aee197 100644 --- a/app/src/main/java/org/schabi/newpipelegacy/MainActivity.java +++ b/app/src/main/java/org/schabi/newpipelegacy/MainActivity.java @@ -99,6 +99,7 @@ public class MainActivity extends AppCompatActivity { private static final int ITEM_ID_BOOKMARKS = -3; private static final int ITEM_ID_DOWNLOADS = -4; private static final int ITEM_ID_HISTORY = -5; + private static final int ITEM_ID_BG_PLAYER = -6; private static final int ITEM_ID_SETTINGS = 0; private static final int ITEM_ID_ABOUT = 1; @@ -116,7 +117,7 @@ protected void onCreate(final Bundle savedInstanceState) { } // enable TLS1.1/1.2 for jelly bean and kitkat devices, to fix download and play for - // mediaCCC sources + // media.ccc.de sources if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.KITKAT) { TLSSocketFactoryCompat.setAsDefault(); } @@ -179,6 +180,9 @@ private void setupDrawer() throws Exception { drawerItems.getMenu() .add(R.id.menu_tabs_group, ITEM_ID_HISTORY, ORDER, R.string.action_history) .setIcon(ThemeHelper.resolveResourceIdFromAttr(this, R.attr.ic_history)); + drawerItems.getMenu() + .add(R.id.menu_tabs_group, ITEM_ID_BG_PLAYER, ORDER, R.string.background_player) + .setIcon(ThemeHelper.resolveResourceIdFromAttr(this, R.attr.ic_play_arrow)); //Settings and About drawerItems.getMenu() @@ -263,6 +267,9 @@ private void tabSelected(final MenuItem item) throws ExtractionException { case ITEM_ID_HISTORY: NavigationHelper.openStatisticFragment(getSupportFragmentManager()); break; + case ITEM_ID_BG_PLAYER: + NavigationHelper.openBackgroundPlayer(this); + break; default: int currentServiceId = ServiceHelper.getSelectedServiceId(this); StreamingService service = NewPipe.getService(currentServiceId); diff --git a/app/src/main/java/org/schabi/newpipelegacy/ReCaptchaActivity.java b/app/src/main/java/org/schabi/newpipelegacy/ReCaptchaActivity.java index 8b3289f00..156367c47 100644 --- a/app/src/main/java/org/schabi/newpipelegacy/ReCaptchaActivity.java +++ b/app/src/main/java/org/schabi/newpipelegacy/ReCaptchaActivity.java @@ -53,6 +53,16 @@ public class ReCaptchaActivity extends AppCompatActivity { public static final String YT_URL = "https://www.youtube.com"; public static final String RECAPTCHA_COOKIES_KEY = "recaptcha_cookies"; + public static String sanitizeRecaptchaUrl(@Nullable final String url) { + if (url == null || url.trim().isEmpty()) { + return YT_URL; // YouTube is the most likely service to have thrown a recaptcha + } else { + // remove "pbj=1" parameter from YouYube urls, as it makes the page JSON and not HTML + return url.replace("&pbj=1", "").replace("pbj=1&", "").replace("?pbj=1", ""); + } + } + + private WebView webView; private String foundCookies = ""; @@ -64,20 +74,16 @@ protected void onCreate(final Bundle savedInstanceState) { Toolbar toolbar = findViewById(R.id.toolbar); setSupportActionBar(toolbar); - String url = getIntent().getStringExtra(RECAPTCHA_URL_EXTRA); - if (url == null || url.isEmpty()) { - url = YT_URL; - } - + final String url = sanitizeRecaptchaUrl(getIntent().getStringExtra(RECAPTCHA_URL_EXTRA)); // set return to Cancel by default setResult(RESULT_CANCELED); - webView = findViewById(R.id.reCaptchaWebView); // enable Javascript WebSettings webSettings = webView.getSettings(); webSettings.setJavaScriptEnabled(true); + webSettings.setUserAgentString(DownloaderImpl.USER_AGENT); webView.setWebViewClient(new WebViewClient() { @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP) @@ -115,8 +121,7 @@ public void onPageFinished(final WebView view, final String url) { webView.clearHistory(); android.webkit.CookieManager cookieManager = CookieManager.getInstance(); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { - cookieManager.removeAllCookies(aBoolean -> { - }); + cookieManager.removeAllCookies(value -> { }); } else { cookieManager.removeAllCookie(); } diff --git a/app/src/main/java/org/schabi/newpipelegacy/RouterActivity.java b/app/src/main/java/org/schabi/newpipelegacy/RouterActivity.java index b5f5e6137..c33f27a5b 100644 --- a/app/src/main/java/org/schabi/newpipelegacy/RouterActivity.java +++ b/app/src/main/java/org/schabi/newpipelegacy/RouterActivity.java @@ -50,6 +50,7 @@ import org.schabi.newpipelegacy.util.ListHelper; import org.schabi.newpipelegacy.util.NavigationHelper; import org.schabi.newpipelegacy.util.PermissionHelper; +import org.schabi.newpipelegacy.util.ShareUtils; import org.schabi.newpipelegacy.util.ThemeHelper; import org.schabi.newpipelegacy.util.urlfinder.UrlFinder; import org.schabi.newpipelegacy.views.FocusOverlayView; @@ -159,27 +160,36 @@ private void handleUrl(final String url) { if (result) { onSuccess(); } else { - onError(); + showUnsupportedUrlDialog(url); } - }, this::handleError)); + }, throwable -> handleError(throwable, url))); } - private void handleError(final Throwable error) { - error.printStackTrace(); + private void handleError(final Throwable throwable, final String url) { + throwable.printStackTrace(); - if (error instanceof ExtractionException) { - Toast.makeText(this, R.string.url_not_supported_toast, Toast.LENGTH_LONG).show(); + if (throwable instanceof ExtractionException) { + showUnsupportedUrlDialog(url); } else { - ExtractorHelper.handleGeneralException(this, -1, null, error, + ExtractorHelper.handleGeneralException(this, -1, url, throwable, UserAction.SOMETHING_ELSE, null); + finish(); } - - finish(); } - private void onError() { - Toast.makeText(this, R.string.url_not_supported_toast, Toast.LENGTH_LONG).show(); - finish(); + private void showUnsupportedUrlDialog(final String url) { + final Context context = getThemeWrapperContext(); + new AlertDialog.Builder(context) + .setTitle(R.string.unsupported_url) + .setMessage(R.string.unsupported_url_dialog_message) + .setIcon(ThemeHelper.resolveResourceIdFromAttr(context, R.attr.ic_share)) + .setPositiveButton(R.string.open_in_browser, + (dialog, which) -> ShareUtils.openUrlInBrowser(this, url)) + .setNegativeButton(R.string.share, + (dialog, which) -> ShareUtils.shareUrl(this, "", url)) // no subject + .setNeutralButton(R.string.cancel, null) + .setOnDismissListener(dialog -> finish()) + .show(); } protected void onSuccess() { @@ -459,7 +469,7 @@ private void handleChoice(final String selectedChoiceKey) { startActivity(intent); finish(); - }, this::handleError) + }, throwable -> handleError(throwable, currentUrl)) ); return; } @@ -492,11 +502,9 @@ private void openDownloadDialog() { downloadDialog.setSelectedVideoStream(selectedVideoStreamIndex); downloadDialog.show(fm, "downloadDialog"); fm.executePendingTransactions(); - downloadDialog.getDialog().setOnDismissListener(dialog -> { - finish(); - }); + downloadDialog.getDialog().setOnDismissListener(dialog -> finish()); }, (@NonNull Throwable throwable) -> { - onError(); + showUnsupportedUrlDialog(currentUrl); }); } diff --git a/app/src/main/java/org/schabi/newpipelegacy/about/AboutActivity.java b/app/src/main/java/org/schabi/newpipelegacy/about/AboutActivity.java index 0fe60c53c..ba7ebbf27 100644 --- a/app/src/main/java/org/schabi/newpipelegacy/about/AboutActivity.java +++ b/app/src/main/java/org/schabi/newpipelegacy/about/AboutActivity.java @@ -32,7 +32,7 @@ public class AboutActivity extends AppCompatActivity { */ private static final SoftwareComponent[] SOFTWARE_COMPONENTS = new SoftwareComponent[]{ new SoftwareComponent("Giga Get", "2014 - 2015", "Peter Cai", - "https://github.com/PaperAirplane-Dev-Team/GigaGet", StandardLicenses.GPL2), + "https://github.com/PaperAirplane-Dev-Team/GigaGet", StandardLicenses.GPL3), new SoftwareComponent("NewPipe Extractor", "2017 - 2020", "Christian Schabesberger", "https://github.com/TeamNewPipe/NewPipeExtractor", StandardLicenses.GPL3), new SoftwareComponent("Jsoup", "2017", "Jonathan Hedley", diff --git a/app/src/main/java/org/schabi/newpipelegacy/about/License.java b/app/src/main/java/org/schabi/newpipelegacy/about/License.java index 38138a061..fe79cddbf 100644 --- a/app/src/main/java/org/schabi/newpipelegacy/about/License.java +++ b/app/src/main/java/org/schabi/newpipelegacy/about/License.java @@ -46,10 +46,10 @@ protected License(final Parcel in) { public Uri getContentUri() { return new Uri.Builder() - .scheme("file") - .path("/android_asset") - .appendPath(filename) - .build(); + .scheme("file") + .path("/android_asset") + .appendPath(filename) + .build(); } public String getAbbreviation() { diff --git a/app/src/main/java/org/schabi/newpipelegacy/about/LicenseFragmentHelper.java b/app/src/main/java/org/schabi/newpipelegacy/about/LicenseFragmentHelper.java index b1c9bf558..fb4dd6217 100644 --- a/app/src/main/java/org/schabi/newpipelegacy/about/LicenseFragmentHelper.java +++ b/app/src/main/java/org/schabi/newpipelegacy/about/LicenseFragmentHelper.java @@ -16,8 +16,8 @@ import java.io.BufferedReader; import java.io.IOException; import java.io.InputStreamReader; +import java.io.UnsupportedEncodingException; import java.lang.ref.WeakReference; -import java.nio.charset.StandardCharsets; import static org.schabi.newpipelegacy.util.Localization.assureCorrectAppLanguage; @@ -41,7 +41,7 @@ private static String getFormattedLicense(@NonNull final Context context, final String webViewData; try { final BufferedReader in = new BufferedReader(new InputStreamReader( - context.getAssets().open(license.getFilename()), StandardCharsets.UTF_8)); + context.getAssets().open(license.getFilename()), "utf-8")); String str; while ((str = in.readLine()) != null) { licenseContent.append(str); @@ -50,10 +50,10 @@ private static String getFormattedLicense(@NonNull final Context context, // split the HTML file and insert the stylesheet into the HEAD of the file webViewData = licenseContent.toString().replace("", - ""); + ""); } catch (IOException e) { throw new IllegalArgumentException( - "Could not get license file: " + license.getFilename(), e); + "Could not get license file: " + license.getFilename(), e); } return webViewData; } @@ -65,16 +65,16 @@ private static String getFormattedLicense(@NonNull final Context context, private static String getLicenseStylesheet(final Context context) { final boolean isLightTheme = ThemeHelper.isLightThemeSelected(context); return "body{padding:12px 15px;margin:0;" - + "background:#" + getHexRGBColor(context, isLightTheme - ? R.color.light_license_background_color - : R.color.dark_license_background_color) + ";" - + "color:#" + getHexRGBColor(context, isLightTheme - ? R.color.light_license_text_color - : R.color.dark_license_text_color) + "}" - + "a[href]{color:#" + getHexRGBColor(context, isLightTheme - ? R.color.light_youtube_primary_color - : R.color.dark_youtube_primary_color) + "}" - + "pre{white-space:pre-wrap}"; + + "background:#" + getHexRGBColor(context, isLightTheme + ? R.color.light_license_background_color + : R.color.dark_license_background_color) + ";" + + "color:#" + getHexRGBColor(context, isLightTheme + ? R.color.light_license_text_color + : R.color.dark_license_text_color) + "}" + + "a[href]{color:#" + getHexRGBColor(context, isLightTheme + ? R.color.light_youtube_primary_color + : R.color.dark_youtube_primary_color) + "}" + + "pre{white-space:pre-wrap}"; } /** @@ -112,17 +112,23 @@ protected void onPostExecute(final Integer result) { return; } - final String webViewData = Base64.encodeToString(getFormattedLicense(activity, license) - .getBytes(StandardCharsets.UTF_8), Base64.NO_PADDING); - final WebView webView = new WebView(activity); - webView.loadData(webViewData, "text/html; charset=UTF-8", "base64"); - - final AlertDialog.Builder alert = new AlertDialog.Builder(activity); - alert.setTitle(license.getName()); - alert.setView(webView); - assureCorrectAppLanguage(activity); - alert.setNegativeButton(activity.getString(R.string.finish), + final String webViewData; + try { + webViewData = Base64.encodeToString(getFormattedLicense(activity, license) + .getBytes("utf-8"), Base64.NO_PADDING); + final WebView webView = new WebView(activity); + webView.loadData(webViewData, "text/html; charset=UTF-8", "base64"); + + final AlertDialog.Builder alert = new AlertDialog.Builder(activity); + alert.setTitle(license.getName()); + alert.setView(webView); + assureCorrectAppLanguage(activity); + alert.setNegativeButton(activity.getString(R.string.finish), (dialog, which) -> dialog.dismiss()); - alert.show(); + alert.show(); + } catch (UnsupportedEncodingException e) { + e.printStackTrace(); + } + } } diff --git a/app/src/main/java/org/schabi/newpipelegacy/about/StandardLicenses.java b/app/src/main/java/org/schabi/newpipelegacy/about/StandardLicenses.java index a548089dd..58cdcc5fa 100644 --- a/app/src/main/java/org/schabi/newpipelegacy/about/StandardLicenses.java +++ b/app/src/main/java/org/schabi/newpipelegacy/about/StandardLicenses.java @@ -4,8 +4,6 @@ * Class containing information about standard software licenses. */ public final class StandardLicenses { - public static final License GPL2 - = new License("GNU General Public License, Version 2.0", "GPLv2", "gpl_2.html"); public static final License GPL3 = new License("GNU General Public License, Version 3.0", "GPLv3", "gpl_3.html"); public static final License APACHE2 diff --git a/app/src/main/java/org/schabi/newpipelegacy/database/feed/dao/FeedDAO.kt b/app/src/main/java/org/schabi/newpipelegacy/database/feed/dao/FeedDAO.kt index 3a6efc671..ecc8288bf 100644 --- a/app/src/main/java/org/schabi/newpipelegacy/database/feed/dao/FeedDAO.kt +++ b/app/src/main/java/org/schabi/newpipelegacy/database/feed/dao/FeedDAO.kt @@ -7,18 +7,19 @@ import androidx.room.Query import androidx.room.Transaction import androidx.room.Update import io.reactivex.Flowable -import java.util.Date import org.schabi.newpipelegacy.database.feed.model.FeedEntity import org.schabi.newpipelegacy.database.feed.model.FeedLastUpdatedEntity import org.schabi.newpipelegacy.database.stream.model.StreamEntity import org.schabi.newpipelegacy.database.subscription.SubscriptionEntity +import java.util.Date @Dao abstract class FeedDAO { @Query("DELETE FROM feed") abstract fun deleteAll(): Int - @Query(""" + @Query( + """ SELECT s.* FROM streams s INNER JOIN feed f @@ -27,10 +28,12 @@ abstract class FeedDAO { ORDER BY s.upload_date IS NULL DESC, s.upload_date DESC, s.uploader ASC LIMIT 500 - """) + """ + ) abstract fun getAllStreams(): Flowable> - @Query(""" + @Query( + """ SELECT s.* FROM streams s INNER JOIN feed f @@ -46,10 +49,12 @@ abstract class FeedDAO { ORDER BY s.upload_date IS NULL DESC, s.upload_date DESC, s.uploader ASC LIMIT 500 - """) + """ + ) abstract fun getAllStreamsFromGroup(groupId: Long): Flowable> - @Query(""" + @Query( + """ DELETE FROM feed WHERE feed.stream_id IN ( @@ -60,10 +65,12 @@ abstract class FeedDAO { WHERE s.upload_date < :date ) - """) + """ + ) abstract fun unlinkStreamsOlderThan(date: Date) - @Query(""" + @Query( + """ DELETE FROM feed WHERE feed.subscription_id = :subscriptionId @@ -76,7 +83,8 @@ abstract class FeedDAO { WHERE s.stream_type = "LIVE_STREAM" OR s.stream_type = "AUDIO_LIVE_STREAM" ) - """) + """ + ) abstract fun unlinkOldLivestreams(subscriptionId: Long) @Insert(onConflict = OnConflictStrategy.IGNORE) @@ -100,12 +108,14 @@ abstract class FeedDAO { } } - @Query(""" + @Query( + """ SELECT MIN(lu.last_updated) FROM feed_last_updated lu INNER JOIN feed_group_subscription_join fgs ON fgs.subscription_id = lu.subscription_id AND fgs.group_id = :groupId - """) + """ + ) abstract fun oldestSubscriptionUpdate(groupId: Long): Flowable> @Query("SELECT MIN(last_updated) FROM feed_last_updated") @@ -114,7 +124,8 @@ abstract class FeedDAO { @Query("SELECT COUNT(*) FROM feed_last_updated WHERE last_updated IS NULL") abstract fun notLoadedCount(): Flowable - @Query(""" + @Query( + """ SELECT COUNT(*) FROM subscriptions s INNER JOIN feed_group_subscription_join fgs @@ -124,20 +135,24 @@ abstract class FeedDAO { ON s.uid = lu.subscription_id WHERE lu.last_updated IS NULL - """) + """ + ) abstract fun notLoadedCountForGroup(groupId: Long): Flowable - @Query(""" + @Query( + """ SELECT s.* FROM subscriptions s LEFT JOIN feed_last_updated lu ON s.uid = lu.subscription_id WHERE lu.last_updated IS NULL OR lu.last_updated < :outdatedThreshold - """) + """ + ) abstract fun getAllOutdated(outdatedThreshold: Date): Flowable> - @Query(""" + @Query( + """ SELECT s.* FROM subscriptions s INNER JOIN feed_group_subscription_join fgs @@ -147,6 +162,7 @@ abstract class FeedDAO { ON s.uid = lu.subscription_id WHERE lu.last_updated IS NULL OR lu.last_updated < :outdatedThreshold - """) + """ + ) abstract fun getAllOutdatedForGroup(groupId: Long, outdatedThreshold: Date): Flowable> } diff --git a/app/src/main/java/org/schabi/newpipelegacy/database/feed/model/FeedEntity.kt b/app/src/main/java/org/schabi/newpipelegacy/database/feed/model/FeedEntity.kt index 7271115e3..03fa728e7 100644 --- a/app/src/main/java/org/schabi/newpipelegacy/database/feed/model/FeedEntity.kt +++ b/app/src/main/java/org/schabi/newpipelegacy/database/feed/model/FeedEntity.kt @@ -10,21 +10,24 @@ import org.schabi.newpipelegacy.database.feed.model.FeedEntity.Companion.SUBSCRI import org.schabi.newpipelegacy.database.stream.model.StreamEntity import org.schabi.newpipelegacy.database.subscription.SubscriptionEntity -@Entity(tableName = FEED_TABLE, - primaryKeys = [STREAM_ID, SUBSCRIPTION_ID], - indices = [Index(SUBSCRIPTION_ID)], - foreignKeys = [ - ForeignKey( - entity = StreamEntity::class, - parentColumns = [StreamEntity.STREAM_ID], - childColumns = [STREAM_ID], - onDelete = ForeignKey.CASCADE, onUpdate = ForeignKey.CASCADE, deferred = true), - ForeignKey( - entity = SubscriptionEntity::class, - parentColumns = [SubscriptionEntity.SUBSCRIPTION_UID], - childColumns = [SUBSCRIPTION_ID], - onDelete = ForeignKey.CASCADE, onUpdate = ForeignKey.CASCADE, deferred = true) - ] +@Entity( + tableName = FEED_TABLE, + primaryKeys = [STREAM_ID, SUBSCRIPTION_ID], + indices = [Index(SUBSCRIPTION_ID)], + foreignKeys = [ + ForeignKey( + entity = StreamEntity::class, + parentColumns = [StreamEntity.STREAM_ID], + childColumns = [STREAM_ID], + onDelete = ForeignKey.CASCADE, onUpdate = ForeignKey.CASCADE, deferred = true + ), + ForeignKey( + entity = SubscriptionEntity::class, + parentColumns = [SubscriptionEntity.SUBSCRIPTION_UID], + childColumns = [SUBSCRIPTION_ID], + onDelete = ForeignKey.CASCADE, onUpdate = ForeignKey.CASCADE, deferred = true + ) + ] ) data class FeedEntity( @ColumnInfo(name = STREAM_ID) diff --git a/app/src/main/java/org/schabi/newpipelegacy/database/feed/model/FeedGroupEntity.kt b/app/src/main/java/org/schabi/newpipelegacy/database/feed/model/FeedGroupEntity.kt index 59559ff89..153564f96 100644 --- a/app/src/main/java/org/schabi/newpipelegacy/database/feed/model/FeedGroupEntity.kt +++ b/app/src/main/java/org/schabi/newpipelegacy/database/feed/model/FeedGroupEntity.kt @@ -9,8 +9,8 @@ import org.schabi.newpipelegacy.database.feed.model.FeedGroupEntity.Companion.SO import org.schabi.newpipelegacy.local.subscription.FeedGroupIcon @Entity( - tableName = FEED_GROUP_TABLE, - indices = [Index(SORT_ORDER)] + tableName = FEED_GROUP_TABLE, + indices = [Index(SORT_ORDER)] ) data class FeedGroupEntity( @PrimaryKey(autoGenerate = true) diff --git a/app/src/main/java/org/schabi/newpipelegacy/database/feed/model/FeedGroupSubscriptionEntity.kt b/app/src/main/java/org/schabi/newpipelegacy/database/feed/model/FeedGroupSubscriptionEntity.kt index 1b3d5b8f9..b9f38bdb1 100644 --- a/app/src/main/java/org/schabi/newpipelegacy/database/feed/model/FeedGroupSubscriptionEntity.kt +++ b/app/src/main/java/org/schabi/newpipelegacy/database/feed/model/FeedGroupSubscriptionEntity.kt @@ -11,22 +11,24 @@ import org.schabi.newpipelegacy.database.feed.model.FeedGroupSubscriptionEntity. import org.schabi.newpipelegacy.database.subscription.SubscriptionEntity @Entity( - tableName = FEED_GROUP_SUBSCRIPTION_TABLE, - primaryKeys = [GROUP_ID, SUBSCRIPTION_ID], - indices = [Index(SUBSCRIPTION_ID)], - foreignKeys = [ - ForeignKey( - entity = FeedGroupEntity::class, - parentColumns = [FeedGroupEntity.ID], - childColumns = [GROUP_ID], - onDelete = CASCADE, onUpdate = CASCADE, deferred = true), + tableName = FEED_GROUP_SUBSCRIPTION_TABLE, + primaryKeys = [GROUP_ID, SUBSCRIPTION_ID], + indices = [Index(SUBSCRIPTION_ID)], + foreignKeys = [ + ForeignKey( + entity = FeedGroupEntity::class, + parentColumns = [FeedGroupEntity.ID], + childColumns = [GROUP_ID], + onDelete = CASCADE, onUpdate = CASCADE, deferred = true + ), - ForeignKey( - entity = SubscriptionEntity::class, - parentColumns = [SubscriptionEntity.SUBSCRIPTION_UID], - childColumns = [SUBSCRIPTION_ID], - onDelete = CASCADE, onUpdate = CASCADE, deferred = true) - ] + ForeignKey( + entity = SubscriptionEntity::class, + parentColumns = [SubscriptionEntity.SUBSCRIPTION_UID], + childColumns = [SUBSCRIPTION_ID], + onDelete = CASCADE, onUpdate = CASCADE, deferred = true + ) + ] ) data class FeedGroupSubscriptionEntity( @ColumnInfo(name = GROUP_ID) diff --git a/app/src/main/java/org/schabi/newpipelegacy/database/feed/model/FeedLastUpdatedEntity.kt b/app/src/main/java/org/schabi/newpipelegacy/database/feed/model/FeedLastUpdatedEntity.kt index 842fa8633..c847f1e19 100644 --- a/app/src/main/java/org/schabi/newpipelegacy/database/feed/model/FeedLastUpdatedEntity.kt +++ b/app/src/main/java/org/schabi/newpipelegacy/database/feed/model/FeedLastUpdatedEntity.kt @@ -4,20 +4,21 @@ import androidx.room.ColumnInfo import androidx.room.Entity import androidx.room.ForeignKey import androidx.room.PrimaryKey -import java.util.Date import org.schabi.newpipelegacy.database.feed.model.FeedLastUpdatedEntity.Companion.FEED_LAST_UPDATED_TABLE import org.schabi.newpipelegacy.database.feed.model.FeedLastUpdatedEntity.Companion.SUBSCRIPTION_ID import org.schabi.newpipelegacy.database.subscription.SubscriptionEntity +import java.util.Date @Entity( - tableName = FEED_LAST_UPDATED_TABLE, - foreignKeys = [ - ForeignKey( - entity = SubscriptionEntity::class, - parentColumns = [SubscriptionEntity.SUBSCRIPTION_UID], - childColumns = [SUBSCRIPTION_ID], - onDelete = ForeignKey.CASCADE, onUpdate = ForeignKey.CASCADE, deferred = true) - ] + tableName = FEED_LAST_UPDATED_TABLE, + foreignKeys = [ + ForeignKey( + entity = SubscriptionEntity::class, + parentColumns = [SubscriptionEntity.SUBSCRIPTION_UID], + childColumns = [SUBSCRIPTION_ID], + onDelete = ForeignKey.CASCADE, onUpdate = ForeignKey.CASCADE, deferred = true + ) + ] ) data class FeedLastUpdatedEntity( @PrimaryKey diff --git a/app/src/main/java/org/schabi/newpipelegacy/database/history/dao/StreamHistoryDAO.java b/app/src/main/java/org/schabi/newpipelegacy/database/history/dao/StreamHistoryDAO.java index 2b48d0ddf..034001a0c 100644 --- a/app/src/main/java/org/schabi/newpipelegacy/database/history/dao/StreamHistoryDAO.java +++ b/app/src/main/java/org/schabi/newpipelegacy/database/history/dao/StreamHistoryDAO.java @@ -20,6 +20,9 @@ import static org.schabi.newpipelegacy.database.stream.StreamStatisticsEntry.STREAM_WATCH_COUNT; import static org.schabi.newpipelegacy.database.stream.model.StreamEntity.STREAM_ID; import static org.schabi.newpipelegacy.database.stream.model.StreamEntity.STREAM_TABLE; +import static org.schabi.newpipelegacy.database.stream.model.StreamStateEntity.JOIN_STREAM_ID_ALIAS; +import static org.schabi.newpipelegacy.database.stream.model.StreamStateEntity.STREAM_PROGRESS_TIME; +import static org.schabi.newpipelegacy.database.stream.model.StreamStateEntity.STREAM_STATE_TABLE; @Dao public abstract class StreamHistoryDAO implements HistoryDAO { @@ -73,6 +76,12 @@ public Flowable> listByService(final int serviceId) { + " SUM(" + STREAM_REPEAT_COUNT + ") AS " + STREAM_WATCH_COUNT + " FROM " + STREAM_HISTORY_TABLE + " GROUP BY " + JOIN_STREAM_ID + ")" - + " ON " + STREAM_ID + " = " + JOIN_STREAM_ID) + + " ON " + STREAM_ID + " = " + JOIN_STREAM_ID + + + " LEFT JOIN " + + "(SELECT " + JOIN_STREAM_ID + " AS " + JOIN_STREAM_ID_ALIAS + ", " + + STREAM_PROGRESS_TIME + + " FROM " + STREAM_STATE_TABLE + " )" + + " ON " + STREAM_ID + " = " + JOIN_STREAM_ID_ALIAS) public abstract Flowable> getStatistics(); } diff --git a/app/src/main/java/org/schabi/newpipelegacy/database/history/model/StreamHistoryEntry.kt b/app/src/main/java/org/schabi/newpipelegacy/database/history/model/StreamHistoryEntry.kt index 71fad2766..3b39abdee 100644 --- a/app/src/main/java/org/schabi/newpipelegacy/database/history/model/StreamHistoryEntry.kt +++ b/app/src/main/java/org/schabi/newpipelegacy/database/history/model/StreamHistoryEntry.kt @@ -2,8 +2,8 @@ package org.schabi.newpipelegacy.database.history.model import androidx.room.ColumnInfo import androidx.room.Embedded -import java.util.Date import org.schabi.newpipelegacy.database.stream.model.StreamEntity +import java.util.Date data class StreamHistoryEntry( @Embedded @@ -25,6 +25,6 @@ data class StreamHistoryEntry( fun hasEqualValues(other: StreamHistoryEntry): Boolean { return this.streamEntity.uid == other.streamEntity.uid && streamId == other.streamId && - accessDate.compareTo(other.accessDate) == 0 + accessDate.compareTo(other.accessDate) == 0 } } diff --git a/app/src/main/java/org/schabi/newpipelegacy/database/playlist/PlaylistStreamEntry.kt b/app/src/main/java/org/schabi/newpipelegacy/database/playlist/PlaylistStreamEntry.kt index 506012cfe..f190a1e88 100644 --- a/app/src/main/java/org/schabi/newpipelegacy/database/playlist/PlaylistStreamEntry.kt +++ b/app/src/main/java/org/schabi/newpipelegacy/database/playlist/PlaylistStreamEntry.kt @@ -6,11 +6,16 @@ import org.schabi.newpipe.extractor.stream.StreamInfoItem import org.schabi.newpipelegacy.database.LocalItem import org.schabi.newpipelegacy.database.playlist.model.PlaylistStreamEntity import org.schabi.newpipelegacy.database.stream.model.StreamEntity +import org.schabi.newpipelegacy.database.stream.model.StreamStateEntity +import kotlin.jvm.Throws -class PlaylistStreamEntry( +data class PlaylistStreamEntry( @Embedded val streamEntity: StreamEntity, + @ColumnInfo(name = StreamStateEntity.STREAM_PROGRESS_TIME, defaultValue = "0") + val progressTime: Long, + @ColumnInfo(name = PlaylistStreamEntity.JOIN_STREAM_ID) val streamId: Long, diff --git a/app/src/main/java/org/schabi/newpipelegacy/database/playlist/dao/PlaylistStreamDAO.java b/app/src/main/java/org/schabi/newpipelegacy/database/playlist/dao/PlaylistStreamDAO.java index 3d560cc7a..b099482e3 100644 --- a/app/src/main/java/org/schabi/newpipelegacy/database/playlist/dao/PlaylistStreamDAO.java +++ b/app/src/main/java/org/schabi/newpipelegacy/database/playlist/dao/PlaylistStreamDAO.java @@ -24,6 +24,9 @@ import static org.schabi.newpipelegacy.database.playlist.model.PlaylistStreamEntity.PLAYLIST_STREAM_JOIN_TABLE; import static org.schabi.newpipelegacy.database.stream.model.StreamEntity.STREAM_ID; import static org.schabi.newpipelegacy.database.stream.model.StreamEntity.STREAM_TABLE; +import static org.schabi.newpipelegacy.database.stream.model.StreamStateEntity.JOIN_STREAM_ID_ALIAS; +import static org.schabi.newpipelegacy.database.stream.model.StreamStateEntity.STREAM_PROGRESS_TIME; +import static org.schabi.newpipelegacy.database.stream.model.StreamStateEntity.STREAM_STATE_TABLE; @Dao public abstract class PlaylistStreamDAO implements BasicDAO { @@ -58,6 +61,13 @@ public Flowable> listByService(final int serviceId) { // then merge with the stream metadata + " ON " + STREAM_ID + " = " + JOIN_STREAM_ID + + + " LEFT JOIN " + + "(SELECT " + JOIN_STREAM_ID + " AS " + JOIN_STREAM_ID_ALIAS + ", " + + STREAM_PROGRESS_TIME + + " FROM " + STREAM_STATE_TABLE + " )" + + " ON " + STREAM_ID + " = " + JOIN_STREAM_ID_ALIAS + + " ORDER BY " + JOIN_INDEX + " ASC") public abstract Flowable> getOrderedStreamsOf(long playlistId); diff --git a/app/src/main/java/org/schabi/newpipelegacy/database/stream/StreamStatisticsEntry.kt b/app/src/main/java/org/schabi/newpipelegacy/database/stream/StreamStatisticsEntry.kt index 4c5427b68..feb79c543 100644 --- a/app/src/main/java/org/schabi/newpipelegacy/database/stream/StreamStatisticsEntry.kt +++ b/app/src/main/java/org/schabi/newpipelegacy/database/stream/StreamStatisticsEntry.kt @@ -2,16 +2,20 @@ package org.schabi.newpipelegacy.database.stream import androidx.room.ColumnInfo import androidx.room.Embedded -import java.util.Date import org.schabi.newpipe.extractor.stream.StreamInfoItem import org.schabi.newpipelegacy.database.LocalItem import org.schabi.newpipelegacy.database.history.model.StreamHistoryEntity import org.schabi.newpipelegacy.database.stream.model.StreamEntity +import org.schabi.newpipelegacy.database.stream.model.StreamStateEntity.STREAM_PROGRESS_TIME +import java.util.Date class StreamStatisticsEntry( @Embedded val streamEntity: StreamEntity, + @ColumnInfo(name = STREAM_PROGRESS_TIME, defaultValue = "0") + val progressTime: Long, + @ColumnInfo(name = StreamHistoryEntity.JOIN_STREAM_ID) val streamId: Long, diff --git a/app/src/main/java/org/schabi/newpipelegacy/database/stream/dao/StreamDAO.kt b/app/src/main/java/org/schabi/newpipelegacy/database/stream/dao/StreamDAO.kt index 9224cd548..b47f77454 100644 --- a/app/src/main/java/org/schabi/newpipelegacy/database/stream/dao/StreamDAO.kt +++ b/app/src/main/java/org/schabi/newpipelegacy/database/stream/dao/StreamDAO.kt @@ -7,13 +7,13 @@ import androidx.room.OnConflictStrategy import androidx.room.Query import androidx.room.Transaction import io.reactivex.Flowable -import java.util.Date import org.schabi.newpipe.extractor.stream.StreamType import org.schabi.newpipe.extractor.stream.StreamType.AUDIO_LIVE_STREAM import org.schabi.newpipe.extractor.stream.StreamType.LIVE_STREAM import org.schabi.newpipelegacy.database.BasicDAO import org.schabi.newpipelegacy.database.stream.model.StreamEntity import org.schabi.newpipelegacy.database.stream.model.StreamEntity.Companion.STREAM_ID +import java.util.Date @Dao abstract class StreamDAO : BasicDAO { @@ -35,10 +35,12 @@ abstract class StreamDAO : BasicDAO { @Insert(onConflict = OnConflictStrategy.IGNORE) internal abstract fun silentInsertAllInternal(streams: List): List - @Query(""" + @Query( + """ SELECT uid, stream_type, textual_upload_date, upload_date, is_upload_date_approximation, duration FROM streams WHERE url = :url AND service_id = :serviceId - """) + """ + ) internal abstract fun getMinimalStreamForCompare(serviceId: Int, url: String): StreamCompareFeed? @Transaction @@ -79,7 +81,7 @@ abstract class StreamDAO : BasicDAO { private fun compareAndUpdateStream(newerStream: StreamEntity) { val existentMinimalStream = getMinimalStreamForCompare(newerStream.serviceId, newerStream.url) - ?: throw IllegalStateException("Stream cannot be null just after insertion.") + ?: throw IllegalStateException("Stream cannot be null just after insertion.") newerStream.uid = existentMinimalStream.uid val isNewerStreamLive = newerStream.streamType == AUDIO_LIVE_STREAM || newerStream.streamType == LIVE_STREAM @@ -88,7 +90,7 @@ abstract class StreamDAO : BasicDAO { // Use the existent upload date if the newer stream does not have a better precision // (i.e. is an approximation). This is done to prevent unnecessary changes. val hasBetterPrecision = - newerStream.uploadDate != null && newerStream.isUploadDateApproximation != true + newerStream.uploadDate != null && newerStream.isUploadDateApproximation != true if (existentMinimalStream.uploadDate != null && !hasBetterPrecision) { newerStream.uploadDate = existentMinimalStream.uploadDate newerStream.textualUploadDate = existentMinimalStream.textualUploadDate @@ -101,7 +103,8 @@ abstract class StreamDAO : BasicDAO { } } - @Query(""" + @Query( + """ DELETE FROM streams WHERE NOT EXISTS (SELECT 1 FROM stream_history sh @@ -112,7 +115,8 @@ abstract class StreamDAO : BasicDAO { AND NOT EXISTS (SELECT 1 FROM feed f WHERE f.stream_id = streams.uid) - """) + """ + ) abstract fun deleteOrphans(): Int /** diff --git a/app/src/main/java/org/schabi/newpipelegacy/database/stream/model/StreamEntity.kt b/app/src/main/java/org/schabi/newpipelegacy/database/stream/model/StreamEntity.kt index 62c4f5659..8f4fad87e 100644 --- a/app/src/main/java/org/schabi/newpipelegacy/database/stream/model/StreamEntity.kt +++ b/app/src/main/java/org/schabi/newpipelegacy/database/stream/model/StreamEntity.kt @@ -5,9 +5,6 @@ import androidx.room.Entity import androidx.room.Ignore import androidx.room.Index import androidx.room.PrimaryKey -import java.io.Serializable -import java.util.Calendar -import java.util.Date import org.schabi.newpipe.extractor.localization.DateWrapper import org.schabi.newpipe.extractor.stream.StreamInfo import org.schabi.newpipe.extractor.stream.StreamInfoItem @@ -16,11 +13,15 @@ import org.schabi.newpipelegacy.database.stream.model.StreamEntity.Companion.STR import org.schabi.newpipelegacy.database.stream.model.StreamEntity.Companion.STREAM_TABLE import org.schabi.newpipelegacy.database.stream.model.StreamEntity.Companion.STREAM_URL import org.schabi.newpipelegacy.player.playqueue.PlayQueueItem +import java.io.Serializable +import java.util.Calendar +import java.util.Date -@Entity(tableName = STREAM_TABLE, - indices = [ - Index(value = [STREAM_SERVICE_ID, STREAM_URL], unique = true) - ] +@Entity( + tableName = STREAM_TABLE, + indices = [ + Index(value = [STREAM_SERVICE_ID, STREAM_URL], unique = true) + ] ) data class StreamEntity( @PrimaryKey(autoGenerate = true) @@ -60,30 +61,29 @@ data class StreamEntity( @ColumnInfo(name = STREAM_IS_UPLOAD_DATE_APPROXIMATION) var isUploadDateApproximation: Boolean? = null ) : Serializable { - @Ignore 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, - textualUploadDate = item.textualUploadDate, uploadDate = item.uploadDate?.date()?.time, - isUploadDateApproximation = item.uploadDate?.isApproximation + serviceId = item.serviceId, url = item.url, title = item.name, + streamType = item.streamType, duration = item.duration, uploader = item.uploaderName, + thumbnailUrl = item.thumbnailUrl, viewCount = item.viewCount, + textualUploadDate = item.textualUploadDate, uploadDate = item.uploadDate?.date()?.time, + isUploadDateApproximation = item.uploadDate?.isApproximation ) @Ignore 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, - textualUploadDate = info.textualUploadDate, uploadDate = info.uploadDate?.date()?.time, - isUploadDateApproximation = info.uploadDate?.isApproximation + serviceId = info.serviceId, url = info.url, title = info.name, + streamType = info.streamType, duration = info.duration, uploader = info.uploaderName, + thumbnailUrl = info.thumbnailUrl, viewCount = info.viewCount, + textualUploadDate = info.textualUploadDate, uploadDate = info.uploadDate?.date()?.time, + isUploadDateApproximation = info.uploadDate?.isApproximation ) @Ignore 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 + serviceId = item.serviceId, url = item.url, title = item.title, + streamType = item.streamType, duration = item.duration, uploader = item.uploader, + thumbnailUrl = item.thumbnailUrl ) fun toStreamInfoItem(): StreamInfoItem { @@ -95,8 +95,11 @@ data class StreamEntity( if (viewCount != null) item.viewCount = viewCount as Long item.textualUploadDate = textualUploadDate item.uploadDate = uploadDate?.let { - DateWrapper(Calendar.getInstance().apply { time = it }, isUploadDateApproximation - ?: false) + DateWrapper( + Calendar.getInstance().apply { time = it }, + isUploadDateApproximation + ?: false + ) } return item diff --git a/app/src/main/java/org/schabi/newpipelegacy/database/stream/model/StreamStateEntity.java b/app/src/main/java/org/schabi/newpipelegacy/database/stream/model/StreamStateEntity.java index caa187e5a..51a9c9352 100644 --- a/app/src/main/java/org/schabi/newpipelegacy/database/stream/model/StreamStateEntity.java +++ b/app/src/main/java/org/schabi/newpipelegacy/database/stream/model/StreamStateEntity.java @@ -22,6 +22,9 @@ public class StreamStateEntity { public static final String STREAM_STATE_TABLE = "stream_state"; public static final String JOIN_STREAM_ID = "stream_id"; + // This additional field is required for the SQL query because 'stream_id' is used + // for some other joins already + public static final String JOIN_STREAM_ID_ALIAS = "stream_id_alias"; public static final String STREAM_PROGRESS_TIME = "progress_time"; /** diff --git a/app/src/main/java/org/schabi/newpipelegacy/database/subscription/SubscriptionDAO.kt b/app/src/main/java/org/schabi/newpipelegacy/database/subscription/SubscriptionDAO.kt index 24ed3bd70..9a58b36a1 100644 --- a/app/src/main/java/org/schabi/newpipelegacy/database/subscription/SubscriptionDAO.kt +++ b/app/src/main/java/org/schabi/newpipelegacy/database/subscription/SubscriptionDAO.kt @@ -20,16 +20,19 @@ abstract class SubscriptionDAO : BasicDAO { @Query("SELECT * FROM subscriptions ORDER BY name COLLATE NOCASE ASC") abstract override fun getAll(): Flowable> - @Query(""" + @Query( + """ SELECT * FROM subscriptions WHERE name LIKE '%' || :filter || '%' ORDER BY name COLLATE NOCASE ASC - """) + """ + ) abstract fun getSubscriptionsFiltered(filter: String): Flowable> - @Query(""" + @Query( + """ SELECT * FROM subscriptions s LEFT JOIN feed_group_subscription_join fgs @@ -38,12 +41,14 @@ abstract class SubscriptionDAO : BasicDAO { WHERE (fgs.subscription_id IS NULL OR fgs.group_id = :currentGroupId) ORDER BY name COLLATE NOCASE ASC - """) + """ + ) abstract fun getSubscriptionsOnlyUngrouped( currentGroupId: Long ): Flowable> - @Query(""" + @Query( + """ SELECT * FROM subscriptions s LEFT JOIN feed_group_subscription_join fgs @@ -53,7 +58,8 @@ abstract class SubscriptionDAO : BasicDAO { AND s.name LIKE '%' || :filter || '%' ORDER BY name COLLATE NOCASE ASC - """) + """ + ) abstract fun getSubscriptionsOnlyUngroupedFiltered( currentGroupId: Long, filter: String diff --git a/app/src/main/java/org/schabi/newpipelegacy/download/DownloadActivity.java b/app/src/main/java/org/schabi/newpipelegacy/download/DownloadActivity.java index 7e4ef1193..84f56075b 100644 --- a/app/src/main/java/org/schabi/newpipelegacy/download/DownloadActivity.java +++ b/app/src/main/java/org/schabi/newpipelegacy/download/DownloadActivity.java @@ -14,6 +14,7 @@ import org.schabi.newpipelegacy.R; import org.schabi.newpipelegacy.util.AndroidTvUtils; +import org.schabi.newpipelegacy.util.NavigationHelper; import org.schabi.newpipelegacy.util.ThemeHelper; import org.schabi.newpipelegacy.views.FocusOverlayView; @@ -87,6 +88,9 @@ public boolean onOptionsItemSelected(final MenuItem item) { case android.R.id.home: onBackPressed(); return true; + case R.id.action_settings: + NavigationHelper.openSettings(this); + return true; default: return super.onOptionsItemSelected(item); } diff --git a/app/src/main/java/org/schabi/newpipelegacy/download/DownloadDialog.java b/app/src/main/java/org/schabi/newpipelegacy/download/DownloadDialog.java index e91141dfc..b77f79e7c 100644 --- a/app/src/main/java/org/schabi/newpipelegacy/download/DownloadDialog.java +++ b/app/src/main/java/org/schabi/newpipelegacy/download/DownloadDialog.java @@ -748,7 +748,7 @@ private void checkSelectedDownload(final StoredDirectoryHelper mainStorage, 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/newpipelegacy/fragments/BaseStateFragment.java b/app/src/main/java/org/schabi/newpipelegacy/fragments/BaseStateFragment.java index 76a4a479e..6352f0a71 100644 --- a/app/src/main/java/org/schabi/newpipelegacy/fragments/BaseStateFragment.java +++ b/app/src/main/java/org/schabi/newpipelegacy/fragments/BaseStateFragment.java @@ -19,9 +19,15 @@ import org.schabi.newpipelegacy.MainActivity; import org.schabi.newpipelegacy.R; import org.schabi.newpipelegacy.ReCaptchaActivity; +import org.schabi.newpipe.extractor.exceptions.AgeRestrictedContentException; import org.schabi.newpipe.extractor.exceptions.ContentNotAvailableException; import org.schabi.newpipe.extractor.exceptions.ContentNotSupportedException; +import org.schabi.newpipe.extractor.exceptions.GeographicRestrictionException; +import org.schabi.newpipe.extractor.exceptions.PaidContentException; +import org.schabi.newpipe.extractor.exceptions.PrivateContentException; import org.schabi.newpipe.extractor.exceptions.ReCaptchaException; +import org.schabi.newpipe.extractor.exceptions.SoundCloudGoPlusContentException; +import org.schabi.newpipe.extractor.exceptions.YoutubeMusicPremiumContentException; import org.schabi.newpipelegacy.report.ErrorActivity; import org.schabi.newpipelegacy.report.UserAction; import org.schabi.newpipelegacy.util.ExceptionUtils; @@ -210,12 +216,30 @@ protected boolean onError(final Throwable exception) { if (exception instanceof ReCaptchaException) { onReCaptchaException((ReCaptchaException) exception); return true; - } else if (exception instanceof ContentNotAvailableException) { - showError(getString(R.string.content_not_available), false); - return true; } else if (ExceptionUtils.isNetworkRelated(exception)) { showError(getString(R.string.network_error), true); return true; + } else if (exception instanceof AgeRestrictedContentException) { + showError(getString(R.string.restricted_video_no_stream), false); + return true; + } else if (exception instanceof GeographicRestrictionException) { + showError(getString(R.string.georestricted_content), false); + return true; + } else if (exception instanceof PaidContentException) { + showError(getString(R.string.paid_content), false); + return true; + } else if (exception instanceof PrivateContentException) { + showError(getString(R.string.private_content), false); + return true; + } else if (exception instanceof SoundCloudGoPlusContentException) { + showError(getString(R.string.soundcloud_go_plus_content), false); + return true; + } else if (exception instanceof YoutubeMusicPremiumContentException) { + showError(getString(R.string.youtube_music_premium_content), false); + return true; + } else if (exception instanceof ContentNotAvailableException) { + showError(getString(R.string.content_not_available), false); + return true; } else if (exception instanceof ContentNotSupportedException) { showError(getString(R.string.content_not_supported), false); return true; diff --git a/app/src/main/java/org/schabi/newpipelegacy/fragments/MainFragment.java b/app/src/main/java/org/schabi/newpipelegacy/fragments/MainFragment.java index 805ec6602..ddc86e018 100644 --- a/app/src/main/java/org/schabi/newpipelegacy/fragments/MainFragment.java +++ b/app/src/main/java/org/schabi/newpipelegacy/fragments/MainFragment.java @@ -148,10 +148,8 @@ public boolean onOptionsItemSelected(final MenuItem item) { switch (item.getItemId()) { case R.id.action_search: try { - NavigationHelper.openSearchFragment( - getFragmentManager(), - ServiceHelper.getSelectedServiceId(activity), - ""); + NavigationHelper.openSearchFragment(getFM(), + ServiceHelper.getSelectedServiceId(activity), ""); } catch (Exception e) { ErrorActivity.reportUiError((AppCompatActivity) getActivity(), e); } diff --git a/app/src/main/java/org/schabi/newpipelegacy/fragments/detail/VideoDetailFragment.java b/app/src/main/java/org/schabi/newpipelegacy/fragments/detail/VideoDetailFragment.java index d488b68d2..90f1c7f8d 100644 --- a/app/src/main/java/org/schabi/newpipelegacy/fragments/detail/VideoDetailFragment.java +++ b/app/src/main/java/org/schabi/newpipelegacy/fragments/detail/VideoDetailFragment.java @@ -53,6 +53,7 @@ import org.schabi.newpipe.extractor.InfoItem; import org.schabi.newpipe.extractor.NewPipe; import org.schabi.newpipe.extractor.ServiceList; +import org.schabi.newpipe.extractor.exceptions.ContentNotSupportedException; import org.schabi.newpipe.extractor.exceptions.ExtractionException; import org.schabi.newpipe.extractor.services.youtube.extractors.YoutubeStreamExtractor; import org.schabi.newpipe.extractor.stream.AudioStream; @@ -84,6 +85,7 @@ import org.schabi.newpipelegacy.util.Localization; import org.schabi.newpipelegacy.util.NavigationHelper; import org.schabi.newpipelegacy.util.PermissionHelper; +import org.schabi.newpipelegacy.util.ServiceHelper; import org.schabi.newpipelegacy.util.ShareUtils; import org.schabi.newpipelegacy.util.StreamItemAdapter; import org.schabi.newpipelegacy.util.StreamItemAdapter.StreamSizeWrapper; @@ -327,7 +329,7 @@ public void onActivityResult(final int requestCode, final int resultCode, final case ReCaptchaActivity.RECAPTCHA_REQUEST: if (resultCode == Activity.RESULT_OK) { NavigationHelper - .openVideoDetailFragment(getFragmentManager(), serviceId, url, name); + .openVideoDetailFragment(getFM(), serviceId, url, name); } else { Log.e(TAG, "ReCaptcha failed"); } @@ -408,9 +410,9 @@ public void onClick(final View v) { openPopupPlayer(false); break; case R.id.detail_controls_playlist_append: - if (getFragmentManager() != null && currentInfo != null) { + if (getFM() != null && currentInfo != null) { PlaylistAppendDialog.fromStreamInfo(currentInfo) - .show(getFragmentManager(), TAG); + .show(getFM(), TAG); } break; case R.id.detail_controls_download: @@ -449,11 +451,8 @@ public void onClick(final View v) { private void openChannel(final String subChannelUrl, final String subChannelName) { try { - NavigationHelper.openChannelFragment( - getFragmentManager(), - currentInfo.getServiceId(), - subChannelUrl, - subChannelName); + NavigationHelper.openChannelFragment(getFM(), currentInfo.getServiceId(), + subChannelUrl, subChannelName); } catch (Exception e) { ErrorActivity.reportUiError((AppCompatActivity) getActivity(), e); } @@ -466,6 +465,9 @@ public boolean onLongClick(final View v) { } switch (v.getId()) { + case R.id.detail_controls_playlist_append: + NavigationHelper.openBookmarksFragment(getFM()); + break; case R.id.detail_controls_background: openBackgroundPlayer(true); break; @@ -589,6 +591,7 @@ protected void initListeners() { detailControlsBackground.setOnClickListener(this); detailControlsPopup.setOnClickListener(this); detailControlsAddToPlaylist.setOnClickListener(this); + detailControlsAddToPlaylist.setOnLongClickListener(this); detailControlsDownload.setOnClickListener(this); detailControlsDownload.setOnLongClickListener(this); @@ -618,7 +621,7 @@ private View.OnTouchListener getOnControlsTouchListener() { private void initThumbnailViews(@NonNull final StreamInfo info) { thumbnailImageView.setImageResource(R.drawable.dummy_thumbnail_dark); if (!TextUtils.isEmpty(info.getThumbnailUrl())) { - final String infoServiceName = NewPipe.getNameOfService(info.getServiceId()); + final String infoServiceName = ServiceHelper.getNameOfServiceById(info.getServiceId()); final ImageLoadingListener onFailListener = new SimpleImageLoadingListener() { @Override public void onLoadingFailed(final String imageUri, final View view, @@ -693,6 +696,18 @@ public boolean onOptionsItemSelected(final MenuItem item) { currentInfo.getOriginalUrl()); } return true; + case R.id.menu_item_share_stream: + if (currentInfo != null) { + final Stream stream; + if (currentInfo.getVideoStreams().isEmpty() + && currentInfo.getVideoOnlyStreams().isEmpty()) { + stream = getDefaultAudioStream(); + } else { + stream = getSelectedVideoStream(); + } + ShareUtils.shareUrl(requireContext(), currentInfo.getName(), stream.getUrl()); + } + return true; case R.id.menu_item_openInBrowser: if (currentInfo != null) { ShareUtils.openUrlInBrowser(requireContext(), currentInfo.getOriginalUrl()); @@ -926,10 +941,10 @@ private boolean shouldShowComments() { //////////////////////////////////////////////////////////////////////////*/ private void openBackgroundPlayer(final boolean append) { - AudioStream audioStream = currentInfo.getAudioStreams() - .get(ListHelper.getDefaultAudioFormat(activity, currentInfo.getAudioStreams())); + final AudioStream audioStream = getDefaultAudioStream(); - boolean useExternalAudioPlayer = PreferenceManager.getDefaultSharedPreferences(activity) + final boolean useExternalAudioPlayer = PreferenceManager + .getDefaultSharedPreferences(activity) .getBoolean(activity.getString(R.string.use_external_audio_player_key), false); if (!useExternalAudioPlayer && android.os.Build.VERSION.SDK_INT >= 16) { @@ -1013,9 +1028,23 @@ private VideoStream getSelectedVideoStream() { return sortedVideoStreams != null ? sortedVideoStreams.get(selectedVideoStreamIndex) : null; } + /** + * Get the stream to play when the current stream is an audio-only stream. + * + * This is the audio-only equivalent of getSelectedVideoStream, + * without the ability for the user to select a custom stream quality. + * + * @return AudioStream instance according to user settings + */ + private AudioStream getDefaultAudioStream() { + final List audioStreams = currentInfo.getAudioStreams(); + final int streamIndex = ListHelper.getDefaultAudioFormat(activity, audioStreams); + return audioStreams.get(streamIndex); + } + private void prepareDescription(final Description description) { if (description == null || TextUtils.isEmpty(description.getContent()) - || description == Description.emptyDescription) { + || description == Description.EMPTY_DESCRIPTION) { return; } @@ -1259,33 +1288,35 @@ public void handleResult(@NonNull final StreamInfo info) { setTitleToUrl(info.getServiceId(), info.getOriginalUrl(), info.getName()); if (!info.getErrors().isEmpty()) { - showSnackBarError(info.getErrors(), - UserAction.REQUESTED_STREAM, - NewPipe.getNameOfService(info.getServiceId()), - info.getUrl(), - 0); - } - - switch (info.getStreamType()) { - case LIVE_STREAM: - case AUDIO_LIVE_STREAM: - detailControlsDownload.setVisibility(View.GONE); - spinnerToolbar.setVisibility(View.GONE); - break; - default: - if (info.getAudioStreams().isEmpty()) { - detailControlsBackground.setVisibility(View.GONE); - } - if (!info.getVideoStreams().isEmpty() || !info.getVideoOnlyStreams().isEmpty()) { - break; + // Bandcamp fan pages are not yet supported and thus a ContentNotAvailableException is + // thrown. This is not an error and thus should not be shown to the user. + for (final Throwable throwable : info.getErrors()) { + if (throwable instanceof ContentNotSupportedException + && "Fan pages are not supported".equals(throwable.getMessage())) { + info.getErrors().remove(throwable); } + } - detailControlsPopup.setVisibility(View.GONE); - spinnerToolbar.setVisibility(View.GONE); - thumbnailPlayButton.setImageResource(R.drawable.ic_headset_shadow); - break; + if (!info.getErrors().isEmpty()) { + showSnackBarError(info.getErrors(), + UserAction.REQUESTED_STREAM, + ServiceHelper.getNameOfServiceById(info.getServiceId()), + info.getUrl(), + 0); + } } + detailControlsDownload.setVisibility(info.getStreamType() == StreamType.LIVE_STREAM + || info.getStreamType() == StreamType.AUDIO_LIVE_STREAM ? View.GONE : View.VISIBLE); + detailControlsBackground.setVisibility(info.getAudioStreams().isEmpty() + ? View.GONE : View.VISIBLE); + + final boolean noVideoStreams = + info.getVideoStreams().isEmpty() && info.getVideoOnlyStreams().isEmpty(); + detailControlsPopup.setVisibility(noVideoStreams ? View.GONE : View.VISIBLE); + thumbnailPlayButton.setImageResource( + noVideoStreams ? R.drawable.ic_headset_shadow : R.drawable.ic_play_arrow_shadow); + if (autoPlayEnabled) { openVideoPlayer(); // Only auto play in the first open @@ -1364,14 +1395,14 @@ protected boolean onError(final Throwable exception) { return true; } - int errorId = exception instanceof YoutubeStreamExtractor.DecryptException + final int errorId = exception instanceof YoutubeStreamExtractor.DeobfuscateException ? R.string.youtube_signature_decryption_error : exception instanceof ExtractionException ? R.string.parsing_error : R.string.general_error; onUnrecoverableError(exception, UserAction.REQUESTED_STREAM, - NewPipe.getNameOfService(serviceId), url, errorId); + ServiceHelper.getNameOfServiceById(serviceId), url, errorId); return true; } diff --git a/app/src/main/java/org/schabi/newpipelegacy/fragments/list/channel/ChannelFragment.java b/app/src/main/java/org/schabi/newpipelegacy/fragments/list/channel/ChannelFragment.java index 2096600a3..27f506418 100644 --- a/app/src/main/java/org/schabi/newpipelegacy/fragments/list/channel/ChannelFragment.java +++ b/app/src/main/java/org/schabi/newpipelegacy/fragments/list/channel/ChannelFragment.java @@ -30,7 +30,6 @@ import org.schabi.newpipelegacy.database.subscription.SubscriptionEntity; import org.schabi.newpipe.extractor.InfoItem; import org.schabi.newpipe.extractor.ListExtractor; -import org.schabi.newpipe.extractor.NewPipe; import org.schabi.newpipe.extractor.channel.ChannelInfo; import org.schabi.newpipe.extractor.exceptions.ContentNotSupportedException; import org.schabi.newpipe.extractor.exceptions.ExtractionException; @@ -46,6 +45,7 @@ import org.schabi.newpipelegacy.util.ImageDisplayConstants; import org.schabi.newpipelegacy.util.Localization; import org.schabi.newpipelegacy.util.NavigationHelper; +import org.schabi.newpipelegacy.util.ServiceHelper; import org.schabi.newpipelegacy.util.ShareUtils; import org.schabi.newpipelegacy.util.ThemeHelper; @@ -244,7 +244,7 @@ private void monitorSubscription(final ChannelInfo info) { final Consumer onError = (Throwable throwable) -> { animateView(headerSubscribeButton, false, 100); showSnackBarError(throwable, UserAction.SUBSCRIPTION, - NewPipe.getNameOfService(currentInfo.getServiceId()), + ServiceHelper.getNameOfServiceById(currentInfo.getServiceId()), "Get subscription status", 0); }; @@ -297,7 +297,7 @@ private void updateSubscription(final ChannelInfo info) { final Consumer onError = (@NonNull Throwable throwable) -> onUnrecoverableError(throwable, UserAction.SUBSCRIPTION, - NewPipe.getNameOfService(info.getServiceId()), + ServiceHelper.getNameOfServiceById(info.getServiceId()), "Updating Subscription for " + info.getUrl(), R.string.subscription_update_failed); @@ -318,7 +318,7 @@ private Disposable monitorSubscribeButton(final Button subscribeButton, final Consumer onError = (@NonNull Throwable throwable) -> onUnrecoverableError(throwable, UserAction.SUBSCRIPTION, - NewPipe.getNameOfService(currentInfo.getServiceId()), + ServiceHelper.getNameOfServiceById(currentInfo.getServiceId()), "Subscription Change", R.string.subscription_change_failed); @@ -426,8 +426,8 @@ public void onClick(final View v) { case R.id.sub_channel_title_view: if (!TextUtils.isEmpty(currentInfo.getParentChannelUrl())) { try { - NavigationHelper.openChannelFragment(getFragmentManager(), - currentInfo.getServiceId(), currentInfo.getParentChannelUrl(), + NavigationHelper.openChannelFragment(getFM(), currentInfo.getServiceId(), + currentInfo.getParentChannelUrl(), currentInfo.getParentChannelName()); } catch (Exception e) { ErrorActivity.reportUiError((AppCompatActivity) getActivity(), e); @@ -505,7 +505,9 @@ public void handleResult(@NonNull final ChannelInfo result) { if (!errors.isEmpty()) { showSnackBarError(errors, UserAction.REQUESTED_CHANNEL, - NewPipe.getNameOfService(result.getServiceId()), result.getUrl(), 0); + ServiceHelper.getNameOfServiceById(result.getServiceId()), + result.getUrl(), 0 + ); } } @@ -565,7 +567,7 @@ public void handleNextItems(final ListExtractor.InfoItemsPage result) { if (!result.getErrors().isEmpty()) { showSnackBarError(result.getErrors(), UserAction.REQUESTED_CHANNEL, - NewPipe.getNameOfService(serviceId), + ServiceHelper.getNameOfServiceById(serviceId), "Get next page of: " + url, R.string.general_error); } @@ -585,7 +587,7 @@ protected boolean onError(final Throwable exception) { ? R.string.parsing_error : R.string.general_error; onUnrecoverableError(exception, UserAction.REQUESTED_CHANNEL, - NewPipe.getNameOfService(serviceId), url, errorId); + ServiceHelper.getNameOfServiceById(serviceId), url, errorId); return true; } diff --git a/app/src/main/java/org/schabi/newpipelegacy/fragments/list/comments/CommentsFragment.java b/app/src/main/java/org/schabi/newpipelegacy/fragments/list/comments/CommentsFragment.java index 46785d181..92ba112bd 100644 --- a/app/src/main/java/org/schabi/newpipelegacy/fragments/list/comments/CommentsFragment.java +++ b/app/src/main/java/org/schabi/newpipelegacy/fragments/list/comments/CommentsFragment.java @@ -7,18 +7,19 @@ import android.view.MenuInflater; import android.view.View; import android.view.ViewGroup; +import android.widget.TextView; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import org.schabi.newpipelegacy.R; import org.schabi.newpipe.extractor.ListExtractor; -import org.schabi.newpipe.extractor.NewPipe; import org.schabi.newpipe.extractor.comments.CommentsInfo; import org.schabi.newpipelegacy.fragments.list.BaseListInfoFragment; import org.schabi.newpipelegacy.report.UserAction; import org.schabi.newpipelegacy.util.AnimationUtils; import org.schabi.newpipelegacy.util.ExtractorHelper; +import org.schabi.newpipelegacy.util.ServiceHelper; import io.reactivex.Single; import io.reactivex.disposables.CompositeDisposable; @@ -28,6 +29,8 @@ public class CommentsFragment extends BaseListInfoFragment { private boolean mIsVisibleToUser = false; + private TextView emptyStateDesc; + public static CommentsFragment getInstance(final int serviceId, final String url, final String name) { CommentsFragment instance = new CommentsFragment(); @@ -35,6 +38,13 @@ public static CommentsFragment getInstance(final int serviceId, final String ur return instance; } + @Override + protected void initViews(final View rootView, final Bundle savedInstanceState) { + super.initViews(rootView, savedInstanceState); + + emptyStateDesc = rootView.findViewById(R.id.empty_state_desc); + } + /*////////////////////////////////////////////////////////////////////////// // LifeCycle //////////////////////////////////////////////////////////////////////////*/ @@ -92,11 +102,16 @@ public void showLoading() { public void handleResult(@NonNull final CommentsInfo result) { super.handleResult(result); + emptyStateDesc.setText( + result.isCommentsDisabled() + ? R.string.comments_are_disabled + : R.string.no_comments); + AnimationUtils.slideUp(getView(), 120, 150, 0.06f); if (!result.getErrors().isEmpty()) { showSnackBarError(result.getErrors(), UserAction.REQUESTED_COMMENTS, - NewPipe.getNameOfService(result.getServiceId()), result.getUrl(), 0); + ServiceHelper.getNameOfServiceById(result.getServiceId()), result.getUrl(), 0); } if (disposables != null) { @@ -110,7 +125,7 @@ public void handleNextItems(final ListExtractor.InfoItemsPage result) { if (!result.getErrors().isEmpty()) { showSnackBarError(result.getErrors(), UserAction.REQUESTED_COMMENTS, - NewPipe.getNameOfService(serviceId), "Get next page of: " + url, + ServiceHelper.getNameOfServiceById(serviceId), "Get next page of: " + url, R.string.general_error); } } @@ -127,7 +142,8 @@ protected boolean onError(final Throwable exception) { hideLoading(); showSnackBarError(exception, UserAction.REQUESTED_COMMENTS, - NewPipe.getNameOfService(serviceId), url, R.string.error_unable_to_load_comments); + ServiceHelper.getNameOfServiceById(serviceId), + url, R.string.error_unable_to_load_comments); return true; } diff --git a/app/src/main/java/org/schabi/newpipelegacy/fragments/list/kiosk/KioskFragment.java b/app/src/main/java/org/schabi/newpipelegacy/fragments/list/kiosk/KioskFragment.java index 0845f5b41..4cb91bfc4 100644 --- a/app/src/main/java/org/schabi/newpipelegacy/fragments/list/kiosk/KioskFragment.java +++ b/app/src/main/java/org/schabi/newpipelegacy/fragments/list/kiosk/KioskFragment.java @@ -24,6 +24,7 @@ import org.schabi.newpipelegacy.util.ExtractorHelper; import org.schabi.newpipelegacy.util.KioskTranslator; import org.schabi.newpipelegacy.util.Localization; +import org.schabi.newpipelegacy.util.ServiceHelper; import icepick.State; import io.reactivex.Single; @@ -173,7 +174,7 @@ public void handleResult(@NonNull final KioskInfo result) { if (!result.getErrors().isEmpty()) { showSnackBarError(result.getErrors(), UserAction.REQUESTED_KIOSK, - NewPipe.getNameOfService(result.getServiceId()), result.getUrl(), 0); + ServiceHelper.getNameOfServiceById(result.getServiceId()), result.getUrl(), 0); } } @@ -183,7 +184,7 @@ public void handleNextItems(final ListExtractor.InfoItemsPage result) { if (!result.getErrors().isEmpty()) { showSnackBarError(result.getErrors(), - UserAction.REQUESTED_PLAYLIST, NewPipe.getNameOfService(serviceId), + UserAction.REQUESTED_PLAYLIST, ServiceHelper.getNameOfServiceById(serviceId), "Get next page of: " + url, 0); } } diff --git a/app/src/main/java/org/schabi/newpipelegacy/fragments/list/playlist/PlaylistFragment.java b/app/src/main/java/org/schabi/newpipelegacy/fragments/list/playlist/PlaylistFragment.java index 78e48d03a..33e7f40a2 100644 --- a/app/src/main/java/org/schabi/newpipelegacy/fragments/list/playlist/PlaylistFragment.java +++ b/app/src/main/java/org/schabi/newpipelegacy/fragments/list/playlist/PlaylistFragment.java @@ -25,7 +25,6 @@ import org.schabi.newpipelegacy.database.playlist.model.PlaylistRemoteEntity; import org.schabi.newpipe.extractor.InfoItem; import org.schabi.newpipe.extractor.ListExtractor; -import org.schabi.newpipe.extractor.NewPipe; import org.schabi.newpipe.extractor.exceptions.ExtractionException; import org.schabi.newpipe.extractor.playlist.PlaylistInfo; import org.schabi.newpipe.extractor.stream.StreamInfoItem; @@ -41,6 +40,7 @@ import org.schabi.newpipelegacy.util.ImageDisplayConstants; import org.schabi.newpipelegacy.util.Localization; import org.schabi.newpipelegacy.util.NavigationHelper; +import org.schabi.newpipelegacy.util.ServiceHelper; import org.schabi.newpipelegacy.util.ShareUtils; import org.schabi.newpipelegacy.util.StreamDialogEntry; import org.schabi.newpipelegacy.util.ThemeHelper; @@ -286,10 +286,8 @@ public void handleResult(@NonNull final PlaylistInfo result) { if (!TextUtils.isEmpty(result.getUploaderUrl())) { headerUploaderLayout.setOnClickListener(v -> { try { - NavigationHelper.openChannelFragment(getFragmentManager(), - result.getServiceId(), - result.getUploaderUrl(), - result.getUploaderName()); + NavigationHelper.openChannelFragment(getFM(), result.getServiceId(), + result.getUploaderUrl(), result.getUploaderName()); } catch (Exception e) { ErrorActivity.reportUiError((AppCompatActivity) getActivity(), e); } @@ -308,7 +306,7 @@ public void handleResult(@NonNull final PlaylistInfo result) { if (!result.getErrors().isEmpty()) { showSnackBarError(result.getErrors(), UserAction.REQUESTED_PLAYLIST, - NewPipe.getNameOfService(result.getServiceId()), result.getUrl(), 0); + ServiceHelper.getNameOfServiceById(result.getServiceId()), result.getUrl(), 0); } remotePlaylistManager.getPlaylist(result) @@ -361,7 +359,7 @@ public void handleNextItems(final ListExtractor.InfoItemsPage result) { if (!result.getErrors().isEmpty()) { showSnackBarError(result.getErrors(), UserAction.REQUESTED_PLAYLIST, - NewPipe.getNameOfService(serviceId), "Get next page of: " + url, 0); + ServiceHelper.getNameOfServiceById(serviceId), "Get next page of: " + url, 0); } } @@ -378,7 +376,7 @@ protected boolean onError(final Throwable exception) { int errorId = exception instanceof ExtractionException ? R.string.parsing_error : R.string.general_error; onUnrecoverableError(exception, UserAction.REQUESTED_PLAYLIST, - NewPipe.getNameOfService(serviceId), url, errorId); + ServiceHelper.getNameOfServiceById(serviceId), url, errorId); return true; } diff --git a/app/src/main/java/org/schabi/newpipelegacy/fragments/list/search/SearchFragment.java b/app/src/main/java/org/schabi/newpipelegacy/fragments/list/search/SearchFragment.java index c2b1b4588..411026bcc 100644 --- a/app/src/main/java/org/schabi/newpipelegacy/fragments/list/search/SearchFragment.java +++ b/app/src/main/java/org/schabi/newpipelegacy/fragments/list/search/SearchFragment.java @@ -43,6 +43,8 @@ import org.schabi.newpipe.extractor.exceptions.ParsingException; import org.schabi.newpipe.extractor.search.SearchExtractor; import org.schabi.newpipe.extractor.search.SearchInfo; +import org.schabi.newpipe.extractor.services.peertube.linkHandler.PeertubeSearchQueryHandlerFactory; +import org.schabi.newpipe.extractor.services.youtube.linkHandler.YoutubeSearchQueryHandlerFactory; import org.schabi.newpipelegacy.fragments.BackPressable; import org.schabi.newpipelegacy.fragments.list.BaseListFragment; import org.schabi.newpipelegacy.local.history.HistoryRecordManager; @@ -51,6 +53,7 @@ import org.schabi.newpipelegacy.util.AndroidTvUtils; import org.schabi.newpipelegacy.util.AnimationUtils; import org.schabi.newpipelegacy.util.Constants; +import org.schabi.newpipelegacy.util.ExceptionUtils; import org.schabi.newpipelegacy.util.ExtractorHelper; import org.schabi.newpipelegacy.util.NavigationHelper; import org.schabi.newpipelegacy.util.ServiceHelper; @@ -78,7 +81,7 @@ import static java.util.Arrays.asList; import static org.schabi.newpipelegacy.util.AnimationUtils.animateView; -public class SearchFragment extends BaseListFragment +public class SearchFragment extends BaseListFragment> implements BackPressable { /*////////////////////////////////////////////////////////////////////////// // Search @@ -133,7 +136,6 @@ public class SearchFragment extends BaseListFragment menuItemToFilterName; private StreamingService service; private Page nextPage; - private String contentCountry; private boolean isSuggestionsEnabled = true; private Disposable searchDisposable; @@ -154,6 +156,7 @@ public class SearchFragment extends BaseListFragment 0 && !isLoading.get()) { hideSuggestionsPanel(); @@ -722,7 +722,7 @@ private void initSuggestionObserver() { suggestionDisposable = observable .switchMap(query -> { final Flowable> flowable = historyRecordManager - .getRelatedSearches(query, 3, 25); + .getRelatedSearches(query, 60, 80); final Observable> local = flowable.toObservable() .map(searchHistoryEntries -> { List result = new ArrayList<>(); @@ -740,6 +740,13 @@ private void initSuggestionObserver() { final Observable> network = ExtractorHelper .suggestionsFor(serviceId, query) + .onErrorReturn(throwable -> { + if (!ExceptionUtils.isNetworkRelated(throwable)) { + showSnackBarError(throwable, UserAction.GET_SUGGESTIONS, + ServiceHelper.getNameOfServiceById(serviceId), searchString, 0); + } + return new ArrayList<>(); + }) .toObservable() .map(strings -> { List result = new ArrayList<>(); @@ -789,28 +796,30 @@ protected void doInitialLoadLogic() { // no-op } - private void search(final String ss, final String[] cf, final String sf) { + private void search(final String theSearchString, + final String[] theContentFilter, + final String theSortFilter) { if (DEBUG) { - Log.d(TAG, "search() called with: query = [" + ss + "]"); + Log.d(TAG, "search() called with: query = [" + theSearchString + "]"); } - if (ss.isEmpty()) { + if (theSearchString.isEmpty()) { return; } try { - final StreamingService streamingService = NewPipe.getServiceByUrl(ss); + final StreamingService streamingService = NewPipe.getServiceByUrl(theSearchString); if (streamingService != null) { showLoading(); disposables.add(Observable - .fromCallable(() -> - NavigationHelper.getIntentByLink(activity, streamingService, ss)) + .fromCallable(() -> NavigationHelper.getIntentByLink(activity, + streamingService, theSearchString)) .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .subscribe(intent -> { - getFragmentManager().popBackStackImmediate(); + getFM().popBackStackImmediate(); activity.startActivity(intent); }, throwable -> - showError(getString(R.string.url_not_supported_toast), false))); + showError(getString(R.string.unsupported_url), false))); return; } } catch (Exception ignored) { @@ -818,29 +827,27 @@ private void search(final String ss, final String[] cf, final String sf) { } lastSearchedString = this.searchString; - this.searchString = ss; + this.searchString = theSearchString; infoListAdapter.clearStreamItemList(); hideSuggestionsPanel(); hideKeyboardSearch(); - historyRecordManager.onSearched(serviceId, ss) + disposables.add(historyRecordManager.onSearched(serviceId, theSearchString) .observeOn(AndroidSchedulers.mainThread()) .subscribe( ignored -> { }, error -> showSnackBarError(error, UserAction.SEARCHED, - NewPipe.getNameOfService(serviceId), ss, 0) - ); - suggestionPublisher.onNext(ss); + ServiceHelper.getNameOfServiceById(serviceId), theSearchString, 0) + )); + suggestionPublisher.onNext(theSearchString); startLoading(false); } @Override public void startLoading(final boolean forceLoad) { super.startLoading(forceLoad); - if (disposables != null) { - disposables.clear(); - } + disposables.clear(); if (searchDisposable != null) { searchDisposable.dispose(); } @@ -879,8 +886,7 @@ protected void loadMoreItems() { @Override protected boolean hasMoreItems() { - // TODO: No way to tell if search has more items in the moment - return true; + return Page.isValid(nextPage); } @Override @@ -893,22 +899,25 @@ protected void onItemSelected(final InfoItem selectedItem) { // Utils //////////////////////////////////////////////////////////////////////////*/ - private void changeContentFilter(final MenuItem item, final List cf) { - this.filterItemCheckedId = item.getItemId(); + private void changeContentFilter(final MenuItem item, final List theContentFilter) { + filterItemCheckedId = item.getItemId(); item.setChecked(true); - this.contentFilter = new String[]{cf.get(0)}; + contentFilter = new String[]{theContentFilter.get(0)}; if (!TextUtils.isEmpty(searchString)) { - search(searchString, this.contentFilter, sortFilter); + search(searchString, contentFilter, sortFilter); } } - private void setQuery(final int sid, final String ss, final String[] cf, final String sf) { - this.serviceId = sid; - this.searchString = searchString; - this.contentFilter = cf; - this.sortFilter = sf; + private void setQuery(final int theServiceId, + final String theSearchString, + final String[] theContentFilter, + final String theSortFilter) { + serviceId = theServiceId; + searchString = theSearchString; + contentFilter = theContentFilter; + sortFilter = theSortFilter; } /*////////////////////////////////////////////////////////////////////////// @@ -922,7 +931,7 @@ public void handleSuggestions(@NonNull final List suggestions) { suggestionsRecyclerView.smoothScrollToPosition(0); suggestionsRecyclerView.post(() -> suggestionListAdapter.setItems(suggestions)); - if (errorPanelRoot.getVisibility() == View.VISIBLE) { + if (suggestionsPanelVisible && errorPanelRoot.getVisibility() == View.VISIBLE) { hideLoading(); } } @@ -939,7 +948,7 @@ public void onSuggestionError(final Throwable exception) { ? R.string.parsing_error : R.string.general_error; onUnrecoverableError(exception, UserAction.GET_SUGGESTIONS, - NewPipe.getNameOfService(serviceId), searchString, errorId); + ServiceHelper.getNameOfServiceById(serviceId), searchString, errorId); } /*////////////////////////////////////////////////////////////////////////// @@ -970,7 +979,7 @@ public void handleResult(@NonNull final SearchInfo result) { && !(exceptions.size() == 1 && exceptions.get(0) instanceof SearchExtractor.NothingFoundException)) { showSnackBarError(result.getErrors(), UserAction.SEARCHED, - NewPipe.getNameOfService(serviceId), searchString, 0); + ServiceHelper.getNameOfServiceById(serviceId), searchString, 0); } searchSuggestion = result.getSearchSuggestion(); @@ -1026,14 +1035,14 @@ private void handleSearchSuggestion() { } @Override - public void handleNextItems(final ListExtractor.InfoItemsPage result) { + public void handleNextItems(final ListExtractor.InfoItemsPage result) { showListFooter(false); infoListAdapter.addInfoItemList(result.getItems()); nextPage = result.getNextPage(); if (!result.getErrors().isEmpty()) { showSnackBarError(result.getErrors(), UserAction.SEARCHED, - NewPipe.getNameOfService(serviceId), + ServiceHelper.getNameOfServiceById(serviceId), "\"" + searchString + "\" → pageUrl: " + nextPage.getUrl() + ", " + "pageIds: " + nextPage.getIds() + ", " + "pageCookies: " + nextPage.getCookies(), 0); @@ -1055,7 +1064,7 @@ protected boolean onError(final Throwable exception) { ? R.string.parsing_error : R.string.general_error; onUnrecoverableError(exception, UserAction.SEARCHED, - NewPipe.getNameOfService(serviceId), searchString, errorId); + ServiceHelper.getNameOfServiceById(serviceId), searchString, errorId); } return true; @@ -1065,8 +1074,7 @@ protected boolean onError(final Throwable exception) { // Suggestion item touch helper //////////////////////////////////////////////////////////////////////////*/ - public int getSuggestionMovementFlags(@NonNull final RecyclerView recyclerView, - @NonNull final RecyclerView.ViewHolder viewHolder) { + public int getSuggestionMovementFlags(@NonNull final RecyclerView.ViewHolder viewHolder) { final int position = viewHolder.getAdapterPosition(); if (position == RecyclerView.NO_POSITION) { return 0; @@ -1077,8 +1085,7 @@ public int getSuggestionMovementFlags(@NonNull final RecyclerView recyclerView, ItemTouchHelper.LEFT | ItemTouchHelper.RIGHT) : 0; } - public void onSuggestionItemSwiped(@NonNull final RecyclerView.ViewHolder viewHolder, - final int i) { + public void onSuggestionItemSwiped(@NonNull final RecyclerView.ViewHolder viewHolder) { final int position = viewHolder.getAdapterPosition(); final String query = suggestionListAdapter.getItem(position).query; final Disposable onDelete = historyRecordManager.deleteSearchHistory(query) diff --git a/app/src/main/java/org/schabi/newpipelegacy/fragments/list/videos/RelatedVideosFragment.java b/app/src/main/java/org/schabi/newpipelegacy/fragments/list/videos/RelatedVideosFragment.java index d37a5a953..4ef769f90 100644 --- a/app/src/main/java/org/schabi/newpipelegacy/fragments/list/videos/RelatedVideosFragment.java +++ b/app/src/main/java/org/schabi/newpipelegacy/fragments/list/videos/RelatedVideosFragment.java @@ -16,12 +16,12 @@ import org.schabi.newpipelegacy.R; import org.schabi.newpipe.extractor.ListExtractor; -import org.schabi.newpipe.extractor.NewPipe; import org.schabi.newpipe.extractor.stream.StreamInfo; import org.schabi.newpipelegacy.fragments.list.BaseListInfoFragment; import org.schabi.newpipelegacy.report.UserAction; import org.schabi.newpipelegacy.util.AnimationUtils; import org.schabi.newpipelegacy.util.RelatedStreamInfo; +import org.schabi.newpipelegacy.util.ServiceHelper; import java.io.Serializable; @@ -123,7 +123,7 @@ public void handleResult(@NonNull final RelatedStreamInfo result) { if (!result.getErrors().isEmpty()) { showSnackBarError(result.getErrors(), UserAction.REQUESTED_STREAM, - NewPipe.getNameOfService(result.getServiceId()), result.getUrl(), 0); + ServiceHelper.getNameOfServiceById(result.getServiceId()), result.getUrl(), 0); } if (disposables != null) { @@ -138,7 +138,7 @@ public void handleNextItems(final ListExtractor.InfoItemsPage result) { if (!result.getErrors().isEmpty()) { showSnackBarError(result.getErrors(), UserAction.REQUESTED_STREAM, - NewPipe.getNameOfService(serviceId), + ServiceHelper.getNameOfServiceById(serviceId), "Get next page of: " + url, R.string.general_error); } @@ -156,7 +156,7 @@ protected boolean onError(final Throwable exception) { hideLoading(); showSnackBarError(exception, UserAction.REQUESTED_STREAM, - NewPipe.getNameOfService(serviceId), url, R.string.general_error); + ServiceHelper.getNameOfServiceById(serviceId), url, R.string.general_error); return true; } diff --git a/app/src/main/java/org/schabi/newpipelegacy/info_list/holder/CommentsInfoItemHolder.java b/app/src/main/java/org/schabi/newpipelegacy/info_list/holder/CommentsInfoItemHolder.java index f54af283f..ccb2436a9 100644 --- a/app/src/main/java/org/schabi/newpipelegacy/info_list/holder/CommentsInfoItemHolder.java +++ b/app/src/main/java/org/schabi/newpipelegacy/info_list/holder/CommentsInfoItemHolder.java @@ -1,6 +1,8 @@ package org.schabi.newpipelegacy.info_list.holder; +import android.view.View; import android.view.ViewGroup; +import android.widget.ImageView; import android.widget.TextView; import org.schabi.newpipelegacy.R; @@ -31,11 +33,13 @@ public class CommentsInfoItemHolder extends CommentsMiniInfoItemHolder { public final TextView itemTitleView; + private final ImageView itemHeartView; public CommentsInfoItemHolder(final InfoItemBuilder infoItemBuilder, final ViewGroup parent) { super(infoItemBuilder, R.layout.list_comments_item, parent); itemTitleView = itemView.findViewById(R.id.itemTitleView); + itemHeartView = itemView.findViewById(R.id.detail_heart_image_view); } @Override @@ -49,5 +53,7 @@ public void updateFromItem(final InfoItem infoItem, final CommentsInfoItem item = (CommentsInfoItem) infoItem; itemTitleView.setText(item.getUploaderName()); + + itemHeartView.setVisibility(item.isHeartedByUploader() ? View.VISIBLE : View.GONE); } } diff --git a/app/src/main/java/org/schabi/newpipelegacy/info_list/holder/CommentsMiniInfoItemHolder.java b/app/src/main/java/org/schabi/newpipelegacy/info_list/holder/CommentsMiniInfoItemHolder.java index c8e1d7e9c..705ee5c77 100644 --- a/app/src/main/java/org/schabi/newpipelegacy/info_list/holder/CommentsMiniInfoItemHolder.java +++ b/app/src/main/java/org/schabi/newpipelegacy/info_list/holder/CommentsMiniInfoItemHolder.java @@ -8,6 +8,7 @@ import android.widget.TextView; import androidx.appcompat.app.AppCompatActivity; +import androidx.core.text.util.LinkifyCompat; import org.schabi.newpipelegacy.R; import org.schabi.newpipe.extractor.InfoItem; @@ -22,6 +23,7 @@ import org.schabi.newpipelegacy.util.NavigationHelper; import org.schabi.newpipelegacy.util.ShareUtils; +import java.io.IOException; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -95,7 +97,14 @@ public void updateFromItem(final InfoItem infoItem, streamUrl = item.getUrl(); itemContentView.setLines(COMMENT_DEFAULT_LINES); - commentText = item.getCommentText(); + try { + commentText = item.getCommentText().getContent().replace(" ", "") + .replace("", " ") + .replace("
", " ").replace("", " ") + .replace("
", " ").replace("
", " "); + } catch (Exception e) { + commentText = " "; + } itemContentView.setText(commentText); itemContentView.setOnTouchListener(CommentTextOnTouchListener.INSTANCE); diff --git a/app/src/main/java/org/schabi/newpipelegacy/info_list/holder/StreamMiniInfoItemHolder.java b/app/src/main/java/org/schabi/newpipelegacy/info_list/holder/StreamMiniInfoItemHolder.java index 6159c1710..ed1d2d2c0 100644 --- a/app/src/main/java/org/schabi/newpipelegacy/info_list/holder/StreamMiniInfoItemHolder.java +++ b/app/src/main/java/org/schabi/newpipelegacy/info_list/holder/StreamMiniInfoItemHolder.java @@ -70,7 +70,8 @@ public void updateFromItem(final InfoItem infoItem, } else { itemProgressView.setVisibility(View.GONE); } - } else if (item.getStreamType() == StreamType.LIVE_STREAM) { + } else if (item.getStreamType() == StreamType.LIVE_STREAM + || item.getStreamType() == StreamType.AUDIO_LIVE_STREAM) { itemDurationView.setText(R.string.duration_live); itemDurationView.setBackgroundColor(ContextCompat.getColor(itemBuilder.getContext(), R.color.live_duration_background_color)); @@ -100,7 +101,7 @@ public void updateFromItem(final InfoItem infoItem, case AUDIO_LIVE_STREAM: enableLongClick(item); break; - case FILE: + case POST_LIVE_AUDIO_STREAM: case NONE: default: disableLongClick(); diff --git a/app/src/main/java/org/schabi/newpipelegacy/local/feed/FeedDatabaseManager.kt b/app/src/main/java/org/schabi/newpipelegacy/local/feed/FeedDatabaseManager.kt index 7d41a4134..20c0a77d3 100644 --- a/app/src/main/java/org/schabi/newpipelegacy/local/feed/FeedDatabaseManager.kt +++ b/app/src/main/java/org/schabi/newpipelegacy/local/feed/FeedDatabaseManager.kt @@ -7,8 +7,6 @@ import io.reactivex.Flowable import io.reactivex.Maybe import io.reactivex.android.schedulers.AndroidSchedulers import io.reactivex.schedulers.Schedulers -import java.util.Calendar -import java.util.Date import org.schabi.newpipe.extractor.stream.StreamInfoItem import org.schabi.newpipe.extractor.stream.StreamType import org.schabi.newpipelegacy.MainActivity.DEBUG @@ -18,6 +16,8 @@ import org.schabi.newpipelegacy.database.feed.model.FeedGroupEntity import org.schabi.newpipelegacy.database.feed.model.FeedLastUpdatedEntity import org.schabi.newpipelegacy.database.stream.model.StreamEntity import org.schabi.newpipelegacy.local.subscription.FeedGroupIcon +import java.util.Calendar +import java.util.Date class FeedDatabaseManager(context: Context) { private val database = NewPipeDatabase.getInstance(context) @@ -65,10 +65,10 @@ class FeedDatabaseManager(context: Context) { } fun outdatedSubscriptionsForGroup(groupId: Long = FeedGroupEntity.GROUP_ALL_ID, outdatedThreshold: Date) = - feedTable.getAllOutdatedForGroup(groupId, outdatedThreshold) + feedTable.getAllOutdatedForGroup(groupId, outdatedThreshold) fun markAsOutdated(subscriptionId: Long) = feedTable - .setLastUpdatedForSubscription(FeedLastUpdatedEntity(subscriptionId, null)) + .setLastUpdatedForSubscription(FeedLastUpdatedEntity(subscriptionId, null)) fun upsertAll( subscriptionId: Long, @@ -116,38 +116,38 @@ class FeedDatabaseManager(context: Context) { fun subscriptionIdsForGroup(groupId: Long): Flowable> { return feedGroupTable.getSubscriptionIdsFor(groupId) - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) } fun updateSubscriptionsForGroup(groupId: Long, subscriptionIds: List): Completable { return Completable.fromCallable { feedGroupTable.updateSubscriptionsForGroup(groupId, subscriptionIds) } - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) } fun createGroup(name: String, icon: FeedGroupIcon): Maybe { return Maybe.fromCallable { feedGroupTable.insert(FeedGroupEntity(0, name, icon)) } - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) } fun getGroup(groupId: Long): Maybe { return feedGroupTable.getGroup(groupId) - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) } fun updateGroup(feedGroupEntity: FeedGroupEntity): Completable { return Completable.fromCallable { feedGroupTable.update(feedGroupEntity) } - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) } fun deleteGroup(groupId: Long): Completable { return Completable.fromCallable { feedGroupTable.delete(groupId) } - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) } fun updateGroupsOrder(groupIdList: List): Completable { @@ -155,8 +155,8 @@ class FeedDatabaseManager(context: Context) { val orderMap = groupIdList.associateBy({ it }, { index++ }) return Completable.fromCallable { feedGroupTable.updateOrder(orderMap) } - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) } fun oldestSubscriptionUpdate(groupId: Long): Flowable> { diff --git a/app/src/main/java/org/schabi/newpipelegacy/local/feed/FeedFragment.kt b/app/src/main/java/org/schabi/newpipelegacy/local/feed/FeedFragment.kt index 2f4018b29..406ee80b0 100644 --- a/app/src/main/java/org/schabi/newpipelegacy/local/feed/FeedFragment.kt +++ b/app/src/main/java/org/schabi/newpipelegacy/local/feed/FeedFragment.kt @@ -32,8 +32,8 @@ import androidx.appcompat.app.AlertDialog import androidx.lifecycle.Observer import androidx.lifecycle.ViewModelProviders import androidx.preference.PreferenceManager +import androidx.swiperefreshlayout.widget.SwipeRefreshLayout import icepick.State -import java.util.Calendar import kotlinx.android.synthetic.main.error_retry.error_button_retry import kotlinx.android.synthetic.main.error_retry.error_message_view import kotlinx.android.synthetic.main.fragment_feed.empty_state_view @@ -51,9 +51,11 @@ import org.schabi.newpipelegacy.local.feed.service.FeedLoadService import org.schabi.newpipelegacy.report.UserAction import org.schabi.newpipelegacy.util.AnimationUtils.animateView import org.schabi.newpipelegacy.util.Localization +import java.util.Calendar class FeedFragment : BaseListFragment() { private lateinit var viewModel: FeedViewModel + private lateinit var swipeRefreshLayout: SwipeRefreshLayout @State @JvmField var listState: Parcelable? = null @@ -71,7 +73,7 @@ class FeedFragment : BaseListFragment() { super.onCreate(savedInstanceState) groupId = arguments?.getLong(KEY_GROUP_ID, FeedGroupEntity.GROUP_ALL_ID) - ?: FeedGroupEntity.GROUP_ALL_ID + ?: FeedGroupEntity.GROUP_ALL_ID groupName = arguments?.getString(KEY_GROUP_NAME) ?: "" } @@ -81,7 +83,8 @@ class FeedFragment : BaseListFragment() { override fun onViewCreated(rootView: View, savedInstanceState: Bundle?) { super.onViewCreated(rootView, savedInstanceState) - + swipeRefreshLayout = requireView().findViewById(R.id.swiperefresh) + swipeRefreshLayout.setOnRefreshListener { reloadContent() } viewModel = ViewModelProviders.of(this, FeedViewModel.Factory(requireContext(), groupId)).get(FeedViewModel::class.java) viewModel.stateLiveData.observe(viewLifecycleOwner, Observer { it?.let(::handleResult) }) } @@ -138,15 +141,15 @@ class FeedFragment : BaseListFragment() { } AlertDialog.Builder(requireContext()) - .setMessage(R.string.feed_use_dedicated_fetch_method_help_text) - .setNeutralButton(enableDisableButtonText) { _, _ -> - sharedPreferences.edit() - .putBoolean(getString(R.string.feed_use_dedicated_fetch_method_key), !usingDedicatedMethod) - .apply() - } - .setPositiveButton(resources.getString(R.string.finish), null) - .create() - .show() + .setMessage(R.string.feed_use_dedicated_fetch_method_help_text) + .setNeutralButton(enableDisableButtonText) { _, _ -> + sharedPreferences.edit() + .putBoolean(getString(R.string.feed_use_dedicated_fetch_method_key), !usingDedicatedMethod) + .apply() + } + .setPositiveButton(resources.getString(R.string.finish), null) + .create() + .show() return true } @@ -176,6 +179,7 @@ class FeedFragment : BaseListFragment() { empty_state_view?.let { animateView(it, false, 0) } animateView(error_panel, false, 0) + swipeRefreshLayout.isRefreshing = false } override fun hideLoading() { @@ -227,7 +231,7 @@ class FeedFragment : BaseListFragment() { showLoading() val isIndeterminate = progressState.currentProgress == -1 && - progressState.maxProgress == -1 + progressState.maxProgress == -1 if (!isIndeterminate) { loading_progress_text.text = "${progressState.currentProgress}/${progressState.maxProgress}" @@ -238,7 +242,7 @@ class FeedFragment : BaseListFragment() { } loading_progress_bar.isIndeterminate = isIndeterminate || - (progressState.maxProgress > 0 && progressState.currentProgress == 0) + (progressState.maxProgress > 0 && progressState.currentProgress == 0) loading_progress_bar.progress = progressState.currentProgress loading_progress_bar.max = progressState.maxProgress @@ -261,8 +265,10 @@ class FeedFragment : BaseListFragment() { } if (loadedState.itemsErrors.isNotEmpty()) { - showSnackBarError(loadedState.itemsErrors, UserAction.REQUESTED_FEED, - "none", "Loading feed", R.string.general_error) + showSnackBarError( + loadedState.itemsErrors, UserAction.REQUESTED_FEED, + "none", "Loading feed", R.string.general_error + ) } if (loadedState.items.isEmpty()) { @@ -305,9 +311,11 @@ class FeedFragment : BaseListFragment() { override fun hasMoreItems() = false private fun triggerUpdate() { - getActivity()?.startService(Intent(requireContext(), FeedLoadService::class.java).apply { - putExtra(FeedLoadService.EXTRA_GROUP_ID, groupId) - }) + getActivity()?.startService( + Intent(requireContext(), FeedLoadService::class.java).apply { + putExtra(FeedLoadService.EXTRA_GROUP_ID, groupId) + } + ) listState = null } diff --git a/app/src/main/java/org/schabi/newpipelegacy/local/feed/FeedState.kt b/app/src/main/java/org/schabi/newpipelegacy/local/feed/FeedState.kt index a02f80265..683f1910a 100644 --- a/app/src/main/java/org/schabi/newpipelegacy/local/feed/FeedState.kt +++ b/app/src/main/java/org/schabi/newpipelegacy/local/feed/FeedState.kt @@ -1,8 +1,8 @@ package org.schabi.newpipelegacy.local.feed import androidx.annotation.StringRes -import java.util.Calendar import org.schabi.newpipe.extractor.stream.StreamInfoItem +import java.util.Calendar sealed class FeedState { data class ProgressState( diff --git a/app/src/main/java/org/schabi/newpipelegacy/local/feed/FeedViewModel.kt b/app/src/main/java/org/schabi/newpipelegacy/local/feed/FeedViewModel.kt index 79736c5ec..c100a3857 100644 --- a/app/src/main/java/org/schabi/newpipelegacy/local/feed/FeedViewModel.kt +++ b/app/src/main/java/org/schabi/newpipelegacy/local/feed/FeedViewModel.kt @@ -9,9 +9,6 @@ import io.reactivex.Flowable import io.reactivex.android.schedulers.AndroidSchedulers import io.reactivex.functions.Function4 import io.reactivex.schedulers.Schedulers -import java.util.Calendar -import java.util.Date -import java.util.concurrent.TimeUnit import org.schabi.newpipe.extractor.stream.StreamInfoItem import org.schabi.newpipelegacy.database.feed.model.FeedGroupEntity import org.schabi.newpipelegacy.local.feed.service.FeedEventManager @@ -20,6 +17,9 @@ import org.schabi.newpipelegacy.local.feed.service.FeedEventManager.Event.IdleEv import org.schabi.newpipelegacy.local.feed.service.FeedEventManager.Event.ProgressEvent import org.schabi.newpipelegacy.local.feed.service.FeedEventManager.Event.SuccessResultEvent import org.schabi.newpipelegacy.util.DEFAULT_THROTTLE_TIMEOUT +import java.util.Calendar +import java.util.Date +import java.util.concurrent.TimeUnit class FeedViewModel(applicationContext: Context, val groupId: Long = FeedGroupEntity.GROUP_ALL_ID) : ViewModel() { class Factory(val context: Context, val groupId: Long = FeedGroupEntity.GROUP_ALL_ID) : ViewModelProvider.Factory { @@ -35,36 +35,38 @@ class FeedViewModel(applicationContext: Context, val groupId: Long = FeedGroupEn val stateLiveData: LiveData = mutableStateLiveData private var combineDisposable = Flowable - .combineLatest( - FeedEventManager.events(), - feedDatabaseManager.asStreamItems(groupId), - feedDatabaseManager.notLoadedCount(groupId), - feedDatabaseManager.oldestSubscriptionUpdate(groupId), + .combineLatest( + FeedEventManager.events(), + feedDatabaseManager.asStreamItems(groupId), + feedDatabaseManager.notLoadedCount(groupId), + feedDatabaseManager.oldestSubscriptionUpdate(groupId), - Function4 { t1: FeedEventManager.Event, t2: List, t3: Long, t4: List -> - return@Function4 CombineResultHolder(t1, t2, t3, t4.firstOrNull()) - } - ) - .throttleLatest(DEFAULT_THROTTLE_TIMEOUT, TimeUnit.MILLISECONDS) - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe { - val (event, listFromDB, notLoadedCount, oldestUpdate) = it + Function4 { t1: FeedEventManager.Event, t2: List, t3: Long, t4: List -> + return@Function4 CombineResultHolder(t1, t2, t3, t4.firstOrNull()) + } + ) + .throttleLatest(DEFAULT_THROTTLE_TIMEOUT, TimeUnit.MILLISECONDS) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe { + val (event, listFromDB, notLoadedCount, oldestUpdate) = it - val oldestUpdateCalendar = - oldestUpdate?.let { Calendar.getInstance().apply { time = it } } + val oldestUpdateCalendar = + oldestUpdate?.let { Calendar.getInstance().apply { time = it } } - mutableStateLiveData.postValue(when (event) { + mutableStateLiveData.postValue( + when (event) { is IdleEvent -> FeedState.LoadedState(listFromDB, oldestUpdateCalendar, notLoadedCount) is ProgressEvent -> FeedState.ProgressState(event.currentProgress, event.maxProgress, event.progressMessage) is SuccessResultEvent -> FeedState.LoadedState(listFromDB, oldestUpdateCalendar, notLoadedCount, event.itemsErrors) is ErrorResultEvent -> FeedState.ErrorState(event.error) - }) - - if (event is ErrorResultEvent || event is SuccessResultEvent) { - FeedEventManager.reset() } + ) + + if (event is ErrorResultEvent || event is SuccessResultEvent) { + FeedEventManager.reset() } + } override fun onCleared() { super.onCleared() diff --git a/app/src/main/java/org/schabi/newpipelegacy/local/feed/service/FeedEventManager.kt b/app/src/main/java/org/schabi/newpipelegacy/local/feed/service/FeedEventManager.kt index cb8831249..fd0235436 100644 --- a/app/src/main/java/org/schabi/newpipelegacy/local/feed/service/FeedEventManager.kt +++ b/app/src/main/java/org/schabi/newpipelegacy/local/feed/service/FeedEventManager.kt @@ -3,8 +3,8 @@ package org.schabi.newpipelegacy.local.feed.service import androidx.annotation.StringRes import io.reactivex.Flowable import io.reactivex.processors.BehaviorProcessor -import java.util.concurrent.atomic.AtomicBoolean import org.schabi.newpipelegacy.local.feed.service.FeedEventManager.Event.IdleEvent +import java.util.concurrent.atomic.AtomicBoolean object FeedEventManager { private var processor: BehaviorProcessor = BehaviorProcessor.create() diff --git a/app/src/main/java/org/schabi/newpipelegacy/local/feed/service/FeedLoadService.kt b/app/src/main/java/org/schabi/newpipelegacy/local/feed/service/FeedLoadService.kt index 72e601be8..72ebdd682 100644 --- a/app/src/main/java/org/schabi/newpipelegacy/local/feed/service/FeedLoadService.kt +++ b/app/src/main/java/org/schabi/newpipelegacy/local/feed/service/FeedLoadService.kt @@ -40,16 +40,12 @@ import io.reactivex.functions.Consumer import io.reactivex.functions.Function import io.reactivex.processors.PublishProcessor import io.reactivex.schedulers.Schedulers -import java.io.IOException -import java.util.Calendar -import java.util.concurrent.TimeUnit -import java.util.concurrent.atomic.AtomicBoolean -import java.util.concurrent.atomic.AtomicInteger import org.reactivestreams.Subscriber import org.reactivestreams.Subscription import org.schabi.newpipe.extractor.ListInfo import org.schabi.newpipe.extractor.exceptions.ReCaptchaException import org.schabi.newpipe.extractor.stream.StreamInfoItem +import org.schabi.newpipelegacy.App import org.schabi.newpipelegacy.MainActivity.DEBUG import org.schabi.newpipelegacy.R import org.schabi.newpipelegacy.database.feed.model.FeedGroupEntity @@ -62,12 +58,17 @@ import org.schabi.newpipelegacy.local.feed.service.FeedEventManager.postEvent import org.schabi.newpipelegacy.local.subscription.SubscriptionManager import org.schabi.newpipelegacy.util.ExceptionUtils import org.schabi.newpipelegacy.util.ExtractorHelper +import java.io.IOException +import java.util.Calendar +import java.util.concurrent.TimeUnit +import java.util.concurrent.atomic.AtomicBoolean +import java.util.concurrent.atomic.AtomicInteger class FeedLoadService : Service() { companion object { private val TAG = FeedLoadService::class.java.simpleName private const val NOTIFICATION_ID = 7293450 - private const val ACTION_CANCEL = "org.schabi.newpipe.local.feed.service.FeedLoadService.CANCEL" + private const val ACTION_CANCEL = App.PACKAGE_NAME + ".local.feed.service.FeedLoadService.CANCEL" /** * How often the notification will be updated. @@ -108,8 +109,11 @@ class FeedLoadService : Service() { override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { if (DEBUG) { - Log.d(TAG, "onStartCommand() called with: intent = [" + intent + "]," + - " flags = [" + flags + "], startId = [" + startId + "]") + Log.d( + TAG, + "onStartCommand() called with: intent = [" + intent + "]," + + " flags = [" + flags + "], startId = [" + startId + "]" + ) } if (intent == null || loadingSubscription != null) { @@ -122,10 +126,10 @@ class FeedLoadService : Service() { val groupId = intent.getLongExtra(EXTRA_GROUP_ID, FeedGroupEntity.GROUP_ALL_ID) val useFeedExtractor = defaultSharedPreferences - .getBoolean(getString(R.string.feed_use_dedicated_fetch_method_key), false) + .getBoolean(getString(R.string.feed_use_dedicated_fetch_method_key), false) val thresholdOutdatedSecondsString = defaultSharedPreferences - .getString(getString(R.string.feed_update_threshold_key), getString(R.string.feed_update_threshold_default_value)) + .getString(getString(R.string.feed_update_threshold_key), getString(R.string.feed_update_threshold_default_value)) val thresholdOutdatedSeconds = thresholdOutdatedSecondsString!!.toInt() startLoading(groupId, useFeedExtractor, thresholdOutdatedSeconds) @@ -182,63 +186,63 @@ class FeedLoadService : Service() { } subscriptions - .limit(1) + .limit(1) - .doOnNext { - currentProgress.set(0) - maxProgress.set(it.size) - } - .filter { it.isNotEmpty() } + .doOnNext { + currentProgress.set(0) + maxProgress.set(it.size) + } + .filter { it.isNotEmpty() } - .observeOn(AndroidSchedulers.mainThread()) - .doOnNext { - startForeground(NOTIFICATION_ID, notificationBuilder.build()) - updateNotificationProgress(null) - broadcastProgress() - } + .observeOn(AndroidSchedulers.mainThread()) + .doOnNext { + startForeground(NOTIFICATION_ID, notificationBuilder.build()) + updateNotificationProgress(null) + broadcastProgress() + } - .observeOn(Schedulers.io()) - .flatMap { Flowable.fromIterable(it) } - .takeWhile { !cancelSignal.get() } - - .parallel(PARALLEL_EXTRACTIONS, PARALLEL_EXTRACTIONS * 2) - .runOn(Schedulers.io(), PARALLEL_EXTRACTIONS * 2) - .filter { !cancelSignal.get() } - - .map { subscriptionEntity -> - try { - val listInfo = if (useFeedExtractor) { - ExtractorHelper - .getFeedInfoFallbackToChannelInfo(subscriptionEntity.serviceId, subscriptionEntity.url) - .blockingGet() - } else { - ExtractorHelper - .getChannelInfo(subscriptionEntity.serviceId, subscriptionEntity.url, true) - .blockingGet() - } as ListInfo - - return@map Notification.createOnNext(Pair(subscriptionEntity.uid, listInfo)) - } catch (e: Throwable) { - val request = "${subscriptionEntity.serviceId}:${subscriptionEntity.url}" - val wrapper = RequestException(subscriptionEntity.uid, request, e) - return@map Notification.createOnError>>(wrapper) - } + .observeOn(Schedulers.io()) + .flatMap { Flowable.fromIterable(it) } + .takeWhile { !cancelSignal.get() } + + .parallel(PARALLEL_EXTRACTIONS, PARALLEL_EXTRACTIONS * 2) + .runOn(Schedulers.io(), PARALLEL_EXTRACTIONS * 2) + .filter { !cancelSignal.get() } + + .map { subscriptionEntity -> + try { + val listInfo = if (useFeedExtractor) { + ExtractorHelper + .getFeedInfoFallbackToChannelInfo(subscriptionEntity.serviceId, subscriptionEntity.url) + .blockingGet() + } else { + ExtractorHelper + .getChannelInfo(subscriptionEntity.serviceId, subscriptionEntity.url, true) + .blockingGet() + } as ListInfo + + return@map Notification.createOnNext(Pair(subscriptionEntity.uid, listInfo)) + } catch (e: Throwable) { + val request = "${subscriptionEntity.serviceId}:${subscriptionEntity.url}" + val wrapper = RequestException(subscriptionEntity.uid, request, e) + return@map Notification.createOnError>>(wrapper) } - .sequential() + } + .sequential() - .observeOn(AndroidSchedulers.mainThread()) - .doOnNext(errorHandlingConsumer) + .observeOn(AndroidSchedulers.mainThread()) + .doOnNext(errorHandlingConsumer) - .observeOn(AndroidSchedulers.mainThread()) - .doOnNext(notificationsConsumer) + .observeOn(AndroidSchedulers.mainThread()) + .doOnNext(notificationsConsumer) - .observeOn(Schedulers.io()) - .buffer(BUFFER_COUNT_BEFORE_INSERT) - .doOnNext(databaseConsumer) + .observeOn(Schedulers.io()) + .buffer(BUFFER_COUNT_BEFORE_INSERT) + .doOnNext(databaseConsumer) - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe(resultSubscriber) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(resultSubscriber) } private fun broadcastProgress() { @@ -275,7 +279,8 @@ class FeedLoadService : Service() { notificationUpdater.onNext(getString(R.string.feed_processing_message)) postEvent(ProgressEvent(R.string.feed_processing_message)) - disposables.add(Single + disposables.add( + Single .fromCallable { feedResultsHolder.ready() @@ -294,7 +299,8 @@ class FeedLoadService : Service() { return@subscribe } stopService() - }) + } + ) } } @@ -365,16 +371,18 @@ class FeedLoadService : Service() { private var maxProgress = AtomicInteger(-1) private fun createNotification(): NotificationCompat.Builder { - val cancelActionIntent = PendingIntent.getBroadcast(this, - NOTIFICATION_ID, Intent(ACTION_CANCEL), 0) + val cancelActionIntent = PendingIntent.getBroadcast( + this, + NOTIFICATION_ID, Intent(ACTION_CANCEL), 0 + ) return NotificationCompat.Builder(this, getString(R.string.notification_channel_id)) - .setOngoing(true) - .setProgress(-1, -1, true) - .setSmallIcon(R.drawable.ic_newpipe_triangle_white) - .setVisibility(NotificationCompat.VISIBILITY_PUBLIC) - .addAction(0, getString(R.string.cancel), cancelActionIntent) - .setContentTitle(getString(R.string.feed_notification_loading)) + .setOngoing(true) + .setProgress(-1, -1, true) + .setSmallIcon(R.drawable.ic_newpipe_triangle_white) + .setVisibility(NotificationCompat.VISIBILITY_PUBLIC) + .addAction(0, getString(R.string.cancel), cancelActionIntent) + .setContentTitle(getString(R.string.feed_notification_loading)) } private fun setupNotification() { @@ -385,10 +393,12 @@ class FeedLoadService : Service() { flow.limit(1).concatWith(flow.skip(1).throttleLatest(NOTIFICATION_SAMPLING_PERIOD.toLong(), TimeUnit.MILLISECONDS)) } - disposables.add(notificationUpdater + disposables.add( + notificationUpdater .publish(throttleAfterFirstEmission) .observeOn(AndroidSchedulers.mainThread()) - .subscribe(this::updateNotificationProgress)) + .subscribe(this::updateNotificationProgress) + ) } private fun updateNotificationProgress(updateDescription: String?) { diff --git a/app/src/main/java/org/schabi/newpipelegacy/local/holder/LocalPlaylistStreamItemHolder.java b/app/src/main/java/org/schabi/newpipelegacy/local/holder/LocalPlaylistStreamItemHolder.java index 8441805bf..56166b42b 100644 --- a/app/src/main/java/org/schabi/newpipelegacy/local/holder/LocalPlaylistStreamItemHolder.java +++ b/app/src/main/java/org/schabi/newpipelegacy/local/holder/LocalPlaylistStreamItemHolder.java @@ -11,17 +11,15 @@ import org.schabi.newpipelegacy.R; import org.schabi.newpipelegacy.database.LocalItem; import org.schabi.newpipelegacy.database.playlist.PlaylistStreamEntry; -import org.schabi.newpipelegacy.database.stream.model.StreamStateEntity; -import org.schabi.newpipe.extractor.NewPipe; import org.schabi.newpipelegacy.local.LocalItemBuilder; import org.schabi.newpipelegacy.local.history.HistoryRecordManager; import org.schabi.newpipelegacy.util.AnimationUtils; import org.schabi.newpipelegacy.util.ImageDisplayConstants; import org.schabi.newpipelegacy.util.Localization; +import org.schabi.newpipelegacy.util.ServiceHelper; import org.schabi.newpipelegacy.views.AnimatedProgressBar; import java.text.DateFormat; -import java.util.ArrayList; import java.util.concurrent.TimeUnit; public class LocalPlaylistStreamItemHolder extends LocalItemHolder { @@ -61,7 +59,7 @@ public void updateFromItem(final LocalItem localItem, itemVideoTitleView.setText(item.getStreamEntity().getTitle()); itemAdditionalDetailsView.setText(Localization .concatenateStrings(item.getStreamEntity().getUploader(), - NewPipe.getNameOfService(item.getStreamEntity().getServiceId()))); + ServiceHelper.getNameOfServiceById(item.getStreamEntity().getServiceId()))); if (item.getStreamEntity().getDuration() > 0) { itemDurationView.setText(Localization @@ -70,15 +68,11 @@ public void updateFromItem(final LocalItem localItem, R.color.duration_background_color)); itemDurationView.setVisibility(View.VISIBLE); - StreamStateEntity state = historyRecordManager - .loadLocalStreamStateBatch(new ArrayList() {{ - add(localItem); - }}).blockingGet().get(0); - if (state != null) { + if (item.getProgressTime() > 0) { itemProgressView.setVisibility(View.VISIBLE); itemProgressView.setMax((int) item.getStreamEntity().getDuration()); itemProgressView.setProgress((int) TimeUnit.MILLISECONDS - .toSeconds(state.getProgressTime())); + .toSeconds(item.getProgressTime())); } else { itemProgressView.setVisibility(View.GONE); } @@ -116,18 +110,14 @@ public void updateState(final LocalItem localItem, } final PlaylistStreamEntry item = (PlaylistStreamEntry) localItem; - StreamStateEntity state = historyRecordManager - .loadLocalStreamStateBatch(new ArrayList() {{ - add(localItem); - }}).blockingGet().get(0); - if (state != null && item.getStreamEntity().getDuration() > 0) { + if (item.getProgressTime() > 0 && item.getStreamEntity().getDuration() > 0) { itemProgressView.setMax((int) item.getStreamEntity().getDuration()); if (itemProgressView.getVisibility() == View.VISIBLE) { itemProgressView.setProgressAnimated((int) TimeUnit.MILLISECONDS - .toSeconds(state.getProgressTime())); + .toSeconds(item.getProgressTime())); } else { itemProgressView.setProgress((int) TimeUnit.MILLISECONDS - .toSeconds(state.getProgressTime())); + .toSeconds(item.getProgressTime())); AnimationUtils.animateView(itemProgressView, true, 500); } } else if (itemProgressView.getVisibility() == View.VISIBLE) { diff --git a/app/src/main/java/org/schabi/newpipelegacy/local/holder/LocalStatisticStreamItemHolder.java b/app/src/main/java/org/schabi/newpipelegacy/local/holder/LocalStatisticStreamItemHolder.java index 33d3e2682..f373277ad 100644 --- a/app/src/main/java/org/schabi/newpipelegacy/local/holder/LocalStatisticStreamItemHolder.java +++ b/app/src/main/java/org/schabi/newpipelegacy/local/holder/LocalStatisticStreamItemHolder.java @@ -11,17 +11,15 @@ import org.schabi.newpipelegacy.R; import org.schabi.newpipelegacy.database.LocalItem; import org.schabi.newpipelegacy.database.stream.StreamStatisticsEntry; -import org.schabi.newpipelegacy.database.stream.model.StreamStateEntity; -import org.schabi.newpipe.extractor.NewPipe; import org.schabi.newpipelegacy.local.LocalItemBuilder; import org.schabi.newpipelegacy.local.history.HistoryRecordManager; import org.schabi.newpipelegacy.util.AnimationUtils; import org.schabi.newpipelegacy.util.ImageDisplayConstants; import org.schabi.newpipelegacy.util.Localization; +import org.schabi.newpipelegacy.util.ServiceHelper; import org.schabi.newpipelegacy.views.AnimatedProgressBar; import java.text.DateFormat; -import java.util.ArrayList; import java.util.concurrent.TimeUnit; /* @@ -75,7 +73,9 @@ private String getStreamInfoDetailLine(final StreamStatisticsEntry entry, final String watchCount = Localization .shortViewCount(itemBuilder.getContext(), entry.getWatchCount()); final String uploadDate = dateFormat.format(entry.getLatestAccessDate()); - final String serviceName = NewPipe.getNameOfService(entry.getStreamEntity().getServiceId()); + final String serviceName = ServiceHelper.getNameOfServiceById( + entry.getStreamEntity().getServiceId() + ); return Localization.concatenateStrings(watchCount, uploadDate, serviceName); } @@ -98,15 +98,11 @@ public void updateFromItem(final LocalItem localItem, R.color.duration_background_color)); itemDurationView.setVisibility(View.VISIBLE); - StreamStateEntity state = historyRecordManager - .loadLocalStreamStateBatch(new ArrayList() {{ - add(localItem); - }}).blockingGet().get(0); - if (state != null) { + if (item.getProgressTime() > 0) { itemProgressView.setVisibility(View.VISIBLE); itemProgressView.setMax((int) item.getStreamEntity().getDuration()); itemProgressView.setProgress((int) TimeUnit.MILLISECONDS - .toSeconds(state.getProgressTime())); + .toSeconds(item.getProgressTime())); } else { itemProgressView.setVisibility(View.GONE); } @@ -146,18 +142,14 @@ public void updateState(final LocalItem localItem, } final StreamStatisticsEntry item = (StreamStatisticsEntry) localItem; - StreamStateEntity state = historyRecordManager - .loadLocalStreamStateBatch(new ArrayList() {{ - add(localItem); - }}).blockingGet().get(0); - if (state != null && item.getStreamEntity().getDuration() > 0) { + if (item.getProgressTime() > 0 && item.getStreamEntity().getDuration() > 0) { itemProgressView.setMax((int) item.getStreamEntity().getDuration()); if (itemProgressView.getVisibility() == View.VISIBLE) { itemProgressView.setProgressAnimated((int) TimeUnit.MILLISECONDS - .toSeconds(state.getProgressTime())); + .toSeconds(item.getProgressTime())); } else { itemProgressView.setProgress((int) TimeUnit.MILLISECONDS - .toSeconds(state.getProgressTime())); + .toSeconds(item.getProgressTime())); AnimationUtils.animateView(itemProgressView, true, 500); } } else if (itemProgressView.getVisibility() == View.VISIBLE) { diff --git a/app/src/main/java/org/schabi/newpipelegacy/local/holder/RemotePlaylistItemHolder.java b/app/src/main/java/org/schabi/newpipelegacy/local/holder/RemotePlaylistItemHolder.java index bb09ff659..003ba7df8 100644 --- a/app/src/main/java/org/schabi/newpipelegacy/local/holder/RemotePlaylistItemHolder.java +++ b/app/src/main/java/org/schabi/newpipelegacy/local/holder/RemotePlaylistItemHolder.java @@ -5,11 +5,11 @@ import org.schabi.newpipelegacy.database.LocalItem; import org.schabi.newpipelegacy.database.playlist.model.PlaylistRemoteEntity; -import org.schabi.newpipe.extractor.NewPipe; import org.schabi.newpipelegacy.local.LocalItemBuilder; import org.schabi.newpipelegacy.local.history.HistoryRecordManager; import org.schabi.newpipelegacy.util.ImageDisplayConstants; import org.schabi.newpipelegacy.util.Localization; +import org.schabi.newpipelegacy.util.ServiceHelper; import java.text.DateFormat; @@ -39,9 +39,9 @@ public void updateFromItem(final LocalItem localItem, // Here is where the uploader name is set in the bookmarked playlists library if (!TextUtils.isEmpty(item.getUploader())) { itemUploaderView.setText(Localization.concatenateStrings(item.getUploader(), - NewPipe.getNameOfService(item.getServiceId()))); + ServiceHelper.getNameOfServiceById(item.getServiceId()))); } else { - itemUploaderView.setText(NewPipe.getNameOfService(item.getServiceId())); + itemUploaderView.setText(ServiceHelper.getNameOfServiceById(item.getServiceId())); } diff --git a/app/src/main/java/org/schabi/newpipelegacy/local/playlist/LocalPlaylistFragment.java b/app/src/main/java/org/schabi/newpipelegacy/local/playlist/LocalPlaylistFragment.java index a6d8e6a5f..f162710fa 100644 --- a/app/src/main/java/org/schabi/newpipelegacy/local/playlist/LocalPlaylistFragment.java +++ b/app/src/main/java/org/schabi/newpipelegacy/local/playlist/LocalPlaylistFragment.java @@ -350,31 +350,32 @@ public void onError(final Throwable exception) { } @Override - public void onComplete() { } + public void onComplete() { + } }; } @Override public boolean onOptionsItemSelected(final MenuItem item) { - switch (item.getItemId()) { - case R.id.menu_item_remove_watched: - if (!isRemovingWatched) { - new AlertDialog.Builder(requireContext()) - .setMessage(R.string.remove_watched_popup_warning) - .setTitle(R.string.remove_watched_popup_title) - .setPositiveButton(R.string.yes, - (DialogInterface d, int id) -> removeWatchedStreams(false)) - .setNeutralButton( - R.string.remove_watched_popup_yes_and_partially_watched_videos, - (DialogInterface d, int id) -> removeWatchedStreams(true)) - .setNegativeButton(R.string.cancel, - (DialogInterface d, int id) -> d.cancel()) - .create() - .show(); - } - break; - default: - return super.onOptionsItemSelected(item); + if (item.getItemId() == R.id.menu_item_remove_watched) { + if (!isRemovingWatched) { + new AlertDialog.Builder(requireContext()) + .setMessage(R.string.remove_watched_popup_warning) + .setTitle(R.string.remove_watched_popup_title) + .setPositiveButton(R.string.yes, + (DialogInterface d, int id) -> removeWatchedStreams(false)) + .setNeutralButton( + R.string.remove_watched_popup_yes_and_partially_watched_videos, + (DialogInterface d, int id) -> removeWatchedStreams(true)) + .setNegativeButton(R.string.cancel, + (DialogInterface d, int id) -> d.cancel()) + .create() + .show(); + } + } else if (item.getItemId() == R.id.menu_item_rename_playlist) { + createRenameDialog(); + } else { + return super.onOptionsItemSelected(item); } return true; } @@ -432,7 +433,7 @@ public void removeWatchedStreams(final boolean removePartiallyWatched) { playlistItem.getStreamId()); final boolean hasState = streamStatesIter.next() != null; - if (indexInHistory < 0 || hasState) { + if (indexInHistory < 0 || hasState) { notWatchedItems.add(playlistItem); } else if (!thumbnailVideoRemoved && playlistManager.getPlaylistThumbnail(playlistId) diff --git a/app/src/main/java/org/schabi/newpipelegacy/local/subscription/SubscriptionFragment.kt b/app/src/main/java/org/schabi/newpipelegacy/local/subscription/SubscriptionFragment.kt index 74d8e5de9..d9645d32d 100644 --- a/app/src/main/java/org/schabi/newpipelegacy/local/subscription/SubscriptionFragment.kt +++ b/app/src/main/java/org/schabi/newpipelegacy/local/subscription/SubscriptionFragment.kt @@ -29,12 +29,6 @@ import com.xwray.groupie.Section import com.xwray.groupie.kotlinandroidextensions.GroupieViewHolder import icepick.State import io.reactivex.disposables.CompositeDisposable -import java.io.File -import java.text.SimpleDateFormat -import java.util.Date -import java.util.Locale -import kotlin.math.floor -import kotlin.math.max import kotlinx.android.synthetic.main.dialog_title.view.itemAdditionalDetails import kotlinx.android.synthetic.main.dialog_title.view.itemTitleView import kotlinx.android.synthetic.main.fragment_subscription.items_list @@ -68,6 +62,12 @@ import org.schabi.newpipelegacy.util.NavigationHelper import org.schabi.newpipelegacy.util.OnClickGesture import org.schabi.newpipelegacy.util.ShareUtils import org.schabi.newpipelegacy.util.ThemeHelper +import java.io.File +import java.text.SimpleDateFormat +import java.util.Date +import java.util.Locale +import kotlin.math.floor +import kotlin.math.max class SubscriptionFragment : BaseStateFragment() { private lateinit var viewModel: SubscriptionViewModel @@ -208,14 +208,19 @@ class SubscriptionFragment : BaseStateFragment() { if (!exportFile.parentFile.canWrite() || !exportFile.parentFile.canRead()) { Toast.makeText(activity, R.string.invalid_directory, Toast.LENGTH_SHORT).show() } else { - activity.startService(Intent(activity, SubscriptionsExportService::class.java) - .putExtra(KEY_FILE_PATH, exportFile.absolutePath)) + activity.startService( + Intent(activity, SubscriptionsExportService::class.java) + .putExtra(KEY_FILE_PATH, exportFile.absolutePath) + ) } } else if (requestCode == REQUEST_IMPORT_CODE) { val path = Utils.getFileForUri(data.data!!).absolutePath - ImportConfirmationDialog.show(this, Intent(activity, SubscriptionsImportService::class.java) + ImportConfirmationDialog.show( + this, + Intent(activity, SubscriptionsImportService::class.java) .putExtra(KEY_MODE, PREVIOUS_EXPORT_MODE) - .putExtra(KEY_VALUE, path)) + .putExtra(KEY_VALUE, path) + ) } } } @@ -247,9 +252,9 @@ class SubscriptionFragment : BaseStateFragment() { feedGroupsCarousel = FeedGroupCarouselItem(requireContext(), carouselAdapter) feedGroupsSortMenuItem = HeaderWithMenuItem( - getString(R.string.feed_groups_header_title), - ThemeHelper.resolveResourceIdFromAttr(requireContext(), R.attr.ic_sort), - menuItemOnClickListener = ::openReorderDialog + getString(R.string.feed_groups_header_title), + ThemeHelper.resolveResourceIdFromAttr(requireContext(), R.attr.ic_sort), + menuItemOnClickListener = ::openReorderDialog ) add(Section(feedGroupsSortMenuItem, listOf(feedGroupsCarousel))) @@ -260,10 +265,11 @@ class SubscriptionFragment : BaseStateFragment() { subscriptionsSection.setHideWhenEmpty(true) importExportItem = FeedImportExportItem( - { onImportPreviousSelected() }, - { onImportFromServiceSelected(it) }, - { onExportSelected() }, - importExportItemExpandedState ?: false) + { onImportPreviousSelected() }, + { onImportFromServiceSelected(it) }, + { onExportSelected() }, + importExportItemExpandedState ?: false + ) groupAdapter.add(Section(importExportItem, listOf(subscriptionsSection))) } @@ -284,8 +290,8 @@ class SubscriptionFragment : BaseStateFragment() { private fun showLongTapDialog(selectedItem: ChannelInfoItem) { val commands = arrayOf( - getString(R.string.share), - getString(R.string.unsubscribe) + getString(R.string.share), + getString(R.string.unsubscribe) ) val actions = DialogInterface.OnClickListener { _, i -> @@ -301,16 +307,18 @@ class SubscriptionFragment : BaseStateFragment() { bannerView.itemAdditionalDetails.visibility = View.GONE AlertDialog.Builder(requireContext()) - .setCustomTitle(bannerView) - .setItems(commands, actions) - .create() - .show() + .setCustomTitle(bannerView) + .setItems(commands, actions) + .create() + .show() } private fun deleteChannel(selectedItem: ChannelInfoItem) { - disposables.add(subscriptionManager.deleteSubscription(selectedItem.serviceId, selectedItem.url).subscribe { - Toast.makeText(requireContext(), getString(R.string.channel_unsubscribed), Toast.LENGTH_SHORT).show() - }) + disposables.add( + subscriptionManager.deleteSubscription(selectedItem.serviceId, selectedItem.url).subscribe { + Toast.makeText(requireContext(), getString(R.string.channel_unsubscribed), Toast.LENGTH_SHORT).show() + } + ) } override fun doInitialLoadLogic() = Unit @@ -332,8 +340,10 @@ class SubscriptionFragment : BaseStateFragment() { } private val listenerChannelItem = object : OnClickGesture() { - override fun selected(selectedItem: ChannelInfoItem) = NavigationHelper.openChannelFragment(fm, - selectedItem.serviceId, selectedItem.url, selectedItem.name) + override fun selected(selectedItem: ChannelInfoItem) = NavigationHelper.openChannelFragment( + fm, + selectedItem.serviceId, selectedItem.url, selectedItem.name + ) override fun held(selectedItem: ChannelInfoItem) = showLongTapDialog(selectedItem) } @@ -420,14 +430,16 @@ class SubscriptionFragment : BaseStateFragment() { private fun shouldUseGridLayout(): Boolean { val listMode = PreferenceManager.getDefaultSharedPreferences(requireContext()) - .getString(getString(R.string.list_view_mode_key), getString(R.string.list_view_mode_value)) + .getString(getString(R.string.list_view_mode_key), getString(R.string.list_view_mode_value)) return when (listMode) { getString(R.string.list_view_mode_auto_key) -> { val configuration = resources.configuration - (configuration.orientation == Configuration.ORIENTATION_LANDSCAPE && - configuration.isLayoutSizeAtLeast(Configuration.SCREENLAYOUT_SIZE_LARGE)) + ( + configuration.orientation == Configuration.ORIENTATION_LANDSCAPE && + configuration.isLayoutSizeAtLeast(Configuration.SCREENLAYOUT_SIZE_LARGE) + ) } getString(R.string.list_view_mode_grid_key) -> true else -> false diff --git a/app/src/main/java/org/schabi/newpipelegacy/local/subscription/SubscriptionManager.kt b/app/src/main/java/org/schabi/newpipelegacy/local/subscription/SubscriptionManager.kt index 643a521c7..61edeba54 100644 --- a/app/src/main/java/org/schabi/newpipelegacy/local/subscription/SubscriptionManager.kt +++ b/app/src/main/java/org/schabi/newpipelegacy/local/subscription/SubscriptionManager.kt @@ -32,7 +32,8 @@ class SubscriptionManager(context: Context) { filterQuery.isNotEmpty() -> { return if (showOnlyUngrouped) { subscriptionTable.getSubscriptionsOnlyUngroupedFiltered( - currentGroupId, filterQuery) + currentGroupId, filterQuery + ) } else { subscriptionTable.getSubscriptionsFiltered(filterQuery) } @@ -44,7 +45,8 @@ class SubscriptionManager(context: Context) { fun upsertAll(infoList: List): List { val listEntities = subscriptionTable.upsertAll( - infoList.map { SubscriptionEntity.from(it) }) + infoList.map { SubscriptionEntity.from(it) } + ) database.runInTransaction { infoList.forEachIndexed { index, info -> diff --git a/app/src/main/java/org/schabi/newpipelegacy/local/subscription/SubscriptionViewModel.kt b/app/src/main/java/org/schabi/newpipelegacy/local/subscription/SubscriptionViewModel.kt index 3bf7ef918..44c2d763b 100644 --- a/app/src/main/java/org/schabi/newpipelegacy/local/subscription/SubscriptionViewModel.kt +++ b/app/src/main/java/org/schabi/newpipelegacy/local/subscription/SubscriptionViewModel.kt @@ -6,11 +6,11 @@ import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import com.xwray.groupie.Group import io.reactivex.schedulers.Schedulers -import java.util.concurrent.TimeUnit import org.schabi.newpipelegacy.local.feed.FeedDatabaseManager import org.schabi.newpipelegacy.local.subscription.item.ChannelItem import org.schabi.newpipelegacy.local.subscription.item.FeedGroupCardItem import org.schabi.newpipelegacy.util.DEFAULT_THROTTLE_TIMEOUT +import java.util.concurrent.TimeUnit class SubscriptionViewModel(application: Application) : AndroidViewModel(application) { private var feedDatabaseManager: FeedDatabaseManager = FeedDatabaseManager(application) @@ -22,22 +22,22 @@ class SubscriptionViewModel(application: Application) : AndroidViewModel(applica val feedGroupsLiveData: LiveData> = mutableFeedGroupsLiveData private var feedGroupItemsDisposable = feedDatabaseManager.groups() - .throttleLatest(DEFAULT_THROTTLE_TIMEOUT, TimeUnit.MILLISECONDS) - .map { it.map(::FeedGroupCardItem) } - .subscribeOn(Schedulers.io()) - .subscribe( - { mutableFeedGroupsLiveData.postValue(it) }, - { mutableStateLiveData.postValue(SubscriptionState.ErrorState(it)) } - ) + .throttleLatest(DEFAULT_THROTTLE_TIMEOUT, TimeUnit.MILLISECONDS) + .map { it.map(::FeedGroupCardItem) } + .subscribeOn(Schedulers.io()) + .subscribe( + { mutableFeedGroupsLiveData.postValue(it) }, + { mutableStateLiveData.postValue(SubscriptionState.ErrorState(it)) } + ) private var stateItemsDisposable = subscriptionManager.subscriptions() - .throttleLatest(DEFAULT_THROTTLE_TIMEOUT, TimeUnit.MILLISECONDS) - .map { it.map { entity -> ChannelItem(entity.toChannelInfoItem(), entity.uid, ChannelItem.ItemVersion.MINI) } } - .subscribeOn(Schedulers.io()) - .subscribe( - { mutableStateLiveData.postValue(SubscriptionState.LoadedState(it)) }, - { mutableStateLiveData.postValue(SubscriptionState.ErrorState(it)) } - ) + .throttleLatest(DEFAULT_THROTTLE_TIMEOUT, TimeUnit.MILLISECONDS) + .map { it.map { entity -> ChannelItem(entity.toChannelInfoItem(), entity.uid, ChannelItem.ItemVersion.MINI) } } + .subscribeOn(Schedulers.io()) + .subscribe( + { mutableStateLiveData.postValue(SubscriptionState.LoadedState(it)) }, + { mutableStateLiveData.postValue(SubscriptionState.ErrorState(it)) } + ) override fun onCleared() { super.onCleared() diff --git a/app/src/main/java/org/schabi/newpipelegacy/local/subscription/SubscriptionsImportFragment.java b/app/src/main/java/org/schabi/newpipelegacy/local/subscription/SubscriptionsImportFragment.java index bff85f6e4..49d7d270e 100644 --- a/app/src/main/java/org/schabi/newpipelegacy/local/subscription/SubscriptionsImportFragment.java +++ b/app/src/main/java/org/schabi/newpipelegacy/local/subscription/SubscriptionsImportFragment.java @@ -85,7 +85,7 @@ public void onCreate(final Bundle savedInstanceState) { if (supportedSources.isEmpty() && currentServiceId != Constants.NO_SERVICE_ID) { ErrorActivity.reportError(activity, Collections.emptyList(), null, null, ErrorActivity.ErrorInfo.make(UserAction.SOMETHING_ELSE, - NewPipe.getNameOfService(currentServiceId), + ServiceHelper.getNameOfServiceById(currentServiceId), "Service don't support importing", R.string.general_error)); activity.finish(); } diff --git a/app/src/main/java/org/schabi/newpipelegacy/local/subscription/dialog/FeedGroupDialog.kt b/app/src/main/java/org/schabi/newpipelegacy/local/subscription/dialog/FeedGroupDialog.kt index 775599e6c..847cc1723 100644 --- a/app/src/main/java/org/schabi/newpipelegacy/local/subscription/dialog/FeedGroupDialog.kt +++ b/app/src/main/java/org/schabi/newpipelegacy/local/subscription/dialog/FeedGroupDialog.kt @@ -23,8 +23,6 @@ import com.xwray.groupie.Section import com.xwray.groupie.kotlinandroidextensions.GroupieViewHolder import icepick.Icepick import icepick.State -import java.io.Serializable -import kotlin.collections.contains import kotlinx.android.synthetic.main.dialog_feed_group_create.* import kotlinx.android.synthetic.main.toolbar_search_layout.* import org.schabi.newpipelegacy.R @@ -42,6 +40,8 @@ import org.schabi.newpipelegacy.local.subscription.item.PickerIconItem import org.schabi.newpipelegacy.local.subscription.item.PickerSubscriptionItem import org.schabi.newpipelegacy.util.AndroidTvUtils import org.schabi.newpipelegacy.util.ThemeHelper +import java.io.Serializable +import kotlin.collections.contains class FeedGroupDialog : DialogFragment(), BackPressable { private lateinit var viewModel: FeedGroupDialogViewModel @@ -115,21 +115,30 @@ class FeedGroupDialog : DialogFragment(), BackPressable { override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) - viewModel = ViewModelProvider(this, - FeedGroupDialogViewModel.Factory(requireContext(), - groupId, subscriptionsCurrentSearchQuery, subscriptionsShowOnlyUngrouped) + viewModel = ViewModelProvider( + this, + FeedGroupDialogViewModel.Factory( + requireContext(), + groupId, subscriptionsCurrentSearchQuery, subscriptionsShowOnlyUngrouped + ) ).get(FeedGroupDialogViewModel::class.java) viewModel.groupLiveData.observe(viewLifecycleOwner, Observer(::handleGroup)) - viewModel.subscriptionsLiveData.observe(viewLifecycleOwner, Observer { - setupSubscriptionPicker(it.first, it.second) - }) - viewModel.dialogEventLiveData.observe(viewLifecycleOwner, Observer { - when (it) { - ProcessingEvent -> disableInput() - SuccessEvent -> dismiss() + viewModel.subscriptionsLiveData.observe( + viewLifecycleOwner, + Observer { + setupSubscriptionPicker(it.first, it.second) } - }) + ) + viewModel.dialogEventLiveData.observe( + viewLifecycleOwner, + Observer { + when (it) { + ProcessingEvent -> disableInput() + SuccessEvent -> dismiss() + } + } + ) subscriptionGroupAdapter = GroupAdapter().apply { add(subscriptionMainSection) @@ -140,8 +149,10 @@ class FeedGroupDialog : DialogFragment(), BackPressable { // Disable animations, too distracting. itemAnimator = null adapter = subscriptionGroupAdapter - layoutManager = GridLayoutManager(requireContext(), subscriptionGroupAdapter.spanCount, - RecyclerView.VERTICAL, false).apply { + layoutManager = GridLayoutManager( + requireContext(), subscriptionGroupAdapter.spanCount, + RecyclerView.VERTICAL, false + ).apply { spanSizeLookup = subscriptionGroupAdapter.spanSizeLookup } } @@ -354,7 +365,8 @@ class FeedGroupDialog : DialogFragment(), BackPressable { val selectedCount = this.selectedSubscriptions.size val selectedCountText = resources.getQuantityString( R.plurals.feed_group_dialog_selection_count, - selectedCount, selectedCount) + selectedCount, selectedCount + ) selected_subscription_count_view.text = selectedCountText subscriptions_header_info.text = selectedCountText } @@ -409,10 +421,12 @@ class FeedGroupDialog : DialogFragment(), BackPressable { separator.onlyVisibleIn(SubscriptionsPickerScreen, IconPickerScreen) cancel_button.onlyVisibleIn(InitialScreen, DeleteScreen) - confirm_button.setText(when { - currentScreen == InitialScreen && groupId == NO_GROUP_SELECTED -> R.string.create - else -> android.R.string.ok - }) + confirm_button.setText( + when { + currentScreen == InitialScreen && groupId == NO_GROUP_SELECTED -> R.string.create + else -> android.R.string.ok + } + ) delete_button.visibility = when { currentScreen != InitialScreen -> View.GONE @@ -469,8 +483,10 @@ class FeedGroupDialog : DialogFragment(), BackPressable { } private fun hideKeyboardSearch() { - inputMethodManager.hideSoftInputFromWindow(toolbar_search_edit_text.windowToken, - InputMethodManager.RESULT_UNCHANGED_SHOWN) + inputMethodManager.hideSoftInputFromWindow( + toolbar_search_edit_text.windowToken, + InputMethodManager.RESULT_UNCHANGED_SHOWN + ) toolbar_search_edit_text.clearFocus() } @@ -481,8 +497,10 @@ class FeedGroupDialog : DialogFragment(), BackPressable { } private fun hideKeyboard() { - inputMethodManager.hideSoftInputFromWindow(group_name_input.windowToken, - InputMethodManager.RESULT_UNCHANGED_SHOWN) + inputMethodManager.hideSoftInputFromWindow( + group_name_input.windowToken, + InputMethodManager.RESULT_UNCHANGED_SHOWN + ) group_name_input.clearFocus() } diff --git a/app/src/main/java/org/schabi/newpipelegacy/local/subscription/dialog/FeedGroupDialogViewModel.kt b/app/src/main/java/org/schabi/newpipelegacy/local/subscription/dialog/FeedGroupDialogViewModel.kt index 8c8a3e463..7aaccfcdb 100644 --- a/app/src/main/java/org/schabi/newpipelegacy/local/subscription/dialog/FeedGroupDialogViewModel.kt +++ b/app/src/main/java/org/schabi/newpipelegacy/local/subscription/dialog/FeedGroupDialogViewModel.kt @@ -32,9 +32,9 @@ class FeedGroupDialogViewModel( private var subscriptionsFlowable = Flowable .combineLatest( - filterSubscriptions.startWith(initialQuery), - toggleShowOnlyUngrouped.startWith(initialShowOnlyUngrouped), - BiFunction { t1: String, t2: Boolean -> Filter(t1, t2) } + filterSubscriptions.startWith(initialQuery), + toggleShowOnlyUngrouped.startWith(initialShowOnlyUngrouped), + BiFunction { t1: String, t2: Boolean -> Filter(t1, t2) } ) .distinctUntilChanged() .switchMap { filter -> @@ -55,8 +55,10 @@ class FeedGroupDialogViewModel( .subscribe(mutableGroupLiveData::postValue) private var subscriptionsDisposable = Flowable - .combineLatest(subscriptionsFlowable, feedDatabaseManager.subscriptionIdsForGroup(groupId), - BiFunction { t1: List, t2: List -> t1 to t2.toSet() }) + .combineLatest( + subscriptionsFlowable, feedDatabaseManager.subscriptionIdsForGroup(groupId), + BiFunction { t1: List, t2: List -> t1 to t2.toSet() } + ) .subscribeOn(Schedulers.io()) .subscribe(mutableSubscriptionsLiveData::postValue) @@ -68,15 +70,19 @@ class FeedGroupDialogViewModel( } fun createGroup(name: String, selectedIcon: FeedGroupIcon, selectedSubscriptions: Set) { - doAction(feedDatabaseManager.createGroup(name, selectedIcon) - .flatMapCompletable { - feedDatabaseManager.updateSubscriptionsForGroup(it, selectedSubscriptions.toList()) - }) + doAction( + feedDatabaseManager.createGroup(name, selectedIcon) + .flatMapCompletable { + feedDatabaseManager.updateSubscriptionsForGroup(it, selectedSubscriptions.toList()) + } + ) } fun updateGroup(name: String, selectedIcon: FeedGroupIcon, selectedSubscriptions: Set, sortOrder: Long) { - doAction(feedDatabaseManager.updateSubscriptionsForGroup(groupId, selectedSubscriptions.toList()) - .andThen(feedDatabaseManager.updateGroup(FeedGroupEntity(groupId, name, selectedIcon, sortOrder)))) + doAction( + feedDatabaseManager.updateSubscriptionsForGroup(groupId, selectedSubscriptions.toList()) + .andThen(feedDatabaseManager.updateGroup(FeedGroupEntity(groupId, name, selectedIcon, sortOrder))) + ) } fun deleteGroup() { @@ -120,8 +126,10 @@ class FeedGroupDialogViewModel( ) : ViewModelProvider.Factory { @Suppress("UNCHECKED_CAST") override fun create(modelClass: Class): T { - return FeedGroupDialogViewModel(context.applicationContext, - groupId, initialQuery, initialShowOnlyUngrouped) as T + return FeedGroupDialogViewModel( + context.applicationContext, + groupId, initialQuery, initialShowOnlyUngrouped + ) as T } } } diff --git a/app/src/main/java/org/schabi/newpipelegacy/local/subscription/dialog/FeedGroupReorderDialog.kt b/app/src/main/java/org/schabi/newpipelegacy/local/subscription/dialog/FeedGroupReorderDialog.kt index dcc37eba7..516c2f567 100644 --- a/app/src/main/java/org/schabi/newpipelegacy/local/subscription/dialog/FeedGroupReorderDialog.kt +++ b/app/src/main/java/org/schabi/newpipelegacy/local/subscription/dialog/FeedGroupReorderDialog.kt @@ -16,7 +16,6 @@ import com.xwray.groupie.TouchCallback import com.xwray.groupie.kotlinandroidextensions.GroupieViewHolder import icepick.Icepick import icepick.State -import java.util.Collections import kotlinx.android.synthetic.main.dialog_feed_group_reorder.confirm_button import kotlinx.android.synthetic.main.dialog_feed_group_reorder.feed_groups_list import org.schabi.newpipelegacy.R @@ -25,6 +24,7 @@ import org.schabi.newpipelegacy.local.subscription.dialog.FeedGroupReorderDialog import org.schabi.newpipelegacy.local.subscription.dialog.FeedGroupReorderDialogViewModel.DialogEvent.SuccessEvent import org.schabi.newpipelegacy.local.subscription.item.FeedGroupReorderItem import org.schabi.newpipelegacy.util.ThemeHelper +import java.util.Collections class FeedGroupReorderDialog : DialogFragment() { private lateinit var viewModel: FeedGroupReorderDialogViewModel @@ -51,12 +51,15 @@ class FeedGroupReorderDialog : DialogFragment() { viewModel = ViewModelProviders.of(this).get(FeedGroupReorderDialogViewModel::class.java) viewModel.groupsLiveData.observe(viewLifecycleOwner, Observer(::handleGroups)) - viewModel.dialogEventLiveData.observe(viewLifecycleOwner, Observer { - when (it) { - ProcessingEvent -> disableInput() - SuccessEvent -> dismiss() + viewModel.dialogEventLiveData.observe( + viewLifecycleOwner, + Observer { + when (it) { + ProcessingEvent -> disableInput() + SuccessEvent -> dismiss() + } } - }) + ) feed_groups_list.layoutManager = LinearLayoutManager(requireContext()) feed_groups_list.adapter = groupAdapter diff --git a/app/src/main/java/org/schabi/newpipelegacy/local/subscription/dialog/FeedGroupReorderDialogViewModel.kt b/app/src/main/java/org/schabi/newpipelegacy/local/subscription/dialog/FeedGroupReorderDialogViewModel.kt index 6c99dabb2..1456e0353 100644 --- a/app/src/main/java/org/schabi/newpipelegacy/local/subscription/dialog/FeedGroupReorderDialogViewModel.kt +++ b/app/src/main/java/org/schabi/newpipelegacy/local/subscription/dialog/FeedGroupReorderDialogViewModel.kt @@ -21,9 +21,9 @@ class FeedGroupReorderDialogViewModel(application: Application) : AndroidViewMod private var actionProcessingDisposable: Disposable? = null private var groupsDisposable = feedDatabaseManager.groups() - .limit(1) - .subscribeOn(Schedulers.io()) - .subscribe(mutableGroupsLiveData::postValue) + .limit(1) + .subscribeOn(Schedulers.io()) + .subscribe(mutableGroupsLiveData::postValue) override fun onCleared() { super.onCleared() @@ -40,8 +40,8 @@ class FeedGroupReorderDialogViewModel(application: Application) : AndroidViewMod mutableDialogEventLiveData.value = DialogEvent.ProcessingEvent actionProcessingDisposable = completable - .subscribeOn(Schedulers.io()) - .subscribe { mutableDialogEventLiveData.postValue(DialogEvent.SuccessEvent) } + .subscribeOn(Schedulers.io()) + .subscribe { mutableDialogEventLiveData.postValue(DialogEvent.SuccessEvent) } } } diff --git a/app/src/main/java/org/schabi/newpipelegacy/local/subscription/item/ChannelItem.kt b/app/src/main/java/org/schabi/newpipelegacy/local/subscription/item/ChannelItem.kt index d9d4c21f5..49c661e13 100644 --- a/app/src/main/java/org/schabi/newpipelegacy/local/subscription/item/ChannelItem.kt +++ b/app/src/main/java/org/schabi/newpipelegacy/local/subscription/item/ChannelItem.kt @@ -36,8 +36,10 @@ class ChannelItem( viewHolder.itemAdditionalDetails.text = getDetailLine(viewHolder.root.context) if (itemVersion == ItemVersion.NORMAL) viewHolder.itemChannelDescriptionView.text = infoItem.description - ImageLoader.getInstance().displayImage(infoItem.thumbnailUrl, viewHolder.itemThumbnailView, - ImageDisplayConstants.DISPLAY_THUMBNAIL_OPTIONS) + ImageLoader.getInstance().displayImage( + infoItem.thumbnailUrl, viewHolder.itemThumbnailView, + ImageDisplayConstants.DISPLAY_THUMBNAIL_OPTIONS + ) gesturesListener?.run { viewHolder.containerView.setOnClickListener { selected(infoItem) } diff --git a/app/src/main/java/org/schabi/newpipelegacy/local/subscription/item/FeedGroupReorderItem.kt b/app/src/main/java/org/schabi/newpipelegacy/local/subscription/item/FeedGroupReorderItem.kt index c4481dccf..d33241b6d 100644 --- a/app/src/main/java/org/schabi/newpipelegacy/local/subscription/item/FeedGroupReorderItem.kt +++ b/app/src/main/java/org/schabi/newpipelegacy/local/subscription/item/FeedGroupReorderItem.kt @@ -20,7 +20,7 @@ data class FeedGroupReorderItem( val dragCallback: ItemTouchHelper ) : Item() { constructor (feedGroupEntity: FeedGroupEntity, dragCallback: ItemTouchHelper) : - this(feedGroupEntity.uid, feedGroupEntity.name, feedGroupEntity.icon, dragCallback) + this(feedGroupEntity.uid, feedGroupEntity.name, feedGroupEntity.icon, dragCallback) override fun getId(): Long { return when (groupId) { diff --git a/app/src/main/java/org/schabi/newpipelegacy/local/subscription/item/FeedImportExportItem.kt b/app/src/main/java/org/schabi/newpipelegacy/local/subscription/item/FeedImportExportItem.kt index e55ed05a4..e1d6bcf28 100644 --- a/app/src/main/java/org/schabi/newpipelegacy/local/subscription/item/FeedImportExportItem.kt +++ b/app/src/main/java/org/schabi/newpipelegacy/local/subscription/item/FeedImportExportItem.kt @@ -49,8 +49,10 @@ class FeedImportExportItem( expandIconListener?.let { viewHolder.import_export_options.removeListener(it) } expandIconListener = CollapsibleView.StateListener { newState -> - AnimationUtils.animateRotation(viewHolder.import_export_expand_icon, - 250, if (newState == CollapsibleView.COLLAPSED) 0 else 180) + AnimationUtils.animateRotation( + viewHolder.import_export_expand_icon, + 250, if (newState == CollapsibleView.COLLAPSED) 0 else 180 + ) } viewHolder.import_export_options.currentState = if (isExpanded) CollapsibleView.EXPANDED else CollapsibleView.COLLAPSED @@ -85,8 +87,10 @@ class FeedImportExportItem( } private fun setupImportFromItems(listHolder: ViewGroup) { - val previousBackupItem = addItemView(listHolder.context.getString(R.string.previous_export), - ThemeHelper.resolveResourceIdFromAttr(listHolder.context, R.attr.ic_backup), listHolder) + val previousBackupItem = addItemView( + listHolder.context.getString(R.string.previous_export), + ThemeHelper.resolveResourceIdFromAttr(listHolder.context, R.attr.ic_backup), listHolder + ) previousBackupItem.setOnClickListener { onImportPreviousSelected() } val iconColor = if (ThemeHelper.isLightThemeSelected(listHolder.context)) Color.BLACK else Color.WHITE @@ -112,8 +116,10 @@ class FeedImportExportItem( } private fun setupExportToItems(listHolder: ViewGroup) { - val previousBackupItem = addItemView(listHolder.context.getString(R.string.file), - ThemeHelper.resolveResourceIdFromAttr(listHolder.context, R.attr.ic_save), listHolder) + val previousBackupItem = addItemView( + listHolder.context.getString(R.string.file), + ThemeHelper.resolveResourceIdFromAttr(listHolder.context, R.attr.ic_save), listHolder + ) previousBackupItem.setOnClickListener { onExportSelected() } } } diff --git a/app/src/main/java/org/schabi/newpipelegacy/local/subscription/item/HeaderWithMenuItem.kt b/app/src/main/java/org/schabi/newpipelegacy/local/subscription/item/HeaderWithMenuItem.kt index 5d01b5146..ac519c302 100644 --- a/app/src/main/java/org/schabi/newpipelegacy/local/subscription/item/HeaderWithMenuItem.kt +++ b/app/src/main/java/org/schabi/newpipelegacy/local/subscription/item/HeaderWithMenuItem.kt @@ -37,11 +37,11 @@ class HeaderWithMenuItem( viewHolder.header_menu_item.setImageResource(itemIcon) val listener: OnClickListener? = - onClickListener?.let { OnClickListener { onClickListener.invoke() } } + onClickListener?.let { OnClickListener { onClickListener.invoke() } } viewHolder.root.setOnClickListener(listener) val menuItemListener: OnClickListener? = - menuItemOnClickListener?.let { OnClickListener { menuItemOnClickListener.invoke() } } + menuItemOnClickListener?.let { OnClickListener { menuItemOnClickListener.invoke() } } viewHolder.header_menu_item.setOnClickListener(menuItemListener) updateMenuItemVisibility(viewHolder) } diff --git a/app/src/main/java/org/schabi/newpipelegacy/local/subscription/item/PickerSubscriptionItem.kt b/app/src/main/java/org/schabi/newpipelegacy/local/subscription/item/PickerSubscriptionItem.kt index 1c2c857db..2851f06ff 100644 --- a/app/src/main/java/org/schabi/newpipelegacy/local/subscription/item/PickerSubscriptionItem.kt +++ b/app/src/main/java/org/schabi/newpipelegacy/local/subscription/item/PickerSubscriptionItem.kt @@ -21,8 +21,10 @@ data class PickerSubscriptionItem( override fun getSpanSize(spanCount: Int, position: Int): Int = 1 override fun bind(viewHolder: GroupieViewHolder, position: Int) { - ImageLoader.getInstance().displayImage(subscriptionEntity.avatarUrl, - viewHolder.thumbnail_view, ImageDisplayConstants.DISPLAY_AVATAR_OPTIONS) + ImageLoader.getInstance().displayImage( + subscriptionEntity.avatarUrl, + viewHolder.thumbnail_view, ImageDisplayConstants.DISPLAY_AVATAR_OPTIONS + ) viewHolder.title_view.text = subscriptionEntity.name viewHolder.selected_highlight.visibility = if (isSelected) View.VISIBLE else View.GONE @@ -38,7 +40,9 @@ data class PickerSubscriptionItem( fun updateSelected(containerView: View, isSelected: Boolean) { this.isSelected = isSelected - animateView(containerView.selected_highlight, - AnimationUtils.Type.LIGHT_SCALE_AND_ALPHA, isSelected, 150) + animateView( + containerView.selected_highlight, + AnimationUtils.Type.LIGHT_SCALE_AND_ALPHA, isSelected, 150 + ) } } diff --git a/app/src/main/java/org/schabi/newpipelegacy/local/subscription/services/SubscriptionsExportService.java b/app/src/main/java/org/schabi/newpipelegacy/local/subscription/services/SubscriptionsExportService.java index 1a0b99f67..4e0b66b52 100644 --- a/app/src/main/java/org/schabi/newpipelegacy/local/subscription/services/SubscriptionsExportService.java +++ b/app/src/main/java/org/schabi/newpipelegacy/local/subscription/services/SubscriptionsExportService.java @@ -27,6 +27,7 @@ import org.reactivestreams.Subscriber; import org.reactivestreams.Subscription; +import org.schabi.newpipelegacy.App; import org.schabi.newpipelegacy.R; import org.schabi.newpipelegacy.database.subscription.SubscriptionEntity; import org.schabi.newpipe.extractor.subscription.SubscriptionItem; @@ -50,7 +51,7 @@ public class SubscriptionsExportService extends BaseImportExportService { * A {@link LocalBroadcastManager local broadcast} will be made with this action * when the export is successfully completed. */ - public static final String EXPORT_COMPLETE_ACTION = "org.schabi.newpipelegacy.local" + public static final String EXPORT_COMPLETE_ACTION = App.PACKAGE_NAME + ".local" + ".subscription.services.SubscriptionsExportService.EXPORT_COMPLETE"; private Subscription subscription; diff --git a/app/src/main/java/org/schabi/newpipelegacy/local/subscription/services/SubscriptionsImportService.java b/app/src/main/java/org/schabi/newpipelegacy/local/subscription/services/SubscriptionsImportService.java index cd2d65fa4..09571055e 100644 --- a/app/src/main/java/org/schabi/newpipelegacy/local/subscription/services/SubscriptionsImportService.java +++ b/app/src/main/java/org/schabi/newpipelegacy/local/subscription/services/SubscriptionsImportService.java @@ -29,6 +29,7 @@ import org.reactivestreams.Subscriber; import org.reactivestreams.Subscription; +import org.schabi.newpipelegacy.App; import org.schabi.newpipelegacy.R; import org.schabi.newpipelegacy.database.subscription.SubscriptionEntity; import org.schabi.newpipe.extractor.NewPipe; @@ -66,7 +67,7 @@ public class SubscriptionsImportService extends BaseImportExportService { * A {@link LocalBroadcastManager local broadcast} will be made with this action * when the import is successfully completed. */ - public static final String IMPORT_COMPLETE_ACTION = "org.schabi.newpipelegacy.local" + public static final String IMPORT_COMPLETE_ACTION = App.PACKAGE_NAME + ".local" + ".subscription.services.SubscriptionsImportService.IMPORT_COMPLETE"; /** diff --git a/app/src/main/java/org/schabi/newpipelegacy/player/BackgroundPlayer.java b/app/src/main/java/org/schabi/newpipelegacy/player/BackgroundPlayer.java index f43d9493b..ebe760ebf 100644 --- a/app/src/main/java/org/schabi/newpipelegacy/player/BackgroundPlayer.java +++ b/app/src/main/java/org/schabi/newpipelegacy/player/BackgroundPlayer.java @@ -29,6 +29,8 @@ import android.content.res.Resources; import android.graphics.Bitmap; import android.os.Build; +import android.os.Build.VERSION; +import android.os.Build.VERSION_CODES; import android.os.IBinder; import android.preference.PreferenceManager; import android.util.Log; @@ -40,6 +42,9 @@ import androidx.annotation.RequiresApi; import androidx.core.app.NotificationCompat; + +import androidx.core.app.NotificationCompat.DecoratedCustomViewStyle; +import androidx.media.app.NotificationCompat.MediaStyle; import com.google.android.exoplayer2.PlaybackParameters; import com.google.android.exoplayer2.Player; import com.google.android.exoplayer2.source.MediaSource; @@ -224,6 +229,11 @@ private NotificationCompat.Builder createNotification() { if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.JELLY_BEAN) { builder.setPriority(NotificationCompat.PRIORITY_MAX); } + + if (VERSION.SDK_INT > VERSION_CODES.Q) { + builder.setStyle(new MediaStyle()); + } + return builder; } diff --git a/app/src/main/java/org/schabi/newpipelegacy/player/BackgroundPlayerActivity.java b/app/src/main/java/org/schabi/newpipelegacy/player/BackgroundPlayerActivity.java index d7551708e..414b71809 100644 --- a/app/src/main/java/org/schabi/newpipelegacy/player/BackgroundPlayerActivity.java +++ b/app/src/main/java/org/schabi/newpipelegacy/player/BackgroundPlayerActivity.java @@ -1,6 +1,7 @@ package org.schabi.newpipelegacy.player; import android.content.Intent; +import android.view.Menu; import android.view.MenuItem; import org.schabi.newpipelegacy.R; @@ -70,4 +71,9 @@ public boolean onPlayerOptionSelected(final MenuItem item) { public Intent getPlayerShutdownIntent() { return new Intent(ACTION_CLOSE); } + + @Override + public boolean onCreateOptionsMenu(Menu menu) { + return super.onCreateOptionsMenu(menu); + } } diff --git a/app/src/main/java/org/schabi/newpipelegacy/player/BasePlayer.java b/app/src/main/java/org/schabi/newpipelegacy/player/BasePlayer.java index 1ef1dc24b..4f60f7ace 100644 --- a/app/src/main/java/org/schabi/newpipelegacy/player/BasePlayer.java +++ b/app/src/main/java/org/schabi/newpipelegacy/player/BasePlayer.java @@ -157,7 +157,7 @@ public abstract class BasePlayer implements //////////////////////////////////////////////////////////////////////////*/ protected static final int PLAY_PREV_ACTIVATION_LIMIT_MILLIS = 5000; // 5 seconds - protected static final int PROGRESS_LOOP_INTERVAL_MILLIS = 500; + protected static final int PROGRESS_LOOP_INTERVAL_MILLIS = 2000; // 2 seconds protected SimpleExoPlayer simpleExoPlayer; protected AudioReactor audioReactor; diff --git a/app/src/main/java/org/schabi/newpipelegacy/player/MainVideoPlayer.java b/app/src/main/java/org/schabi/newpipelegacy/player/MainVideoPlayer.java index 66951f794..64a52435f 100644 --- a/app/src/main/java/org/schabi/newpipelegacy/player/MainVideoPlayer.java +++ b/app/src/main/java/org/schabi/newpipelegacy/player/MainVideoPlayer.java @@ -48,6 +48,7 @@ import android.widget.Button; import android.widget.ImageButton; import android.widget.ImageView; +import android.widget.LinearLayout; import android.widget.PopupMenu; import android.widget.ProgressBar; import android.widget.RelativeLayout; @@ -86,6 +87,7 @@ import org.schabi.newpipelegacy.util.ListHelper; import org.schabi.newpipelegacy.util.NavigationHelper; import org.schabi.newpipelegacy.util.PermissionHelper; +import org.schabi.newpipelegacy.util.ServiceHelper; import org.schabi.newpipelegacy.util.ShareUtils; import org.schabi.newpipelegacy.util.StateSaver; import org.schabi.newpipelegacy.util.ThemeHelper; @@ -517,6 +519,7 @@ public void onPlaybackParameterChanged(final float playbackTempo, final float pl private class VideoPlayerImpl extends VideoPlayer { private static final float MAX_GESTURE_LENGTH = 0.75f; + private LinearLayout metadata; private TextView titleTextView; private TextView channelTextView; private RelativeLayout volumeRelativeLayout; @@ -561,6 +564,7 @@ private class VideoPlayerImpl extends VideoPlayer { @Override public void initViews(final View view) { super.initViews(view); + this.metadata = view.findViewById(R.id.metadataView); this.titleTextView = view.findViewById(R.id.titleTextView); this.channelTextView = view.findViewById(R.id.channelTextView); this.volumeRelativeLayout = view.findViewById(R.id.volumeRelativeLayout); @@ -619,6 +623,7 @@ public void initListeners() { gestureDetector.setIsLongpressEnabled(false); getRootView().setOnTouchListener(listener); + metadata.setOnClickListener(this); queueButton.setOnClickListener(this); repeatButton.setOnClickListener(this); shuffleButton.setOnClickListener(this); @@ -702,6 +707,12 @@ public void onShuffleClicked() { updatePlaybackButtons(); } + @Override + public void onPlay() { + super.onPlay(); + showControlsThenHide(); + } + /*////////////////////////////////////////////////////////////////////////// // Playback Listener //////////////////////////////////////////////////////////////////////////*/ @@ -814,6 +825,15 @@ public void onMuteUnmuteButtonClicked() { setMuteButton(muteButton, playerImpl.isMuted()); } + public void onMetadataClicked() { + NavigationHelper.openVideoDetail(context, + ServiceHelper.getSelectedServiceId(context), + playerImpl.getVideoUrl(), playerImpl.getVideoTitle()); + + ((View) getControlAnimationView().getParent()).setVisibility(View.GONE); + destroy(); + finish(); + } @Override public void onClick(final View v) { @@ -850,6 +870,8 @@ public void onClick(final View v) { return; } else if (v.getId() == kodiButton.getId()) { onKodiShare(); + } else if (v.getId() == metadata.getId()) { + onMetadataClicked(); } if (getCurrentState() != STATE_COMPLETED) { diff --git a/app/src/main/java/org/schabi/newpipelegacy/player/ServicePlayerActivity.java b/app/src/main/java/org/schabi/newpipelegacy/player/ServicePlayerActivity.java index dd2e5e7c7..d147332d3 100644 --- a/app/src/main/java/org/schabi/newpipelegacy/player/ServicePlayerActivity.java +++ b/app/src/main/java/org/schabi/newpipelegacy/player/ServicePlayerActivity.java @@ -18,6 +18,7 @@ import android.widget.SeekBar; import android.widget.TextView; +import androidx.annotation.Nullable; import androidx.appcompat.app.AppCompatActivity; import androidx.appcompat.widget.Toolbar; import androidx.recyclerview.widget.ItemTouchHelper; @@ -40,6 +41,7 @@ import org.schabi.newpipelegacy.player.playqueue.PlayQueueItemTouchCallback; import org.schabi.newpipelegacy.util.Localization; import org.schabi.newpipelegacy.util.NavigationHelper; +import org.schabi.newpipelegacy.util.ServiceHelper; import org.schabi.newpipelegacy.util.ThemeHelper; import java.util.Collections; @@ -149,6 +151,10 @@ public boolean onCreateOptionsMenu(final Menu m) { getMenuInflater().inflate(R.menu.menu_play_queue, m); getMenuInflater().inflate(getPlayerOptionMenuResource(), m); onMaybeMuteChanged(); + // to avoid null reference + if (player != null) { + onPlaybackParameterChanged(player.getPlaybackParameters()); + } return true; } @@ -485,7 +491,8 @@ public void onClick(final View view) { } else if (view.getId() == shuffleButton.getId()) { player.onShuffleClicked(); } else if (view.getId() == metadata.getId()) { - scrollToSelected(); + onOpenDetail(ServiceHelper.getSelectedServiceId(getApplicationContext()), + player.getVideoUrl(), player.getVideoTitle()); } else if (view.getId() == progressLiveSync.getId()) { player.seekToDefault(); } @@ -508,6 +515,7 @@ public void onPlaybackParameterChanged(final float playbackTempo, final float pl final boolean playbackSkipSilence) { if (player != null) { player.setPlaybackParameters(playbackTempo, playbackPitch, playbackSkipSilence); + onPlaybackParameterChanged(player.getPlaybackParameters()); } } @@ -689,7 +697,7 @@ private void onPlayModeChanged(final int repeatMode, final boolean shuffled) { shuffleButton.setImageAlpha(shuffleAlpha); } - private void onPlaybackParameterChanged(final PlaybackParameters parameters) { + private void onPlaybackParameterChanged(@Nullable final PlaybackParameters parameters) { if (parameters != null) { if (menu != null && player != null) { final MenuItem item = menu.findItem(R.id.action_playback_speed); diff --git a/app/src/main/java/org/schabi/newpipelegacy/player/VideoPlayer.java b/app/src/main/java/org/schabi/newpipelegacy/player/VideoPlayer.java index 46a018ebc..9be8a9e5e 100644 --- a/app/src/main/java/org/schabi/newpipelegacy/player/VideoPlayer.java +++ b/app/src/main/java/org/schabi/newpipelegacy/player/VideoPlayer.java @@ -99,7 +99,7 @@ public abstract class VideoPlayer extends BasePlayer 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 + public static final int DPAD_CONTROLS_HIDE_TIME = 5000; // 5 seconds protected static final int RENDERER_UNAVAILABLE = -1; diff --git a/app/src/main/java/org/schabi/newpipelegacy/player/helper/AudioReactor.java b/app/src/main/java/org/schabi/newpipelegacy/player/helper/AudioReactor.java index e98d56920..5bb4d9e43 100644 --- a/app/src/main/java/org/schabi/newpipelegacy/player/helper/AudioReactor.java +++ b/app/src/main/java/org/schabi/newpipelegacy/player/helper/AudioReactor.java @@ -5,35 +5,33 @@ import android.animation.ValueAnimator; import android.content.Context; import android.content.Intent; -import android.media.AudioFocusRequest; import android.media.AudioManager; import android.media.audiofx.AudioEffect; -import android.os.Build; import android.util.Log; import androidx.annotation.NonNull; +import androidx.media.AudioFocusRequestCompat; +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 { private static final String TAG = "AudioFocusReactor"; - private static final boolean SHOULD_BUILD_FOCUS_REQUEST = - Build.VERSION.SDK_INT >= Build.VERSION_CODES.O; - private static final int DUCK_DURATION = 1500; private static final float DUCK_AUDIO_TO = .2f; - private static final int FOCUS_GAIN_TYPE = AudioManager.AUDIOFOCUS_GAIN; + private static final int FOCUS_GAIN_TYPE = AudioManagerCompat.AUDIOFOCUS_GAIN; private static final int STREAM_TYPE = AudioManager.STREAM_MUSIC; private final SimpleExoPlayer player; private final Context context; private final AudioManager audioManager; - private final AudioFocusRequest request; + private final AudioFocusRequestCompat request; public AudioReactor(@NonNull final Context context, @NonNull final SimpleExoPlayer player) { @@ -42,20 +40,17 @@ public AudioReactor(@NonNull final Context context, this.audioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE); player.addAnalyticsListener(this); - if (SHOULD_BUILD_FOCUS_REQUEST) { - request = new AudioFocusRequest.Builder(FOCUS_GAIN_TYPE) - .setAcceptsDelayedFocusGain(true) - .setWillPauseWhenDucked(true) - .setOnAudioFocusChangeListener(this) - .build(); - } else { - request = null; - } + request = new AudioFocusRequestCompat.Builder(FOCUS_GAIN_TYPE) + //.setAcceptsDelayedFocusGain(true) + .setWillPauseWhenDucked(true) + .setOnAudioFocusChangeListener(this) + .build(); } public void dispose() { abandonAudioFocus(); player.removeAnalyticsListener(this); + notifyAudioSessionUpdate(false, player.getAudioSessionId()); } /*////////////////////////////////////////////////////////////////////////// @@ -63,19 +58,11 @@ public void dispose() { //////////////////////////////////////////////////////////////////////////*/ public void requestAudioFocus() { - if (SHOULD_BUILD_FOCUS_REQUEST) { - audioManager.requestAudioFocus(request); - } else { - audioManager.requestAudioFocus(this, STREAM_TYPE, FOCUS_GAIN_TYPE); - } + AudioManagerCompat.requestAudioFocus(audioManager, request); } public void abandonAudioFocus() { - if (SHOULD_BUILD_FOCUS_REQUEST) { - audioManager.abandonAudioFocusRequest(request); - } else { - audioManager.abandonAudioFocus(this); - } + AudioManagerCompat.abandonAudioFocusRequest(audioManager, request); } public int getVolume() { @@ -87,7 +74,7 @@ public void setVolume(final int volume) { } public int getMaxVolume() { - return audioManager.getStreamMaxVolume(STREAM_TYPE); + return AudioManagerCompat.getStreamMaxVolume(audioManager, STREAM_TYPE); } /*////////////////////////////////////////////////////////////////////////// @@ -163,11 +150,20 @@ public void onAnimationEnd(final Animator animation) { @Override public void onAudioSessionId(final EventTime eventTime, final int audioSessionId) { + notifyAudioSessionUpdate(true, audioSessionId); + } + + 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(context)) { 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/newpipelegacy/player/helper/LoadController.java b/app/src/main/java/org/schabi/newpipelegacy/player/helper/LoadController.java index a205befb2..ea8068db4 100644 --- a/app/src/main/java/org/schabi/newpipelegacy/player/helper/LoadController.java +++ b/app/src/main/java/org/schabi/newpipelegacy/player/helper/LoadController.java @@ -31,7 +31,8 @@ private LoadController(final int initialPlaybackBufferMs, DefaultLoadControl.Builder builder = new DefaultLoadControl.Builder(); builder.setBufferDurationsMs(minimumPlaybackbufferMs, optimalPlaybackBufferMs, - initialPlaybackBufferMs, initialPlaybackBufferMs); + initialPlaybackBufferMs, + DefaultLoadControl.DEFAULT_BUFFER_FOR_PLAYBACK_AFTER_REBUFFER_MS); internalLoadControl = builder.createDefaultLoadControl(); } diff --git a/app/src/main/java/org/schabi/newpipelegacy/player/helper/PlaybackParameterDialog.java b/app/src/main/java/org/schabi/newpipelegacy/player/helper/PlaybackParameterDialog.java index 80ac308c0..dbe94a289 100644 --- a/app/src/main/java/org/schabi/newpipelegacy/player/helper/PlaybackParameterDialog.java +++ b/app/src/main/java/org/schabi/newpipelegacy/player/helper/PlaybackParameterDialog.java @@ -1,5 +1,7 @@ package org.schabi.newpipelegacy.player.helper; +import static org.schabi.newpipelegacy.player.BasePlayer.DEBUG; +import static org.schabi.newpipelegacy.util.Localization.assureCorrectAppLanguage; import android.app.Dialog; import android.content.Context; import android.os.Bundle; @@ -18,9 +20,6 @@ import org.schabi.newpipelegacy.R; import org.schabi.newpipelegacy.util.SliderStrategy; -import static org.schabi.newpipelegacy.player.BasePlayer.DEBUG; -import static org.schabi.newpipelegacy.util.Localization.assureCorrectAppLanguage; - public class PlaybackParameterDialog extends DialogFragment { // Minimum allowable range in ExoPlayer private static final double MINIMUM_PLAYBACK_VALUE = 0.10f; @@ -155,7 +154,6 @@ public Dialog onCreateDialog(@Nullable final Bundle savedInstanceState) { setupControlViews(view); final AlertDialog.Builder dialogBuilder = new AlertDialog.Builder(requireActivity()) - .setTitle(R.string.playback_speed_control) .setView(view) .setCancelable(true) .setNegativeButton(R.string.cancel, (dialogInterface, i) -> diff --git a/app/src/main/java/org/schabi/newpipelegacy/player/helper/PlayerDataSource.java b/app/src/main/java/org/schabi/newpipelegacy/player/helper/PlayerDataSource.java index 09bdab3f7..48e47d3f9 100644 --- a/app/src/main/java/org/schabi/newpipelegacy/player/helper/PlayerDataSource.java +++ b/app/src/main/java/org/schabi/newpipelegacy/player/helper/PlayerDataSource.java @@ -21,11 +21,13 @@ public class PlayerDataSource { private static final int EXTRACTOR_MINIMUM_RETRY = Integer.MAX_VALUE; private static final int LIVE_STREAM_EDGE_GAP_MILLIS = 10000; + private final Integer continueLoadingCheckIntervalBytes; private final DataSource.Factory cacheDataSourceFactory; private final DataSource.Factory cachelessDataSourceFactory; public PlayerDataSource(@NonNull final Context context, @NonNull final String userAgent, @NonNull final TransferListener transferListener) { + continueLoadingCheckIntervalBytes = PlayerHelper.getProgressiveLoadIntervalBytes(context); cacheDataSourceFactory = new CacheFactory(context, userAgent, transferListener); cachelessDataSourceFactory = new DefaultDataSourceFactory(context, userAgent, transferListener); @@ -70,6 +72,7 @@ public DashMediaSource.Factory getDashMediaSourceFactory() { public ProgressiveMediaSource.Factory getExtractorMediaSourceFactory() { return new ProgressiveMediaSource.Factory(cacheDataSourceFactory) + .setContinueLoadingCheckIntervalBytes(continueLoadingCheckIntervalBytes) .setLoadErrorHandlingPolicy( new DefaultLoadErrorHandlingPolicy(EXTRACTOR_MINIMUM_RETRY)); } diff --git a/app/src/main/java/org/schabi/newpipelegacy/player/helper/PlayerHelper.java b/app/src/main/java/org/schabi/newpipelegacy/player/helper/PlayerHelper.java index 21c57aada..6bf6cc920 100644 --- a/app/src/main/java/org/schabi/newpipelegacy/player/helper/PlayerHelper.java +++ b/app/src/main/java/org/schabi/newpipelegacy/player/helper/PlayerHelper.java @@ -38,6 +38,7 @@ import java.util.HashSet; import java.util.List; import java.util.Locale; +import java.util.Objects; import java.util.Set; import java.util.concurrent.TimeUnit; @@ -229,7 +230,7 @@ public static long getPreferredCacheSize() { } public static long getPreferredFileSize() { - return 512 * 1024L; + return 2 * 1024 * 1024L; // ExoPlayer CacheDataSink.MIN_RECOMMENDED_FRAGMENT_SIZE } /** @@ -324,6 +325,13 @@ public static void setScreenBrightness(@NonNull final Context context, setScreenBrightness(context, setScreenBrightness, System.currentTimeMillis()); } + @NonNull + public static Integer getProgressiveLoadIntervalBytes(@NonNull final Context context) { + return Integer.parseInt(Objects.requireNonNull(getPreferences(context).getString( + context.getString(R.string.progressive_load_interval_key), + context.getString(R.string.progressive_load_interval_bytes_default_value)))); + } + //////////////////////////////////////////////////////////////////////////// // Private helpers //////////////////////////////////////////////////////////////////////////// diff --git a/app/src/main/java/org/schabi/newpipelegacy/player/playqueue/PlayQueueItemBuilder.java b/app/src/main/java/org/schabi/newpipelegacy/player/playqueue/PlayQueueItemBuilder.java index d95a19b65..4a842af6e 100644 --- a/app/src/main/java/org/schabi/newpipelegacy/player/playqueue/PlayQueueItemBuilder.java +++ b/app/src/main/java/org/schabi/newpipelegacy/player/playqueue/PlayQueueItemBuilder.java @@ -7,9 +7,9 @@ import com.nostra13.universalimageloader.core.ImageLoader; -import org.schabi.newpipe.extractor.NewPipe; import org.schabi.newpipelegacy.util.ImageDisplayConstants; import org.schabi.newpipelegacy.util.Localization; +import org.schabi.newpipelegacy.util.ServiceHelper; public class PlayQueueItemBuilder { private static final String TAG = PlayQueueItemBuilder.class.toString(); @@ -27,7 +27,7 @@ public void buildStreamInfoItem(final PlayQueueItemHolder holder, final PlayQueu holder.itemVideoTitleView.setText(item.getTitle()); } holder.itemAdditionalDetailsView.setText(Localization.concatenateStrings(item.getUploader(), - NewPipe.getNameOfService(item.getServiceId()))); + ServiceHelper.getNameOfServiceById(item.getServiceId()))); if (item.getDuration() > 0) { holder.itemDurationView.setText(Localization.getDurationString(item.getDuration())); diff --git a/app/src/main/java/org/schabi/newpipelegacy/player/resolver/VideoPlaybackResolver.java b/app/src/main/java/org/schabi/newpipelegacy/player/resolver/VideoPlaybackResolver.java index 0b9a2aa8a..2653e9878 100644 --- a/app/src/main/java/org/schabi/newpipelegacy/player/resolver/VideoPlaybackResolver.java +++ b/app/src/main/java/org/schabi/newpipelegacy/player/resolver/VideoPlaybackResolver.java @@ -106,7 +106,9 @@ public MediaSource resolve(@NonNull final StreamInfo info) { SELECTION_FLAG_AUTOSELECT, PlayerHelper.captionLanguageOf(context, subtitle)); final MediaSource textSource = dataSource.getSampleMediaSourceFactory() - .createMediaSource(Uri.parse(subtitle.getURL()), textFormat, TIME_UNSET); + .createMediaSource( + Uri.parse(subtitle.getContent()), textFormat, TIME_UNSET + ); mediaSources.add(textSource); } } diff --git a/app/src/main/java/org/schabi/newpipelegacy/report/AcraReportSenderFactory.java b/app/src/main/java/org/schabi/newpipelegacy/report/AcraReportSenderFactory.java index e0562bae4..c0088774a 100644 --- a/app/src/main/java/org/schabi/newpipelegacy/report/AcraReportSenderFactory.java +++ b/app/src/main/java/org/schabi/newpipelegacy/report/AcraReportSenderFactory.java @@ -4,9 +4,12 @@ import androidx.annotation.NonNull; +import com.google.auto.service.AutoService; + import org.acra.config.CoreConfiguration; import org.acra.sender.ReportSender; import org.acra.sender.ReportSenderFactory; +import org.schabi.newpipelegacy.App; /* * Created by Christian Schabesberger on 13.09.16. @@ -28,6 +31,10 @@ * along with NewPipe. If not, see . */ +/** + * Used by ACRA in {@link App}.initAcra() as the factory for report senders. + */ +@AutoService(ReportSenderFactory.class) public class AcraReportSenderFactory implements ReportSenderFactory { @NonNull public ReportSender create(@NonNull final Context context, diff --git a/app/src/main/java/org/schabi/newpipelegacy/settings/ContentSettingsFragment.java b/app/src/main/java/org/schabi/newpipelegacy/settings/ContentSettingsFragment.java index d087f683c..febd8be98 100644 --- a/app/src/main/java/org/schabi/newpipelegacy/settings/ContentSettingsFragment.java +++ b/app/src/main/java/org/schabi/newpipelegacy/settings/ContentSettingsFragment.java @@ -21,6 +21,7 @@ import org.schabi.newpipelegacy.DownloaderImpl; import org.schabi.newpipelegacy.NewPipeDatabase; import org.schabi.newpipelegacy.R; +import org.schabi.newpipelegacy.ReCaptchaActivity; import org.schabi.newpipe.extractor.NewPipe; import org.schabi.newpipe.extractor.localization.ContentCountry; import org.schabi.newpipe.extractor.localization.Localization; @@ -76,6 +77,22 @@ public void onCreate(@Nullable final Bundle savedInstanceState) { .getPreferredContentCountry(requireContext()); initialLanguage = PreferenceManager .getDefaultSharedPreferences(getContext()).getString("app_language_key", "en"); + + final Preference clearCookiePref = findPreference(getString(R.string.clear_cookie_key)); + + clearCookiePref.setOnPreferenceClickListener(preference -> { + defaultPreferences.edit() + .putString(getString(R.string.recaptcha_cookies_key), "").apply(); + DownloaderImpl.getInstance().setCookie(ReCaptchaActivity.RECAPTCHA_COOKIES_KEY, ""); + Toast.makeText(getActivity(), R.string.recaptcha_cookies_cleared, + Toast.LENGTH_SHORT).show(); + clearCookiePref.setVisible(false); + return true; + }); + + if (defaultPreferences.getString(getString(R.string.recaptcha_cookies_key), "").isEmpty()) { + clearCookiePref.setVisible(false); + } } @Override diff --git a/app/src/main/java/org/schabi/newpipelegacy/settings/PeertubeInstanceListFragment.java b/app/src/main/java/org/schabi/newpipelegacy/settings/PeertubeInstanceListFragment.java index bf9508d8b..1415e4b21 100644 --- a/app/src/main/java/org/schabi/newpipelegacy/settings/PeertubeInstanceListFragment.java +++ b/app/src/main/java/org/schabi/newpipelegacy/settings/PeertubeInstanceListFragment.java @@ -204,7 +204,7 @@ private void restoreDefaults() { .setNegativeButton(R.string.cancel, null) .setPositiveButton(R.string.yes, (dialog, which) -> { sharedPreferences.edit().remove(savedInstanceListKey).apply(); - selectInstance(PeertubeInstance.defaultInstance); + selectInstance(PeertubeInstance.DEFAULT_INSTANCE); updateInstanceList(); instanceListAdapter.notifyDataSetChanged(); }) diff --git a/app/src/main/java/org/schabi/newpipelegacy/settings/VideoAudioSettingsFragment.java b/app/src/main/java/org/schabi/newpipelegacy/settings/VideoAudioSettingsFragment.java index 4159ac5c3..03e1d48f6 100644 --- a/app/src/main/java/org/schabi/newpipelegacy/settings/VideoAudioSettingsFragment.java +++ b/app/src/main/java/org/schabi/newpipelegacy/settings/VideoAudioSettingsFragment.java @@ -16,11 +16,14 @@ import org.schabi.newpipelegacy.R; import org.schabi.newpipelegacy.util.PermissionHelper; -import java.util.LinkedList; +import java.util.ArrayList; +import java.util.Arrays; import java.util.List; +import java.util.LinkedList; public class VideoAudioSettingsFragment extends BasePreferenceFragment { private SharedPreferences.OnSharedPreferenceChangeListener listener; + private ListPreference defaultRes,defaultPopupRes,limitMobDataUsage; @Override public void onCreate(@Nullable final Bundle savedInstanceState) { @@ -29,6 +32,9 @@ public void onCreate(@Nullable final Bundle savedInstanceState) { updateSeekOptions(); listener = (sharedPreferences, s) -> { + defaultRes = (ListPreference) findPreference(getString(R.string.default_resolution_key)); + defaultPopupRes = (ListPreference) findPreference(getString(R.string.default_popup_resolution_key)); + limitMobDataUsage = (ListPreference) findPreference(getString(R.string.limit_mobile_data_usage_key)); // on M and above, if user chooses to minimise to popup player on exit // and the app doesn't have display over other apps permission, @@ -50,7 +56,72 @@ public void onCreate(@Nullable final Bundle savedInstanceState) { } else if (s.equals(getString(R.string.use_inexact_seek_key))) { updateSeekOptions(); } + + //check if "show higher resolutions" was changed + if(s.equals(getString(R.string.show_higher_resolutions_key))){ + + if(checkIfShowHighRes()){ + showHigherResolutions(true); + } + else { + + //if the setting was turned off and any of the defaults is set to 1440p or 2160p, change them to 1080p60 + //(the next highest value) + if(defaultRes.getValue().equals("1440p") || defaultRes.getValue().equals("1440p60") || + defaultRes.getValue().equals("2160p") || defaultRes.getValue().equals("2160p60")){ + defaultRes.setValueIndex(3); + } + if(defaultPopupRes.getValue().equals("1440p") || defaultPopupRes.getValue().equals("1440p60") || + defaultPopupRes.getValue().equals("2160p") || defaultPopupRes.getValue().equals("2160p60")){ + defaultPopupRes.setValueIndex(3); + } + if(limitMobDataUsage.getValue().equals("1440p") || limitMobDataUsage.getValue().equals("1440p60") || + limitMobDataUsage.getValue().equals("2160p") || limitMobDataUsage.getValue().equals("2160p60")){ + limitMobDataUsage.setValueIndex(3); + } + + showHigherResolutions(false); + + } + } + }; + if(!checkIfShowHighRes()){ + showHigherResolutions(false); + } + } + + private boolean checkIfShowHighRes(){ + return getPreferenceManager().getSharedPreferences().getBoolean(getString(R.string.show_higher_resolutions_key),false); + } + + private void showHigherResolutions(boolean show){ + + Resources res = getResources(); + ArrayList resolutions = new ArrayList(Arrays.asList(res.getStringArray(R.array.resolution_list_description))); + ArrayList resolutionValues = new ArrayList(Arrays.asList(res.getStringArray(R.array.resolution_list_values))); + + ArrayList mobileDataResolutions = new ArrayList(Arrays.asList(res.getStringArray(R.array.limit_data_usage_description_list))); + ArrayList mobileDataResolutionValues = new ArrayList(Arrays.asList(res.getStringArray(R.array.limit_data_usage_values_list))); + + if(!show) { + List higherResolutions = Arrays.asList("1440p", "1440p60", "2160p", "2160p60"); + + resolutions.removeAll(higherResolutions); + resolutionValues.removeAll(higherResolutions); + + mobileDataResolutions.removeAll(higherResolutions); + mobileDataResolutionValues.removeAll(higherResolutions); + } + + defaultRes.setEntries(resolutions.toArray(new String[resolutions.size()])); + defaultRes.setEntryValues(resolutionValues.toArray(new String[resolutionValues.size()])); + + defaultPopupRes.setEntries(resolutions.toArray(new String[resolutions.size()])); + defaultPopupRes.setEntryValues(resolutionValues.toArray(new String[resolutionValues.size()])); + + limitMobDataUsage.setEntries(mobileDataResolutions.toArray(new String[mobileDataResolutions.size()])); + limitMobDataUsage.setEntryValues(mobileDataResolutionValues.toArray(new String[mobileDataResolutionValues.size()])); } /** @@ -91,6 +162,11 @@ private void updateSeekOptions() { getString(R.string.seek_duration_key)); durations.setEntryValues(displayedDurationValues.toArray(new CharSequence[0])); durations.setEntries(displayedDescriptionValues.toArray(new CharSequence[0])); + defaultRes = (ListPreference) findPreference(getString(R.string.default_resolution_key)); + defaultPopupRes = (ListPreference) findPreference( + getString(R.string.default_popup_resolution_key)); + limitMobDataUsage = (ListPreference) findPreference( + getString(R.string.limit_mobile_data_usage_key)); final int selectedDuration = Integer.parseInt(durations.getValue()); if (inexactSeek && selectedDuration / (int) DateUtils.SECOND_IN_MILLIS % 10 == 5) { final int newDuration = selectedDuration / (int) DateUtils.SECOND_IN_MILLIS + 5; diff --git a/app/src/main/java/org/schabi/newpipelegacy/settings/tabs/ChooseTabsFragment.java b/app/src/main/java/org/schabi/newpipelegacy/settings/tabs/ChooseTabsFragment.java index feb9e9f94..fc78bebd5 100644 --- a/app/src/main/java/org/schabi/newpipelegacy/settings/tabs/ChooseTabsFragment.java +++ b/app/src/main/java/org/schabi/newpipelegacy/settings/tabs/ChooseTabsFragment.java @@ -29,12 +29,12 @@ import com.google.android.material.floatingactionbutton.FloatingActionButton; import org.schabi.newpipelegacy.R; -import org.schabi.newpipe.extractor.NewPipe; import org.schabi.newpipelegacy.report.ErrorActivity; import org.schabi.newpipelegacy.report.UserAction; import org.schabi.newpipelegacy.settings.SelectChannelFragment; import org.schabi.newpipelegacy.settings.SelectKioskFragment; import org.schabi.newpipelegacy.settings.SelectPlaylistFragment; +import org.schabi.newpipelegacy.util.ServiceHelper; import org.schabi.newpipelegacy.settings.tabs.AddTabDialog.ChooseTabListItem; import org.schabi.newpipelegacy.util.ThemeHelper; @@ -409,18 +409,18 @@ void bind(final int position, final TabViewHolder holder) { tabName = getString(R.string.default_kiosk_page_summary); break; case KIOSK: - tabName = NewPipe.getNameOfService(((Tab.KioskTab) tab) + tabName = ServiceHelper.getNameOfServiceById(((Tab.KioskTab) tab) .getKioskServiceId()) + "/" + tab.getTabName(requireContext()); break; case CHANNEL: - tabName = NewPipe.getNameOfService(((Tab.ChannelTab) tab) + tabName = ServiceHelper.getNameOfServiceById(((Tab.ChannelTab) tab) .getChannelServiceId()) + "/" + tab.getTabName(requireContext()); break; case PLAYLIST: final int serviceId = ((Tab.PlaylistTab) tab).getPlaylistServiceId(); final String serviceName = serviceId == -1 ? getString(R.string.local) - : NewPipe.getNameOfService(serviceId); + : ServiceHelper.getNameOfServiceById(serviceId); tabName = serviceName + "/" + tab.getTabName(requireContext()); break; default: diff --git a/app/src/main/java/org/schabi/newpipelegacy/util/ExceptionUtils.kt b/app/src/main/java/org/schabi/newpipelegacy/util/ExceptionUtils.kt index 9cc4ee369..82c3f3969 100644 --- a/app/src/main/java/org/schabi/newpipelegacy/util/ExceptionUtils.kt +++ b/app/src/main/java/org/schabi/newpipelegacy/util/ExceptionUtils.kt @@ -10,9 +10,11 @@ class ExceptionUtils { */ @JvmStatic fun isInterruptedCaused(throwable: Throwable): Boolean { - return hasExactCause(throwable, - InterruptedIOException::class.java, - InterruptedException::class.java) + return hasExactCause( + throwable, + InterruptedIOException::class.java, + InterruptedException::class.java + ) } /** @@ -20,10 +22,11 @@ class ExceptionUtils { */ @JvmStatic fun isNetworkRelated(throwable: Throwable): Boolean { - return hasAssignableCause(throwable, - IOException::class.java) + return hasAssignableCause( + throwable, + IOException::class.java + ) } - /** * Calls [hasCause] with the `checkSubtypes` parameter set to false. */ diff --git a/app/src/main/java/org/schabi/newpipelegacy/util/ExtractorHelper.java b/app/src/main/java/org/schabi/newpipelegacy/util/ExtractorHelper.java index 7604eb9f3..e0a3a7a08 100644 --- a/app/src/main/java/org/schabi/newpipelegacy/util/ExtractorHelper.java +++ b/app/src/main/java/org/schabi/newpipelegacy/util/ExtractorHelper.java @@ -1,6 +1,6 @@ /* * Copyright 2017 Mauricio Colli - * Extractors.java is part of NewPipe + * ExtractorHelper.java is part of NewPipe * * License: GPL-3.0+ * This program is free software: you can redistribute it and/or modify @@ -37,10 +37,16 @@ import org.schabi.newpipe.extractor.StreamingService; import org.schabi.newpipe.extractor.channel.ChannelInfo; import org.schabi.newpipe.extractor.comments.CommentsInfo; +import org.schabi.newpipe.extractor.exceptions.AgeRestrictedContentException; import org.schabi.newpipe.extractor.exceptions.ContentNotAvailableException; import org.schabi.newpipe.extractor.exceptions.ContentNotSupportedException; +import org.schabi.newpipe.extractor.exceptions.GeographicRestrictionException; +import org.schabi.newpipe.extractor.exceptions.PaidContentException; import org.schabi.newpipe.extractor.exceptions.ParsingException; +import org.schabi.newpipe.extractor.exceptions.PrivateContentException; import org.schabi.newpipe.extractor.exceptions.ReCaptchaException; +import org.schabi.newpipe.extractor.exceptions.SoundCloudGoPlusContentException; +import org.schabi.newpipe.extractor.exceptions.YoutubeMusicPremiumContentException; import org.schabi.newpipe.extractor.feed.FeedExtractor; import org.schabi.newpipe.extractor.feed.FeedInfo; import org.schabi.newpipe.extractor.kiosk.KioskInfo; @@ -288,18 +294,33 @@ public static void handleGeneralException(final Context context, final int servi context.startActivity(intent); } else if (ExceptionUtils.isNetworkRelated(exception)) { Toast.makeText(context, R.string.network_error, Toast.LENGTH_LONG).show(); + } else if (exception instanceof AgeRestrictedContentException) { + Toast.makeText(context, R.string.restricted_video_no_stream, + Toast.LENGTH_LONG).show(); + } else if (exception instanceof GeographicRestrictionException) { + Toast.makeText(context, R.string.georestricted_content, Toast.LENGTH_LONG).show(); + } else if (exception instanceof PaidContentException) { + Toast.makeText(context, R.string.paid_content, Toast.LENGTH_LONG).show(); + } else if (exception instanceof PrivateContentException) { + Toast.makeText(context, R.string.private_content, Toast.LENGTH_LONG).show(); + } else if (exception instanceof SoundCloudGoPlusContentException) { + Toast.makeText(context, R.string.soundcloud_go_plus_content, + Toast.LENGTH_LONG).show(); + } else if (exception instanceof YoutubeMusicPremiumContentException) { + Toast.makeText(context, R.string.youtube_music_premium_content, + Toast.LENGTH_LONG).show(); } else if (exception instanceof ContentNotAvailableException) { Toast.makeText(context, R.string.content_not_available, Toast.LENGTH_LONG).show(); } else if (exception instanceof ContentNotSupportedException) { Toast.makeText(context, R.string.content_not_supported, Toast.LENGTH_LONG).show(); } else { - int errorId = exception instanceof YoutubeStreamExtractor.DecryptException + int errorId = exception instanceof YoutubeStreamExtractor.DeobfuscateException ? R.string.youtube_signature_decryption_error : exception instanceof ParsingException ? R.string.parsing_error : R.string.general_error; ErrorActivity.reportError(handler, context, exception, MainActivity.class, null, ErrorActivity.ErrorInfo.make(userAction, serviceId == -1 ? "none" - : NewPipe.getNameOfService(serviceId), + : ServiceHelper.getNameOfServiceById(serviceId), url + (optionalErrorMessage == null ? "" : optionalErrorMessage), errorId)); } diff --git a/app/src/main/java/org/schabi/newpipelegacy/util/KioskTranslator.java b/app/src/main/java/org/schabi/newpipelegacy/util/KioskTranslator.java index dde686970..6552baea0 100644 --- a/app/src/main/java/org/schabi/newpipelegacy/util/KioskTranslator.java +++ b/app/src/main/java/org/schabi/newpipelegacy/util/KioskTranslator.java @@ -44,6 +44,14 @@ public static String getTranslatedKioskName(final String kioskId, final Context return c.getString(R.string.most_liked); case "conferences": return c.getString(R.string.conferences); + case "recent": + return c.getString(R.string.recent); + case "live": + return c.getString(R.string.duration_live); + case "Featured": + return c.getString(R.string.featured); + case "Radio": + return c.getString(R.string.radio); default: return kioskId; } @@ -59,9 +67,16 @@ public static int getKioskIcon(final String kioskId, final Context c) { case "Local": return ThemeHelper.resolveResourceIdFromAttr(c, R.attr.ic_kiosk_local); case "Recently added": + case "recent": return ThemeHelper.resolveResourceIdFromAttr(c, R.attr.ic_kiosk_recent); case "Most liked": return ThemeHelper.resolveResourceIdFromAttr(c, R.attr.ic_thumb_up); + case "live": + return ThemeHelper.resolveResourceIdFromAttr(c, R.attr.ic_live_tv); + case "Featured": + return ThemeHelper.resolveResourceIdFromAttr(c, R.attr.ic_stars); + case "Radio": + return ThemeHelper.resolveResourceIdFromAttr(c, R.attr.ic_radio); default: return 0; } diff --git a/app/src/main/java/org/schabi/newpipelegacy/util/Localization.java b/app/src/main/java/org/schabi/newpipelegacy/util/Localization.java index 6abf785cb..8cd0e3bae 100644 --- a/app/src/main/java/org/schabi/newpipelegacy/util/Localization.java +++ b/app/src/main/java/org/schabi/newpipelegacy/util/Localization.java @@ -5,6 +5,7 @@ import android.content.SharedPreferences; import android.content.res.Configuration; import android.content.res.Resources; +import android.icu.text.CompactDecimalFormat; import android.os.Build; import android.preference.PreferenceManager; import android.text.TextUtils; @@ -185,6 +186,11 @@ public static String localizeWatchingCount(final Context context, final long wat } public static String shortCount(final Context context, final long count) { + if (Build.VERSION.SDK_INT >= 24) { + return CompactDecimalFormat.getInstance(getAppLocale(context), + CompactDecimalFormat.CompactStyle.SHORT).format(count); + } + double value = (double) count; if (count >= 1000000000) { return localizeNumber(context, round(value / 1000000000, 1)) diff --git a/app/src/main/java/org/schabi/newpipelegacy/util/NavigationHelper.java b/app/src/main/java/org/schabi/newpipelegacy/util/NavigationHelper.java index f04d6b2f4..bfc16780a 100644 --- a/app/src/main/java/org/schabi/newpipelegacy/util/NavigationHelper.java +++ b/app/src/main/java/org/schabi/newpipelegacy/util/NavigationHelper.java @@ -124,7 +124,8 @@ public static Intent getPlayerIntent(@NonNull final Context context, .putExtra(BasePlayer.IS_MUTED, isMuted); } - public static void playOnMainPlayer(final Context context, final PlayQueue queue, + public static void playOnMainPlayer(final Context context, + final PlayQueue queue, final boolean resumePlayback) { final Intent playerIntent = getPlayerIntent(context, MainVideoPlayer.class, queue, resumePlayback); @@ -132,7 +133,8 @@ public static void playOnMainPlayer(final Context context, final PlayQueue queue context.startActivity(playerIntent); } - public static void playOnPopupPlayer(final Context context, final PlayQueue queue, + public static void playOnPopupPlayer(final Context context, + final PlayQueue queue, final boolean resumePlayback) { if (!PermissionHelper.isPopupEnabled(context)) { PermissionHelper.showPopupEnablementToast(context); @@ -144,7 +146,8 @@ public static void playOnPopupPlayer(final Context context, final PlayQueue queu getPlayerIntent(context, PopupVideoPlayer.class, queue, resumePlayback)); } - public static void playOnBackgroundPlayer(final Context context, final PlayQueue queue, + public static void playOnBackgroundPlayer(final Context context, + final PlayQueue queue, final boolean resumePlayback) { Toast.makeText(context, R.string.background_player_playing_toast, Toast.LENGTH_SHORT) .show(); @@ -481,6 +484,11 @@ public static void openRouterActivity(final Context context, final String url) { context.startActivity(mIntent); } + public static void openBackgroundPlayer(final Context context) { + final Intent intent = new Intent(context, BackgroundPlayerActivity.class); + context.startActivity(intent); + } + public static void openAbout(final Context context) { Intent intent = new Intent(context, AboutActivity.class); context.startActivity(intent); diff --git a/app/src/main/java/org/schabi/newpipelegacy/util/ServiceHelper.java b/app/src/main/java/org/schabi/newpipelegacy/util/ServiceHelper.java index b2b32c2d5..15fa7028a 100644 --- a/app/src/main/java/org/schabi/newpipelegacy/util/ServiceHelper.java +++ b/app/src/main/java/org/schabi/newpipelegacy/util/ServiceHelper.java @@ -6,6 +6,8 @@ import androidx.annotation.DrawableRes; import androidx.annotation.StringRes; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; import com.grack.nanojson.JsonObject; import com.grack.nanojson.JsonParser; @@ -38,6 +40,8 @@ public static int getIcon(final int serviceId) { return R.drawable.place_holder_gadse; case 3: return R.drawable.place_holder_peertube; + case 4: + return R.drawable.place_holder_bandcamp; default: return R.drawable.place_holder_circle; } @@ -48,6 +52,7 @@ public static String getTranslatedFilterString(final String filter, final Contex case "all": return c.getString(R.string.all); case "videos": + case "sepia_videos": case "music_videos": return c.getString(R.string.videos_string); case "channels": @@ -124,6 +129,29 @@ public static int getSelectedServiceId(final Context context) { return serviceId; } + @Nullable + public static StreamingService getSelectedService(final Context context) { + final String serviceName = PreferenceManager.getDefaultSharedPreferences(context) + .getString(context.getString(R.string.current_service_key), + context.getString(R.string.default_service_value)); + + try { + return NewPipe.getService(serviceName); + } catch (final ExtractionException e) { + return null; + } + } + + @NonNull + public static String getNameOfServiceById(final int serviceId) { + return ServiceList.all().stream() + .filter(s -> s.getServiceId() == serviceId) + .findFirst() + .map(StreamingService::getServiceInfo) + .map(StreamingService.ServiceInfo::getName) + .orElse(""); + } + public static void setSelectedServiceId(final Context context, final int serviceId) { String serviceName; try { @@ -135,8 +163,10 @@ public static void setSelectedServiceId(final Context context, final int service setSelectedServicePreferences(context, serviceName); } - public static void setSelectedServiceId(final Context context, final String serviceName) { - int serviceId = NewPipe.getIdOfService(serviceName); + public static void setSelectedServiceId( + final Context context, final String serviceName) + throws ExtractionException { + int serviceId = NewPipe.getService(serviceName).getServiceId(); if (serviceId == -1) { setSelectedServicePreferences(context, DEFAULT_FALLBACK_SERVICE.getServiceInfo().getName()); diff --git a/app/src/main/java/org/schabi/newpipelegacy/views/FocusOverlayView.java b/app/src/main/java/org/schabi/newpipelegacy/views/FocusOverlayView.java index 30c8b8338..ac2b60a62 100644 --- a/app/src/main/java/org/schabi/newpipelegacy/views/FocusOverlayView.java +++ b/app/src/main/java/org/schabi/newpipelegacy/views/FocusOverlayView.java @@ -171,6 +171,8 @@ public void setColorFilter(final ColorFilter colorFilter) { } public static void setupFocusObserver(final Dialog dialog) { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN_MR2) + return; Rect displayRect = new Rect(); Window window = dialog.getWindow(); @@ -186,6 +188,8 @@ public static void setupFocusObserver(final Dialog dialog) { } public static void setupFocusObserver(final Activity activity) { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN_MR2) + return; Rect displayRect = new Rect(); Window window = activity.getWindow(); diff --git a/app/src/main/java/us/shandian/giga/get/DownloadMissionRecover.java b/app/src/main/java/us/shandian/giga/get/DownloadMissionRecover.java index 83389e489..f088991bd 100644 --- a/app/src/main/java/us/shandian/giga/get/DownloadMissionRecover.java +++ b/app/src/main/java/us/shandian/giga/get/DownloadMissionRecover.java @@ -132,7 +132,7 @@ private void resolveStream() throws IOException, ExtractionException, HttpError switch (mRecovery.kind) { case 'a': for (AudioStream audio : mExtractor.getAudioStreams()) { - if (audio.average_bitrate == mRecovery.desiredBitrate && audio.getFormat() == mRecovery.format) { + if (audio.getAverageBitrate() == mRecovery.desiredBitrate && audio.getFormat() == mRecovery.format) { url = audio.getUrl(); break; } @@ -155,7 +155,7 @@ private void resolveStream() throws IOException, ExtractionException, HttpError for (SubtitlesStream subtitles : mExtractor.getSubtitles(mRecovery.format)) { String tag = subtitles.getLanguageTag(); if (tag.equals(mRecovery.desired) && subtitles.isAutoGenerated() == mRecovery.desired2) { - url = subtitles.getURL(); + url = subtitles.getUrl(); break; } } diff --git a/app/src/main/java/us/shandian/giga/get/MissionRecoveryInfo.java b/app/src/main/java/us/shandian/giga/get/MissionRecoveryInfo.java index 4a2948131..b8304cd6b 100644 --- a/app/src/main/java/us/shandian/giga/get/MissionRecoveryInfo.java +++ b/app/src/main/java/us/shandian/giga/get/MissionRecoveryInfo.java @@ -25,7 +25,7 @@ public class MissionRecoveryInfo implements Serializable, Parcelable { public MissionRecoveryInfo(@NonNull Stream stream) { if (stream instanceof AudioStream) { - desiredBitrate = ((AudioStream) stream).average_bitrate; + desiredBitrate = ((AudioStream) stream).getAverageBitrate(); desired2 = false; kind = 'a'; } else if (stream instanceof VideoStream) { diff --git a/app/src/main/res/drawable-nodpi/place_holder_bandcamp.png b/app/src/main/res/drawable-nodpi/place_holder_bandcamp.png new file mode 100644 index 000000000..848e109c2 Binary files /dev/null and b/app/src/main/res/drawable-nodpi/place_holder_bandcamp.png differ diff --git a/app/src/main/res/drawable/ic_heart.xml b/app/src/main/res/drawable/ic_heart.xml new file mode 100644 index 000000000..86d1f0527 --- /dev/null +++ b/app/src/main/res/drawable/ic_heart.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_live_tv_black_24dp.xml b/app/src/main/res/drawable/ic_live_tv_black_24dp.xml new file mode 100644 index 000000000..1f7957c4a --- /dev/null +++ b/app/src/main/res/drawable/ic_live_tv_black_24dp.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/ic_live_tv_white_24dp.xml b/app/src/main/res/drawable/ic_live_tv_white_24dp.xml new file mode 100644 index 000000000..303858f9d --- /dev/null +++ b/app/src/main/res/drawable/ic_live_tv_white_24dp.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/layout-large-land/activity_main_player.xml b/app/src/main/res/layout-large-land/activity_main_player.xml index c5aa806ed..f3930a3ad 100644 --- a/app/src/main/res/layout-large-land/activity_main_player.xml +++ b/app/src/main/res/layout-large-land/activity_main_player.xml @@ -159,6 +159,8 @@ android:layout_alignParentLeft="true" android:layout_alignParentTop="true" android:layout_toLeftOf="@+id/qualityTextView" + android:clickable="true" + android:focusable="true" android:gravity="top" android:orientation="vertical" android:paddingLeft="8dp" @@ -169,15 +171,16 @@ android:id="@+id/titleTextView" android:layout_width="match_parent" android:layout_height="wrap_content" + android:clickable="false" android:ellipsize="marquee" android:fadingEdge="horizontal" + android:focusable="true" android:marqueeRepeatLimit="marquee_forever" android:scrollHorizontally="true" android:singleLine="true" android:textColor="@android:color/white" android:textSize="15sp" android:textStyle="bold" - android:clickable="true" tools:ignore="RtlHardcoded" tools:text="The Video Title LONG very LONG"/> @@ -185,14 +188,15 @@ android:id="@+id/channelTextView" android:layout_width="match_parent" android:layout_height="wrap_content" + android:clickable="false" android:ellipsize="marquee" android:fadingEdge="horizontal" + android:focusable="true" android:marqueeRepeatLimit="marquee_forever" android:scrollHorizontally="true" android:singleLine="true" android:textColor="@android:color/white" android:textSize="12sp" - android:clickable="true" tools:text="The Video Artist LONG very LONG very Long"/> diff --git a/app/src/main/res/layout/activity_main_player.xml b/app/src/main/res/layout/activity_main_player.xml index 4bae123ee..799913d8d 100644 --- a/app/src/main/res/layout/activity_main_player.xml +++ b/app/src/main/res/layout/activity_main_player.xml @@ -157,6 +157,8 @@ android:layout_alignParentLeft="true" android:layout_alignParentTop="true" android:layout_toLeftOf="@+id/qualityTextView" + android:clickable="true" + android:focusable="true" android:gravity="top" android:orientation="vertical" android:paddingLeft="8dp" @@ -167,6 +169,7 @@ android:id="@+id/titleTextView" android:layout_width="match_parent" android:layout_height="wrap_content" + android:clickable="false" android:ellipsize="marquee" android:fadingEdge="horizontal" android:marqueeRepeatLimit="marquee_forever" @@ -175,7 +178,6 @@ android:textColor="@android:color/white" android:textSize="15sp" android:textStyle="bold" - android:clickable="true" android:focusable="true" tools:ignore="RtlHardcoded" tools:text="The Video Title LONG very LONG"/> @@ -184,6 +186,7 @@ android:id="@+id/channelTextView" android:layout_width="match_parent" android:layout_height="wrap_content" + android:clickable="false" android:ellipsize="marquee" android:fadingEdge="horizontal" android:marqueeRepeatLimit="marquee_forever" @@ -191,7 +194,6 @@ android:singleLine="true" android:textColor="@android:color/white" android:textSize="12sp" - android:clickable="true" android:focusable="true" tools:text="The Video Artist LONG very LONG very Long"/> 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 17893fecc..255417730 100644 --- a/app/src/main/res/layout/dialog_feed_group_create.xml +++ b/app/src/main/res/layout/dialog_feed_group_create.xml @@ -37,6 +37,7 @@ android:layout_width="0dp" android:layout_height="wrap_content" android:layout_marginStart="8dp" + android:layout_marginLeft="8dp" android:gravity="center_vertical" android:hint="@string/feed_group_dialog_name_input" android:paddingTop="6dp" @@ -193,6 +194,7 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_alignParentStart="true" + android:layout_alignParentLeft="true" android:layout_centerVertical="true" android:minWidth="0dp" android:scaleType="centerInside" @@ -207,7 +209,8 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_toStartOf="@+id/confirm_button" - android:text="@android:string/cancel" /> + android:layout_toLeftOf="@+id/confirm_button" + android:text="@string/cancel" />