diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index f61e320c9..88e12e728 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -1,64 +1,77 @@ -NewPipe contribution guidelines +FoxPipe contribution guidelines =============================== -PLEASE READ THESE GUIDELINES CAREFULLY BEFORE ANY CONTRIBUTION! - ## Crash reporting -Do not report crashes in the GitHub issue tracker. NewPipe has an automated crash report system that will ask you to -send a report via e-mail when a crash occurs. This contains all the data we need for debugging, and allows you to even -add a comment to it. You'll see exactly what is sent, the system is 100% transparent. +Report crashes through the automated crash report system of NewPipe. +This way all the data needed for debugging is included in your bugreport for GitHub. +You'll see exactly what is sent, be able to add your comments, and then send it. ## Issue reporting/feature requests -* Search the [existing issues](https://github.com/TeamNewPipe/NewPipe/issues) first to make sure your issue/feature -hasn't been reported/requested before. -* Check whether your issue/feature is already fixed/implemented. -* Check if the issue still exists in the latest release/beta version. -* If you are an Android/Java developer, you are always welcome to fix an issue or implement a feature yourself. PRs welcome! -* We use English for development. Issues in other languages will be closed and ignored. +* **Already reported**? Browse the [existing issues](https://github.com/ShareASmile/FoxPipe/issues) to make sure your issue/feature hasn't been reported/requested. +* **Already fixed**? Check whether your issue/feature is already fixed/implemented. +* **Still relevant**? Check if the issue still exists in the latest release/beta version. +* **Can you fix it**? If you are an Android/Java developer, you are always welcome to fix an issue or implement a feature yourself. PRs welcome! +* **Is it in English**? We use English for development. Issues in other languages will be closed and ignored. * Please only add *one* issue at a time. Do not put multiple issues into one thread. -* Follow the template! Issues or feature requests not matching the template might be closed. +* **The template**: Fill it out, Follow the template! Issues or feature requests not matching the template might be closed. -## Bug Fixing -* If you want to help NewPipe to become free of bugs (this is our utopic goal for NewPipe), you can send us an email to -tnp@newpipe.schabi.org to let us know that you intend to help. We'll send you further instructions. You may, on request, -register at our [Sentry](https://sentry.schabi.org) instance (see section "Crash reporting" for more information). +## Code contribution -## Translation +### Guidelines -* NewPipe is translated via [Weblate](https://hosted.weblate.org/projects/newpipe/strings/). You can log in there -with your GitHub account. -* If the language you want to translate is not on Weblate, you can add it: see [How to add a new language](https://github.com/TeamNewPipe/NewPipe/wiki/How-to-add-a-new-language-to-NewPipe) in the wiki. +* Stick to NewPipe's *style conventions* of [checkStyle](https://github.com/checkstyle/checkstyle) and [ktlint](https://github.com/pinterest/ktlint). They run each time you build the project. +* Stick to [F-Droid contribution guidelines](https://f-droid.org/wiki/page/Inclusion_Policy). +* In particular **do not bring non-free software** (e.g. binary blobs) into the project. Make sure you do not introduce any closed-source library from Google. -## Code contribution +### Before starting development -* Stick to NewPipe's style conventions: follow [checkStyle](https://github.com/checkstyle/checkstyle). It will run each time you build the project. -* Do not bring non-free software (e.g. binary blobs) into the project. Also, make sure you do not introduce Google - libraries. -* Stick to [F-Droid contribution guidelines](https://f-droid.org/wiki/page/Inclusion_Policy). -* Make changes on a separate branch with a meaningful name, not on the master neither dev branch. This is commonly known as *feature branch workflow*. You - may then send your changes as a pull request (PR) on GitHub. -* When submitting changes, you confirm that your code is licensed under the terms of the - [GNU General Public License v3](https://www.gnu.org/licenses/gpl-3.0.html). -* Please test (compile and run) your code before you submit changes! Ideally, provide test feedback in the PR - description. Untested code will **not** be merged! +* If you want to help out with an existing bug report or feature request, **leave a comment** on that issue saying you want to try your hand at it. +* If there is no existing issue for what you want to work on, **open a new one** describing the changes you are planning to introduce. This gives the team and the community a chance to give **feedback** before you spend time on something that is already in development, should be done differently, or should be avoided completely. +* Please show **intention to maintain your features** and code after you contribute a PR. Unmaintained code is a hassle for core developers. If you do not intend to maintain features you plan to contribute, please rethink your submission, or clearly state that in the PR description. +* Create PRs that cover only **one specific issue/solution/bug**. Do not create PRs that are huge monoliths and could have been split into multiple independent contributions. +* FoxPipe uses [NewPipeExtractor](https://github.com/TeamNewPipe/NewPipeExtractor) to fetch data from services. If you need to change something there, you must test your changes in NewPipe. Telling NewPipe to use your extractor version can be accomplished by editing the `app/build.gradle` file: the comments under the "NewPipe libraries" section of `dependencies` will help you out. + +### Kotlin in NewPipe +* NewPipe will remain mostly Java for time being +* Contributions containing a simple conversion from Java to Kotlin should be avoided. Conversions to Kotlin should only be done if Kotlin actually brings improvements like bug fixes or better performance which are not, or only with much more effort, implementable in Java. The core team sees Java as an easier to learn and generally well adopted programming language. + +### Creating a Pull Request (PR) + +* Make changes on a **separate branch** with a meaningful name, not on the _master_ branch or the _dev_ branch. This is commonly known as *feature branch workflow*. You may then send your changes as a pull request (PR) on GitHub. +* Please **test** (compile and run) your code before submitting changes! Ideally, provide test feedback in the PR description. Untested code will **not** be merged! +* Respond if someone requests changes or otherwise raises issues about your PRs. * Try to figure out yourself why builds on our CI fail. -* Make sure your PR is up-to-date with the rest of the code. Often, a simple click on "Update branch" will do the job, - but if not, you are asked to rebase the dev branch manually and resolve the problems on your own. You can find help [on the wiki](https://github.com/TeamNewPipe/NewPipe/wiki/How-to-merge-a-PR). That will make the - maintainers' jobs way easier. -* Please show intention to maintain your features and code after you contributed it. Unmaintained code is a hassle for - the core developers, and just adds work. If you do not intend to maintain features you contributed, please think again - about submission, or clearly state that in the description of your PR. -* Respond yourselves if someone requests changes or otherwise raises issues about your PRs. -* Send PR that only cover one specific issue/solution/bug. Do not send PRs that are huge and consists of multiple - independent solutions. +* Make sure your PR is **up-to-date** with the rest of the code. Often, a simple click on "Update branch" will do the job, but if not, you must *rebase* your branch on the `dev` branch manually and resolve the conflicts on your own. You can find help [on the wiki](https://github.com/TeamNewPipe/NewPipe/wiki/How-to-merge-a-PR). Doing this makes the maintainers' job way easier. + +## IDE setup & building the app + +### Basic setup + +NewPipe is developed using [Android Studio](https://developer.android.com/studio/). Learn more about how to install it and how it works in the [official documentation](https://developer.android.com/studio/intro). In particular, make sure you have accepted Android Studio's SDK licences. Once Android Studio is ready, setting up the NewPipe project is fairly simple: +- Clone the NewPipe repository with `git clone https://github.com/SharASmile/FoxPipe.git` (or use the link from your own fork, if you want to open a PR). +- Open the folder you just cloned with Android Studio. +- Build and run it just like you would do with any other app, with the green triangle in the top bar. + +You may find [SonarLint](https://www.sonarlint.org/intellij)'s **inspections** useful in helping you to write good code and prevent bugs. + +### checkStyle setup + +The [checkStyle](https://github.com/checkstyle/checkstyle) plugin verifies that Java code abides by the project style. It runs automatically each time you build the project. If you want to view errors directly in the editor, instead of having to skim through the build output, you can install an Android Studio plugin: +- Go to `File -> Settings -> Plugins`, search for `checkstyle` and install `CheckStyle-IDEA`. +- Go to `File -> Settings -> Tools -> Checkstyle`. +- Add NewPipe's configuration file by clicking the `+` in the right toolbar of the "Configuration File" list. +- Under the "Use a local Checkstyle file" bullet, click on `Browse` and pick the file named `checkstyle.xml` in the project's root folder. +- Enable "Store relative to project location" so that moving the directory around does not create issues. +- Insert a description in the top bar, then click `Next` and then `Finish`. +- Activate the configuration file you just added by enabling the checkbox on the left. +- Click `Ok` and you are done. + +### ktlint setup + +The [ktlint](https://github.com/pinterest/ktlint) plugin does the same job as checkStyle for Kotlin files. Installing the related plugin is as simple as going to `File -> Settings -> Plugins`, searching for `ktlint` and installing `Ktlint (unofficial)`. ## Communication -* There is an IRC channel on Freenode which is regularly visited by the core team and other developers: - [#newpipe](irc:irc.freenode.net/newpipe). [Click here for Webchat](https://webchat.freenode.net/?channels=newpipe)! -* If you want to get in touch with the core team or one of our other contributors you can send an email to - tnp@newpipe.schabi.org. Please do not send issue reports, they will be ignored and remain unanswered! Use the GitHub issue - tracker described above! -* Feel free to post suggestions, changes, ideas etc. on GitHub or IRC! +* You can post your suggestions, changes, ideas etc. on either GitHub \ No newline at end of file diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml index bbbfee912..2c79d62cd 100644 --- a/.github/FUNDING.yml +++ b/.github/FUNDING.yml @@ -1 +1 @@ -github: XiangRongLin +liberapay: TeamNewPipe diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index dbc1c05a5..e87e58c50 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -7,20 +7,19 @@ assignees: '' --- -Please note, we only support the latest version of NewPipe. In order to check your app version, open the left drawer and click on "About". If you don't have the latest version, upgrade to it and reproduce the problem before opening the issue. The release page (https://github.com/TeamNewPipe/NewPipe/releases/latest) is where you can get it. + -P.S.: Our contribution guidelines might be a nice document to read before you fill out the report :) You can find it at https://github.com/TeamNewPipe/NewPipe/blob/HEAD/.github/CONTRIBUTING.md + -To make it easier for us to help you please enter detailed information in the template we have provided below. If a section isn't relevant, just delete it, though it would be helpful to still provide as much detail as possible. ---> - - +### Checklist + -### Version - -- +- [x] I am using the latest version - x.xx.x +- [ ] I checked, but didn't find any duplicates (open OR closed) of this issue in the repo. +- [ ] I have read the contribution guidelines given at https://github.com/TeamNewPipe/NewPipe/blob/HEAD/.github/CONTRIBUTING.md. +- [ ] This issue contains only one bug. I will open one issue for every bug report I want to file. ### Steps to reproduce the bug + +### Actual behaviour + + + + ### Expected behavior -### Actual behaviour - + ### Screenshots/Screen recordings - + +< + + ### Logs - + + + + + + +### Device info + + - Android version/Custom ROM version: + - Device model: diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 000000000..b0fdb56db --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,8 @@ +blank_issues_enabled: false +contact_links: + - name: 💬 IRC + url: https://web.libera.chat/#newpipe + about: Chat with us via IRC for quick Q/A + - name: 💬 Matrix + url: https://matrix.to/#/#newpipe:libera.chat + about: Chat with us via Matrix for quick Q/A diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md index 90134a204..a071d5ffc 100644 --- a/.github/ISSUE_TEMPLATE/feature_request.md +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -5,35 +5,20 @@ labels: enhancement assignees: '' --- - + - - -#### Describe the feature you want - - - -#### Is your feature request related to a problem? Please describe it - - - + -#### Additional context - +### Checklist + - +- [x] I checked, but didn't find any duplicates (open OR closed) of this issue in the repo. +- [ ] I have read the contribution guidelines given at https://github.com/TeamNewPipe/NewPipe/blob/HEAD/.github/CONTRIBUTING.md. +- [ ] This issue contains only one feature request. I will open one issue for every feature I want to request. -#### How will you/everyone benefit from this feature? - +#### What feature do you want? + - +#### Why do you want this feature? + \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/question.md b/.github/ISSUE_TEMPLATE/question.md new file mode 100644 index 000000000..8c82e6e73 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/question.md @@ -0,0 +1,23 @@ +--- +name: Question +about: Ask about anything NewPipe-Zing related +labels: question +assignees: '' + +--- + + + + + +### Checklist + +- [x] I checked, but didn't find any duplicates (open OR closed) of this issue in the repo. +- [ ] I have read the contribution guidelines given at https://github.com/TeamNewPipe/NewPipe/blob/HEAD/.github/CONTRIBUTING.md. + +#### What's your question(s)? + + +#### Additional context + diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index f12eb2fe8..5b8bcedb8 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -1,28 +1,34 @@ - + #### What is it? -- [ ] Bug fix (user facing) +- [ ] Bugfix (user facing) - [ ] Feature (user facing) -- [ ] Code base improvement (dev facing) +- [ ] Codebase improvement (dev facing) - [ ] Meta improvement to the project (dev facing) #### Description of the changes in your PR - + - record videos - create clones - take over the world +#### Before/After Screenshots/Screen Record + +- Before: +- After: + #### Fixes the following issue(s) - -- + +- Fixes # #### Relies on the following changes - -#### Testing apk - -debug.zip +#### APK testing + + +The APK can be found by going to the "Checks" tab below the title. On the left pane, click on "CI", scroll down to "artifacts" and click "app" to download the zip file which contains the debug APK of this PR. -#### Agreement -- [ ] I carefully read the [contribution guidelines](https://github.com/TeamNewPipe/NewPipe/blob/HEAD/.github/CONTRIBUTING.md) and agree to them. +#### Due diligence +- [ ] I read the [contribution guidelines](https://github.com/TeamNewPipe/NewPipe/blob/HEAD/.github/CONTRIBUTING.md). diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 000000000..afe35d4fe --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,113 @@ +name: CI + +on: + workflow_dispatch: + pull_request: + branches: + - '**' + paths-ignore: + - 'README.md' + - 'fastlane/**' + - 'assets/**' + - '.github/**/*.md' + - '.github/FUNDING.yml' + - '.github/ISSUE_TEMPLATE/**' + push: + branches: + - '**' + 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 19 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 --stacktrace + +# 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 deleted file mode 100644 index b6f5f2acf..000000000 --- a/.github/workflows/release-legacy.yml +++ /dev/null @@ -1,40 +0,0 @@ -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/.github/workflows/release.yml b/.github/workflows/release.yml deleted file mode 100644 index e318206e1..000000000 --- a/.github/workflows/release.yml +++ /dev/null @@ -1,41 +0,0 @@ -name: release - -on: - push: - tags: - - "v*" - - "!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/.gitignore b/.gitignore deleted file mode 100644 index 5c6962be1..000000000 --- a/.gitignore +++ /dev/null @@ -1,20 +0,0 @@ -.gitignore -.gradle -/local.properties -.DS_Store -/build -/captures -/app/app.iml -/.idea -/*.iml -*~ -.weblate -*.class - -# vscode / eclipse files -*.classpath -*.project -*.settings -bin/ -.vscode/ -*.code-workspace diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 1714c70d5..000000000 --- a/.travis.yml +++ /dev/null @@ -1,18 +0,0 @@ -language: android -jdk: - - oraclejdk8 -android: - components: - # The BuildTools version used by NewPipe - - tools - - build-tools-29.0.3 - - # The SDK version used to compile NewPipe - - android-29 - -before_install: - - yes | sdkmanager "platforms;android-29" -script: ./gradlew -Dorg.gradle.jvmargs=-Xmx1536m assembleDebug lintDebug testDebugUnitTest - -licenses: - - '.+' diff --git a/LICENSE b/LICENSE index 94a9ed024..e62ec04cd 100644 --- a/LICENSE +++ b/LICENSE @@ -1,7 +1,7 @@ - GNU GENERAL PUBLIC LICENSE +GNU GENERAL PUBLIC LICENSE Version 3, 29 June 2007 - Copyright (C) 2007 Free Software Foundation, Inc. + Copyright (C) 2007 Free Software Foundation, Inc. Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. @@ -645,7 +645,7 @@ the "copyright" line and a pointer to where the full notice is found. GNU General Public License for more details. You should have received a copy of the GNU General Public License - along with this program. If not, see . + along with this program. If not, see . Also add information on how to contact you by electronic and paper mail. @@ -664,11 +664,11 @@ might be different; for a GUI interface, you would use an "about box". You should also get your employer (if you work as a programmer) or school, if any, to sign a "copyright disclaimer" for the program, if necessary. For more information on this, and how to apply and follow the GNU GPL, see -. +. The GNU General Public License does not permit incorporating your program into proprietary programs. If your program is a subroutine library, you may consider it more useful to permit linking proprietary applications with the library. If this is what you want to do, use the GNU Lesser General Public License instead of this License. But first, please read -. +. diff --git a/README.md b/README.md index 7f81c3fa6..0d9808907 100644 --- a/README.md +++ b/README.md @@ -1,28 +1,23 @@ -# See https://github.com/TeamNewPipe/NewPipe/issues/4918 -No active development is going to occur here! +# FoxPipe - -

-

NewPipe

+A NewPipe fork with the old UI having separate Players handling 3 seperate Queues +

+

FoxPipe

A libre lightweight streaming frontend for Android.

-

- + - - - - +


-

ScreenshotsDescriptionFeaturesUpdatesContributionDonateLicense

-

WebsiteBlogFAQPress

+

ScreenshotsDescriptionFeaturesInstallation and updatesContributionLicense

+

WebsiteFAQPress


WARNING: THIS IS A BETA VERSION, THEREFORE YOU MAY ENCOUNTER BUGS. IF YOU DO, OPEN AN ISSUE VIA OUR GITHUB REPOSITORY. -PUTTING NEWPIPE OR ANY FORK OF IT INTO GOOGLE PLAYSTORE VIOLATES THEIR TERMS OF CONDITIONS. +PUTTING NEWPIPE OR ANY FORK OF IT INTO THE GOOGLE PLAY STORE VIOLATES THEIR TERMS AND CONDITIONS. ## Screenshots @@ -31,17 +26,21 @@ No active development is going to occur here! [](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_08.png) -[](fastlane/metadata/android/en-US/images/phoneScreenshots/shot_09.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 legacy version of [NewPipeExtractor](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 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. -NewPipe does not use any Google framework libraries, nor the YouTube API. Websites are only parsed to fetch required info, so this app can be used on devices without Google services installed. Also, you don't need a YouTube account to use NewPipe, which is 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. ### Features @@ -51,8 +50,7 @@ NewPipe does not use any Google framework libraries, nor the YouTube API. Websit * Listen to YouTube videos * Popup mode (floating player) * Select streaming player to watch video with -* Download videos -* Download audio only +* Make Personalised Watch Later Playlists * Open a video in Kodi * Show next/related videos * Search YouTube in a specific language @@ -73,11 +71,6 @@ NewPipe does not use any Google framework libraries, nor the YouTube API. Websit * Livestream support * Show comments -### Coming Features - -* Cast to UPnP and Cast -* … and many more - ### Supported Services NewPipe supports multiple services. Our [docs](https://teamnewpipe.github.io/documentation/) provide more info on how a new service can be added to the app and the extractor. Please get in touch with us if you intend to add a new one. Currently supported services are: @@ -86,20 +79,17 @@ NewPipe supports multiple services. Our [docs](https://teamnewpipe.github.io/doc * SoundCloud \[beta\] * media.ccc.de \[beta\] * PeerTube instances \[beta\] +* Bandcamp \[beta\] -## Updates -When a change to the NewPipe code occurs (due to either adding features or bug fixing), eventually a release will occur. These are in the format x.xx.x . In order to get this new version, you can: - * 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. - * Download the APK from [releases](https://github.com/TeamNewPipe/NewPipe/releases) and install it. - * Update via F-droid. This is the slowest method of getting updates, as F-Droid must recognize changes, build the APK itself, sign it, then push the update to users. + + -When you install an APK from one of these options, it will be incompatible with an APK from one of the other options. This is due to different signing keys being used. Signing keys help ensure that a user isn't tricked into installing a malicious update to an app, and are independent. F-Droid and GitHub use different signing keys, and building an APK debug excludes a key. The signing key issue is being discussed in issue [#1981](https://github.com/TeamNewPipe/NewPipe/issues/1981), and may be fixed by setting up our own repository on F-Droid. +## 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. -In the meanwhile, if you want to switch sources for some reason (e.g. NewPipe's core functionality was broken and F-Droid doesn't have the update yet), we recommend following this procedure: -1. Back up your data via "Settings>Content>Export Database" so you keep your history, subscriptions, and playlists -2. Uninstall NewPipe -3. Download the APK from the new source and install it -4. Import the data from step 1 via "Settings>Content>Import Database" +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. ## Contribution Whether you have ideas, translations, design changes, code cleaning, or real heavy code changes, help is always welcome. @@ -107,37 +97,17 @@ The more is done the better it gets! If you'd like to get involved, check our [contribution notes](.github/CONTRIBUTING.md). -## 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 [website](https://newpipe.schabi.org/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 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.schabi.org/legal/privacy/). +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. ## License -[![GNU GPLv3 Image](https://www.gnu.org/graphics/gplv3-127x51.png)](http://www.gnu.org/licenses/gpl-3.0.en.html) +[![GNU GPLv3 Image](https://www.gnu.org/graphics/gplv3-127x51.png)](https://www.gnu.org/licenses/gpl-3.0.en.html) 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. +(at your option) any later version. diff --git a/app/.gitignore b/app/.gitignore deleted file mode 100644 index 53edac5e4..000000000 --- a/app/.gitignore +++ /dev/null @@ -1,3 +0,0 @@ -.gitignore -/build -*.iml diff --git a/app/build.gradle b/app/build.gradle index d4889cf70..58f72bc54 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -2,19 +2,19 @@ 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' + compileSdkVersion 30 + buildToolsVersion '30.0.3' defaultConfig { - applicationId "org.schabi.newpipe" - resValue "string", "app_name", "NewPipe" + applicationId "com.soulwin.foxpipe" + resValue "string", "app_name", "FoxPipe" minSdkVersion 19 + //noinspection ExpiredTargetSdkVersion targetSdkVersion 29 - versionCode 993 - versionName "0.22.3.2" + versionCode 956 + versionName "0.19.8.3" multiDexEnabled true @@ -34,15 +34,15 @@ android { // 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" - resValue "string", "app_name", "NewPipe Debug" + resValue "string", "app_name", "FoxPipe debug" } else { applicationIdSuffix ".debug." + normalizedWorkingBranch - resValue "string", "app_name", "NewPipe " + workingBranch - archivesBaseName = 'NewPipe_' + normalizedWorkingBranch + resValue "string", "app_name", "FoxPipe " + workingBranch + archivesBaseName = 'FoxPipe_' + normalizedWorkingBranch } } @@ -51,11 +51,9 @@ 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' - applicationIdSuffix ".preunified" - resValue "string", "app_name", "NewPipe " + "preunified" } } @@ -64,6 +62,9 @@ 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 { @@ -91,7 +92,6 @@ android { ext { icepickVersion = '3.2.0' - checkstyleVersion = '8.38' stethoVersion = '1.5.1' leakCanaryVersion = '2.5' exoPlayerVersion = '2.11.8' @@ -99,127 +99,139 @@ ext { androidxRoomVersion = '2.2.5' 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 { + if (!System.properties.containsKey('skipFormatKtlint')) { + preDebugBuild.dependsOn formatKtlint + } preDebugBuild.dependsOn runKtlint } dependencies { +/** Desugaring **/ coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.1.5' - implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version" - - implementation "frankiesardo:icepick:${icepickVersion}" - kapt "frankiesardo:icepick-processor:${icepickVersion}" - - checkstyle "com.puppycrawl.tools:checkstyle:${checkstyleVersion}" +/** Checkstyle **/ ktlint "com.pinterest:ktlint:0.40.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}" - - implementation "androidx.multidex:multidex:2.0.1" - - testImplementation 'junit:junit:4.13.2' - testImplementation 'org.mockito:mockito-core:3.3.3' +/** Kotlin **/ + implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:${kotlin_version}" - 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:eb07d70a2ce03bee3cc74fc33b2e4173e1c21436' +/** 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.RSoulwin:NewPipeExtractor:d731ca24559172813df0fdebcbf10239cc26d6fc' + +/** Third-party libraries **/ + // Instance state boilerplate elimination + implementation "frankiesardo:icepick:${icepickVersion}" + kapt "frankiesardo:icepick-processor:${icepickVersion}" - implementation "com.github.TeamNewPipe:nanojson:1d9e1aea9049fc9f85e68b43ba39fe7be1c1f751" + // HTML parser implementation "org.jsoup:jsoup:1.13.1" - //noinspection GradleDependency --> do not update okhttp above 3.12.xx to keep supporting Android 4.4 users + // HTTP client + //noinspection GradleDependency --> do not update okhttp beyond 3.12.x to keep supporting Android 4.4 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}" - 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" - 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.room:room-runtime:${androidxRoomVersion}" - implementation "androidx.room:room-rxjava2:${androidxRoomVersion}" - kapt "androidx.room:room-compiler:${androidxRoomVersion}" + // 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" - implementation "ch.acra:acra-core:5.5.0" + // Crash reporting + implementation "ch.acra:acra-core:5.7.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.8.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/newpipe/database/AppDatabaseTest.kt b/app/src/androidTest/java/org/schabi/newpipe/database/AppDatabaseTest.kt index e37eb5db9..239c46f7a 100644 --- a/app/src/androidTest/java/org/schabi/newpipe/database/AppDatabaseTest.kt +++ b/app/src/androidTest/java/org/schabi/newpipe/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/newpipe/DebugApp.kt b/app/src/debug/java/org/schabi/newpipe/DebugApp.kt index 5cfde80b8..24f9fdf3f 100644 --- a/app/src/debug/java/org/schabi/newpipe/DebugApp.kt +++ b/app/src/debug/java/org/schabi/newpipe/DebugApp.kt @@ -1,6 +1,5 @@ package org.schabi.newpipe -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 68c7ea338..f1f4365c6 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -1,7 +1,8 @@ + package="org.schabi.newpipe" + android:installLocation="auto"> @@ -13,6 +14,9 @@ + - + + + @@ -206,28 +215,6 @@ - - - - - - - - - - - - - - - - - - - - - - @@ -239,23 +226,13 @@ - - - - - - - - - - - - - - - + + + + + @@ -283,7 +260,7 @@ - + @@ -314,26 +291,62 @@ + - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/assets/epl1.html b/app/src/main/assets/epl1.html new file mode 100644 index 000000000..7123552dd --- /dev/null +++ b/app/src/main/assets/epl1.html @@ -0,0 +1,245 @@ + + + + + + + Eclipse Public License - Version 1.0 + + + + + +

Eclipse Public License - v 1.0

+ +

THE ACCOMPANYING PROGRAM IS PROVIDED UNDER THE TERMS OF THIS ECLIPSE + PUBLIC LICENSE ("AGREEMENT"). ANY USE, REPRODUCTION OR + DISTRIBUTION OF THE PROGRAM CONSTITUTES RECIPIENT'S ACCEPTANCE OF THIS + AGREEMENT.

+ +

1. DEFINITIONS

+ +

"Contribution" means:

+ +

a) in the case of the initial Contributor, the initial + code and documentation distributed under this Agreement, and

+

b) in the case of each subsequent Contributor:

+

i) changes to the Program, and

+

ii) additions to the Program;

+

where such changes and/or additions to the Program + originate from and are distributed by that particular Contributor. A + Contribution 'originates' from a Contributor if it was added to the + Program by such Contributor itself or anyone acting on such + Contributor's behalf. Contributions do not include additions to the + Program which: (i) are separate modules of software distributed in + conjunction with the Program under their own license agreement, and (ii) + are not derivative works of the Program.

+ +

"Contributor" means any person or entity that distributes + the Program.

+ +

"Licensed Patents" mean patent claims licensable by a + Contributor which are necessarily infringed by the use or sale of its + Contribution alone or when combined with the Program.

+ +

"Program" means the Contributions distributed in accordance + with this Agreement.

+ +

"Recipient" means anyone who receives the Program under + this Agreement, including all Contributors.

+ +

2. GRANT OF RIGHTS

+ +

a) Subject to the terms of this Agreement, each + Contributor hereby grants Recipient a non-exclusive, worldwide, + royalty-free copyright license to reproduce, prepare derivative works + of, publicly display, publicly perform, distribute and sublicense the + Contribution of such Contributor, if any, and such derivative works, in + source code and object code form.

+ +

b) Subject to the terms of this Agreement, each + Contributor hereby grants Recipient a non-exclusive, worldwide, + royalty-free patent license under Licensed Patents to make, use, sell, + offer to sell, import and otherwise transfer the Contribution of such + Contributor, if any, in source code and object code form. This patent + license shall apply to the combination of the Contribution and the + Program if, at the time the Contribution is added by the Contributor, + such addition of the Contribution causes such combination to be covered + by the Licensed Patents. The patent license shall not apply to any other + combinations which include the Contribution. No hardware per se is + licensed hereunder.

+ +

c) Recipient understands that although each Contributor + grants the licenses to its Contributions set forth herein, no assurances + are provided by any Contributor that the Program does not infringe the + patent or other intellectual property rights of any other entity. Each + Contributor disclaims any liability to Recipient for claims brought by + any other entity based on infringement of intellectual property rights + or otherwise. As a condition to exercising the rights and licenses + granted hereunder, each Recipient hereby assumes sole responsibility to + secure any other intellectual property rights needed, if any. For + example, if a third party patent license is required to allow Recipient + to distribute the Program, it is Recipient's responsibility to acquire + that license before distributing the Program.

+ +

d) Each Contributor represents that to its knowledge it + has sufficient copyright rights in its Contribution, if any, to grant + the copyright license set forth in this Agreement.

+ +

3. REQUIREMENTS

+ +

A Contributor may choose to distribute the Program in object code + form under its own license agreement, provided that:

+ +

a) it complies with the terms and conditions of this + Agreement; and

+ +

b) its license agreement:

+ +

i) effectively disclaims on behalf of all Contributors + all warranties and conditions, express and implied, including warranties + or conditions of title and non-infringement, and implied warranties or + conditions of merchantability and fitness for a particular purpose;

+ +

ii) effectively excludes on behalf of all Contributors + all liability for damages, including direct, indirect, special, + incidental and consequential damages, such as lost profits;

+ +

iii) states that any provisions which differ from this + Agreement are offered by that Contributor alone and not by any other + party; and

+ +

iv) states that source code for the Program is available + from such Contributor, and informs licensees how to obtain it in a + reasonable manner on or through a medium customarily used for software + exchange.

+ +

When the Program is made available in source code form:

+ +

a) it must be made available under this Agreement; and

+ +

b) a copy of this Agreement must be included with each + copy of the Program.

+ +

Contributors may not remove or alter any copyright notices contained + within the Program.

+ +

Each Contributor must identify itself as the originator of its + Contribution, if any, in a manner that reasonably allows subsequent + Recipients to identify the originator of the Contribution.

+ +

4. COMMERCIAL DISTRIBUTION

+ +

Commercial distributors of software may accept certain + responsibilities with respect to end users, business partners and the + like. While this license is intended to facilitate the commercial use of + the Program, the Contributor who includes the Program in a commercial + product offering should do so in a manner which does not create + potential liability for other Contributors. Therefore, if a Contributor + includes the Program in a commercial product offering, such Contributor + ("Commercial Contributor") hereby agrees to defend and + indemnify every other Contributor ("Indemnified Contributor") + against any losses, damages and costs (collectively "Losses") + arising from claims, lawsuits and other legal actions brought by a third + party against the Indemnified Contributor to the extent caused by the + acts or omissions of such Commercial Contributor in connection with its + distribution of the Program in a commercial product offering. The + obligations in this section do not apply to any claims or Losses + relating to any actual or alleged intellectual property infringement. In + order to qualify, an Indemnified Contributor must: a) promptly notify + the Commercial Contributor in writing of such claim, and b) allow the + Commercial Contributor to control, and cooperate with the Commercial + Contributor in, the defense and any related settlement negotiations. The + Indemnified Contributor may participate in any such claim at its own + expense.

+ +

For example, a Contributor might include the Program in a commercial + product offering, Product X. That Contributor is then a Commercial + Contributor. If that Commercial Contributor then makes performance + claims, or offers warranties related to Product X, those performance + claims and warranties are such Commercial Contributor's responsibility + alone. Under this section, the Commercial Contributor would have to + defend claims against the other Contributors related to those + performance claims and warranties, and if a court requires any other + Contributor to pay any damages as a result, the Commercial Contributor + must pay those damages.

+ +

5. NO WARRANTY

+ +

EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, THE PROGRAM IS + PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS + OF ANY KIND, EITHER EXPRESS OR IMPLIED INCLUDING, WITHOUT LIMITATION, + ANY WARRANTIES OR CONDITIONS OF TITLE, NON-INFRINGEMENT, MERCHANTABILITY + OR FITNESS FOR A PARTICULAR PURPOSE. Each Recipient is solely + responsible for determining the appropriateness of using and + distributing the Program and assumes all risks associated with its + exercise of rights under this Agreement , including but not limited to + the risks and costs of program errors, compliance with applicable laws, + damage to or loss of data, programs or equipment, and unavailability or + interruption of operations.

+ +

6. DISCLAIMER OF LIABILITY

+ +

EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, NEITHER RECIPIENT + NOR ANY CONTRIBUTORS SHALL HAVE ANY LIABILITY FOR ANY DIRECT, INDIRECT, + INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING + WITHOUT LIMITATION LOST PROFITS), HOWEVER CAUSED AND ON ANY THEORY OF + LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OR + DISTRIBUTION OF THE PROGRAM OR THE EXERCISE OF ANY RIGHTS GRANTED + HEREUNDER, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGES.

+ +

7. GENERAL

+ +

If any provision of this Agreement is invalid or unenforceable under + applicable law, it shall not affect the validity or enforceability of + the remainder of the terms of this Agreement, and without further action + by the parties hereto, such provision shall be reformed to the minimum + extent necessary to make such provision valid and enforceable.

+ +

If Recipient institutes patent litigation against any entity + (including a cross-claim or counterclaim in a lawsuit) alleging that the + Program itself (excluding combinations of the Program with other + software or hardware) infringes such Recipient's patent(s), then such + Recipient's rights granted under Section 2(b) shall terminate as of the + date such litigation is filed.

+ +

All Recipient's rights under this Agreement shall terminate if it + fails to comply with any of the material terms or conditions of this + Agreement and does not cure such failure in a reasonable period of time + after becoming aware of such noncompliance. If all Recipient's rights + under this Agreement terminate, Recipient agrees to cease use and + distribution of the Program as soon as reasonably practicable. However, + Recipient's obligations under this Agreement and any licenses granted by + Recipient relating to the Program shall continue and survive.

+ +

Everyone is permitted to copy and distribute copies of this + Agreement, but in order to avoid inconsistency the Agreement is + copyrighted and may only be modified in the following manner. The + Agreement Steward reserves the right to publish new versions (including + revisions) of this Agreement from time to time. No one other than the + Agreement Steward has the right to modify this Agreement. The Eclipse + Foundation is the initial Agreement Steward. The Eclipse Foundation may + assign the responsibility to serve as the Agreement Steward to a + suitable separate entity. Each new version of the Agreement will be + given a distinguishing version number. The Program (including + Contributions) may always be distributed subject to the version of the + Agreement under which it was received. In addition, after a new version + of the Agreement is published, Contributor may elect to distribute the + Program (including its Contributions) under the new version. Except as + expressly stated in Sections 2(a) and 2(b) above, Recipient receives no + rights or licenses to the intellectual property of any Contributor under + this Agreement, whether expressly, by implication, estoppel or + otherwise. All rights in the Program not expressly granted under this + Agreement are reserved.

+ +

This Agreement is governed by the laws of the State of New York and + the intellectual property laws of the United States of America. No party + to this Agreement will bring a legal action under this Agreement more + than one year after the cause of action arose. Each party waives its + rights to a jury trial in any resulting litigation.

+ + + + \ No newline at end of file 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/newpipe/App.java b/app/src/main/java/org/schabi/newpipe/App.java index deb0cf1ee..a91098a4e 100644 --- a/app/src/main/java/org/schabi/newpipe/App.java +++ b/app/src/main/java/org/schabi/newpipe/App.java @@ -70,6 +70,7 @@ public class App extends MultiDexApplication { 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; @@ -78,7 +79,6 @@ public static App getApp() { @Override protected void attachBaseContext(final Context base) { super.attachBaseContext(base); - initACRA(); } @@ -107,7 +107,7 @@ public void onCreate() { configureRxJavaErrorHandler(); // Check for new version - new CheckForNewAppVersionTask().execute(); + //new CheckForNewAppVersionTask().execute(); } protected Downloader getDownloader() { @@ -120,7 +120,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()); } @@ -201,10 +201,17 @@ 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() { + if (ACRA.isACRASenderServiceProcess()) { + return; + } + try { final CoreConfiguration acraConfig = new CoreConfigurationBuilder(this) - .setReportSenderFactoryClasses(REPORT_SENDER_FACTORY_CLASSES) .setBuildConfigClass(BuildConfig.class) .build(); ACRA.init(this, acraConfig); @@ -220,7 +227,7 @@ private void initACRA() { } public void initNotificationChannel() { - if (Build.VERSION.SDK_INT < android.os.Build.VERSION_CODES.O) { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) { return; } diff --git a/app/src/main/java/org/schabi/newpipe/CheckForNewAppVersionTask.java b/app/src/main/java/org/schabi/newpipe/CheckForNewAppVersionTask.java index 6878a360a..01f7ab240 100644 --- a/app/src/main/java/org/schabi/newpipe/CheckForNewAppVersionTask.java +++ b/app/src/main/java/org/schabi/newpipe/CheckForNewAppVersionTask.java @@ -11,11 +11,11 @@ import android.net.ConnectivityManager; import android.net.Uri; import android.os.AsyncTask; -import android.preference.PreferenceManager; import android.util.Log; import androidx.core.app.NotificationCompat; import androidx.core.app.NotificationManagerCompat; +import androidx.preference.PreferenceManager; import com.grack.nanojson.JsonObject; import com.grack.nanojson.JsonParser; diff --git a/app/src/main/java/org/schabi/newpipe/DownloaderImpl.java b/app/src/main/java/org/schabi/newpipe/DownloaderImpl.java index 95d3c2b7c..442488dc4 100644 --- a/app/src/main/java/org/schabi/newpipe/DownloaderImpl.java +++ b/app/src/main/java/org/schabi/newpipe/DownloaderImpl.java @@ -2,10 +2,10 @@ import android.content.Context; import android.os.Build; -import android.preference.PreferenceManager; import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import androidx.preference.PreferenceManager; import org.schabi.newpipe.extractor.downloader.Downloader; import org.schabi.newpipe.extractor.downloader.Request; @@ -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:128.0) Gecko/20100101 Firefox/128.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/newpipe/ExitActivity.java b/app/src/main/java/org/schabi/newpipe/ExitActivity.java index 94eff9560..65e180434 100644 --- a/app/src/main/java/org/schabi/newpipe/ExitActivity.java +++ b/app/src/main/java/org/schabi/newpipe/ExitActivity.java @@ -42,7 +42,7 @@ public static void exitAndRemoveFromRecentApps(final Activity activity) { protected void onCreate(final Bundle savedInstanceState) { super.onCreate(savedInstanceState); - if (Build.VERSION.SDK_INT >= 21) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { finishAndRemoveTask(); } else { finish(); diff --git a/app/src/main/java/org/schabi/newpipe/ImageDownloader.java b/app/src/main/java/org/schabi/newpipe/ImageDownloader.java index ca61c9655..c2897cff1 100644 --- a/app/src/main/java/org/schabi/newpipe/ImageDownloader.java +++ b/app/src/main/java/org/schabi/newpipe/ImageDownloader.java @@ -4,7 +4,7 @@ import android.content.Context; import android.content.SharedPreferences; import android.content.res.Resources; -import android.preference.PreferenceManager; +import androidx.preference.PreferenceManager; import com.nostra13.universalimageloader.core.download.BaseImageDownloader; diff --git a/app/src/main/java/org/schabi/newpipe/MainActivity.java b/app/src/main/java/org/schabi/newpipe/MainActivity.java index 37d6d62f5..c801e63e6 100644 --- a/app/src/main/java/org/schabi/newpipe/MainActivity.java +++ b/app/src/main/java/org/schabi/newpipe/MainActivity.java @@ -27,7 +27,6 @@ import android.os.Bundle; import android.os.Handler; import android.os.Looper; -import android.preference.PreferenceManager; import android.util.Log; import android.view.LayoutInflater; import android.view.Menu; @@ -46,10 +45,12 @@ import androidx.appcompat.app.ActionBarDrawerToggle; import androidx.appcompat.app.AppCompatActivity; import androidx.appcompat.widget.Toolbar; +import androidx.core.app.ActivityCompat; import androidx.core.view.GravityCompat; import androidx.drawerlayout.widget.DrawerLayout; import androidx.fragment.app.Fragment; import androidx.fragment.app.FragmentManager; +import androidx.preference.PreferenceManager; import com.google.android.material.navigation.NavigationView; @@ -99,6 +100,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; @@ -115,10 +117,11 @@ protected void onCreate(final Bundle savedInstanceState) { + "savedInstanceState = [" + savedInstanceState + "]"); } - // enable TLS1.1/1.2 for kitkat devices, to fix download and play for mediaCCC sources + // enable TLS1.1/1.2 for kitkat devices, to fix download and play for media.ccc.de sources if (Build.VERSION.SDK_INT == Build.VERSION_CODES.KITKAT) { TLSSocketFactoryCompat.setAsDefault(); } + ThemeHelper.setTheme(this, ServiceHelper.getSelectedServiceId(this)); assureCorrectAppLanguage(this); @@ -163,7 +166,7 @@ private void setupDrawer() throws Exception { drawerItems.getMenu() .add(R.id.menu_tabs_group, ITEM_ID_SUBSCRIPTIONS, ORDER, - R.string.tab_subscriptions) + R.string.tab_subscriptions) .setIcon(ThemeHelper.resolveResourceIdFromAttr(this, R.attr.ic_channel)); drawerItems.getMenu() .add(R.id.menu_tabs_group, ITEM_ID_FEED, ORDER, R.string.fragment_feed_title) @@ -177,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() @@ -204,7 +210,7 @@ public void onDrawerClosed(final View drawerView) { toggleServices(); } if (lastService != ServiceHelper.getSelectedServiceId(MainActivity.this)) { - new Handler(Looper.getMainLooper()).post(MainActivity.this::recreate); + ActivityCompat.recreate(MainActivity.this); } } }); @@ -261,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); @@ -479,10 +488,7 @@ protected void onResume() { Log.d(TAG, "Theme has changed, recreating activity..."); } sharedPreferences.edit().putBoolean(Constants.KEY_THEME_CHANGE, false).apply(); - // https://stackoverflow.com/questions/10844112/ - // Briefly, let the activity resume - // properly posting the recreate call to end of the message queue - new Handler(Looper.getMainLooper()).post(MainActivity.this::recreate); + ActivityCompat.recreate(this); } if (sharedPreferences.getBoolean(Constants.KEY_MAIN_PAGE_CHANGE, false)) { diff --git a/app/src/main/java/org/schabi/newpipe/ReCaptchaActivity.java b/app/src/main/java/org/schabi/newpipe/ReCaptchaActivity.java index 40ea4fd58..24ea1f8f7 100644 --- a/app/src/main/java/org/schabi/newpipe/ReCaptchaActivity.java +++ b/app/src/main/java/org/schabi/newpipe/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,11 +74,7 @@ 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); @@ -78,6 +84,7 @@ protected void onCreate(final Bundle savedInstanceState) { // 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 +122,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/newpipe/RouterActivity.java b/app/src/main/java/org/schabi/newpipe/RouterActivity.java index 39f6b217d..9343c576d 100644 --- a/app/src/main/java/org/schabi/newpipe/RouterActivity.java +++ b/app/src/main/java/org/schabi/newpipe/RouterActivity.java @@ -8,7 +8,6 @@ import android.content.SharedPreferences; import android.content.pm.PackageManager; import android.os.Bundle; -import android.preference.PreferenceManager; import android.text.TextUtils; import android.view.ContextThemeWrapper; import android.view.LayoutInflater; @@ -28,6 +27,7 @@ import androidx.appcompat.content.res.AppCompatResources; import androidx.core.app.NotificationCompat; import androidx.fragment.app.FragmentManager; +import androidx.preference.PreferenceManager; import org.schabi.newpipe.download.DownloadDialog; import org.schabi.newpipe.extractor.Info; @@ -50,6 +50,7 @@ import org.schabi.newpipe.util.ListHelper; import org.schabi.newpipe.util.NavigationHelper; import org.schabi.newpipe.util.PermissionHelper; +import org.schabi.newpipe.util.ShareUtils; import org.schabi.newpipe.util.ThemeHelper; import org.schabi.newpipe.util.urlfinder.UrlFinder; import org.schabi.newpipe.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/newpipe/about/AboutActivity.java b/app/src/main/java/org/schabi/newpipe/about/AboutActivity.java index b5be2dde6..08cf96d85 100644 --- a/app/src/main/java/org/schabi/newpipe/about/AboutActivity.java +++ b/app/src/main/java/org/schabi/newpipe/about/AboutActivity.java @@ -30,38 +30,47 @@ public class AboutActivity extends AppCompatActivity { /** * List of all software components. */ - 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), - new SoftwareComponent("NewPipe Extractor", "2017 - 2020", "Christian Schabesberger", - "https://github.com/TeamNewPipe/NewPipeExtractor", StandardLicenses.GPL3), - new SoftwareComponent("Jsoup", "2017", "Jonathan Hedley", - "https://github.com/jhy/jsoup", StandardLicenses.MIT), - new SoftwareComponent("Rhino", "2015", "Mozilla", - "https://www.mozilla.org/rhino/", StandardLicenses.MPL2), + private static final SoftwareComponent[] SOFTWARE_COMPONENTS = { new SoftwareComponent("ACRA", "2013", "Kevin Gaudin", - "http://www.acra.ch", StandardLicenses.APACHE2), - new SoftwareComponent("Universal Image Loader", "2011 - 2015", "Sergey Tarasevich", - "https://github.com/nostra13/Android-Universal-Image-Loader", - StandardLicenses.APACHE2), + "https://github.com/ACRA/acra", StandardLicenses.APACHE2), + new SoftwareComponent("AndroidX", "2005 - 2011", "The Android Open Source Project", + "https://developer.android.com/jetpack", StandardLicenses.APACHE2), new SoftwareComponent("CircleImageView", "2014 - 2020", "Henning Dodenhof", - "https://github.com/hdodenhof/CircleImageView", StandardLicenses.APACHE2), - new SoftwareComponent("NoNonsense-FilePicker", "2016", "Jonas Kalderstam", - "https://github.com/spacecowboy/NoNonsense-FilePicker", StandardLicenses.MPL2), - new SoftwareComponent("ExoPlayer", "2014 - 2020", "Google Inc", + "https://github.com/hdodenhof/CircleImageView", + StandardLicenses.APACHE2), + new SoftwareComponent("ExoPlayer", "2014 - 2020", "Google, Inc.", "https://github.com/google/ExoPlayer", StandardLicenses.APACHE2), - new SoftwareComponent("RxAndroid", "2015 - 2018", "The RxAndroid authors", + new SoftwareComponent("GigaGet", "2014 - 2015", "Peter Cai", + "https://github.com/PaperAirplane-Dev-Team/GigaGet", StandardLicenses.GPL3), + new SoftwareComponent("Groupie", "2016", "Lisa Wray", + "https://github.com/lisawray/groupie", StandardLicenses.MIT), + new SoftwareComponent("Icepick", "2015", "Frankie Sardo", + "https://github.com/frankiesardo/icepick", StandardLicenses.EPL1), + new SoftwareComponent("Jsoup", "2009 - 2020", "Jonathan Hedley", + "https://github.com/jhy/jsoup", StandardLicenses.MIT), + new SoftwareComponent("Markwon", "2019", "Dimitry Ivanov", + "https://github.com/noties/Markwon", StandardLicenses.APACHE2), + new SoftwareComponent("Material Components for Android", "2016 - 2020", "Google, Inc.", + "https://github.com/material-components/material-components-android", + StandardLicenses.APACHE2), + new SoftwareComponent("NewPipe Extractor", "2017 - 2020", "Christian Schabesberger", + "https://github.com/TeamNewPipe/NewPipeExtractor", StandardLicenses.GPL3), + new SoftwareComponent("NoNonsense-FilePicker", "2016", "Jonas Kalderstam", + "https://github.com/spacecowboy/NoNonsense-FilePicker", + StandardLicenses.MPL2), + new SoftwareComponent("OkHttp", "2019", "Square, Inc.", + "https://square.github.io/okhttp/", StandardLicenses.APACHE2), + new SoftwareComponent("PrettyTime", "2012 - 2020", "Lincoln Baxter, III", + "https://github.com/ocpsoft/prettytime", StandardLicenses.APACHE2), + new SoftwareComponent("RxAndroid", "2015", "The RxAndroid authors", "https://github.com/ReactiveX/RxAndroid", StandardLicenses.APACHE2), + new SoftwareComponent("RxBinding", "2015", "Jake Wharton", + "https://github.com/JakeWharton/RxBinding", StandardLicenses.APACHE2), new SoftwareComponent("RxJava", "2016 - 2020", "RxJava Contributors", "https://github.com/ReactiveX/RxJava", StandardLicenses.APACHE2), - new SoftwareComponent("RxBinding", "2015 - 2018", "Jake Wharton", - "https://github.com/JakeWharton/RxBinding", StandardLicenses.APACHE2), - new SoftwareComponent("PrettyTime", "2012 - 2020", "Lincoln Baxter, III", - "https://github.com/ocpsoft/prettytime", StandardLicenses.APACHE2), - new SoftwareComponent("Markwon", "2017 - 2020", "Noties", - "https://github.com/noties/Markwon", StandardLicenses.APACHE2), - new SoftwareComponent("Groupie", "2016", "Lisa Wray", - "https://github.com/lisawray/groupie", StandardLicenses.MIT) + new SoftwareComponent("Universal Image Loader", "2011 - 2015", "Sergey Tarasevich", + "https://github.com/nostra13/Android-Universal-Image-Loader", + StandardLicenses.APACHE2), }; /** @@ -103,6 +112,7 @@ protected void onCreate(final Bundle savedInstanceState) { tabLayout.setupWithViewPager(mViewPager); } + @Override public boolean onOptionsItemSelected(final MenuItem item) { int id = item.getItemId(); @@ -120,7 +130,8 @@ public boolean onOptionsItemSelected(final MenuItem item) { * A placeholder fragment containing a simple view. */ public static class AboutFragment extends Fragment { - public AboutFragment() { } + public AboutFragment() { + } /** * Created a new instance of this fragment for the given section number. diff --git a/app/src/main/java/org/schabi/newpipe/about/StandardLicenses.java b/app/src/main/java/org/schabi/newpipe/about/StandardLicenses.java index 75a7a8613..60b1e168c 100644 --- a/app/src/main/java/org/schabi/newpipe/about/StandardLicenses.java +++ b/app/src/main/java/org/schabi/newpipe/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 @@ -14,6 +12,8 @@ public final class StandardLicenses { = new License("Mozilla Public License, Version 2.0", "MPL 2.0", "mpl2.html"); public static final License MIT = new License("MIT License", "MIT", "mit.html"); + public static final License EPL1 + = new License("Eclipse Public License, Version 1.0", "EPL 1.0", "epl1.html"); private StandardLicenses() { } } diff --git a/app/src/main/java/org/schabi/newpipe/database/feed/dao/FeedDAO.kt b/app/src/main/java/org/schabi/newpipe/database/feed/dao/FeedDAO.kt index 74f5b369e..d443770cc 100644 --- a/app/src/main/java/org/schabi/newpipe/database/feed/dao/FeedDAO.kt +++ b/app/src/main/java/org/schabi/newpipe/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.newpipe.database.feed.model.FeedEntity import org.schabi.newpipe.database.feed.model.FeedLastUpdatedEntity import org.schabi.newpipe.database.stream.model.StreamEntity import org.schabi.newpipe.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/newpipe/database/feed/model/FeedEntity.kt b/app/src/main/java/org/schabi/newpipe/database/feed/model/FeedEntity.kt index 8a1eb65d4..beeedc62b 100644 --- a/app/src/main/java/org/schabi/newpipe/database/feed/model/FeedEntity.kt +++ b/app/src/main/java/org/schabi/newpipe/database/feed/model/FeedEntity.kt @@ -10,21 +10,24 @@ import org.schabi.newpipe.database.feed.model.FeedEntity.Companion.SUBSCRIPTION_ import org.schabi.newpipe.database.stream.model.StreamEntity import org.schabi.newpipe.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/newpipe/database/feed/model/FeedGroupEntity.kt b/app/src/main/java/org/schabi/newpipe/database/feed/model/FeedGroupEntity.kt index e772168fd..1dd26946a 100644 --- a/app/src/main/java/org/schabi/newpipe/database/feed/model/FeedGroupEntity.kt +++ b/app/src/main/java/org/schabi/newpipe/database/feed/model/FeedGroupEntity.kt @@ -9,8 +9,8 @@ import org.schabi.newpipe.database.feed.model.FeedGroupEntity.Companion.SORT_ORD import org.schabi.newpipe.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/newpipe/database/feed/model/FeedGroupSubscriptionEntity.kt b/app/src/main/java/org/schabi/newpipe/database/feed/model/FeedGroupSubscriptionEntity.kt index eac6bddee..40f7d203b 100644 --- a/app/src/main/java/org/schabi/newpipe/database/feed/model/FeedGroupSubscriptionEntity.kt +++ b/app/src/main/java/org/schabi/newpipe/database/feed/model/FeedGroupSubscriptionEntity.kt @@ -11,22 +11,24 @@ import org.schabi.newpipe.database.feed.model.FeedGroupSubscriptionEntity.Compan import org.schabi.newpipe.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/newpipe/database/feed/model/FeedLastUpdatedEntity.kt b/app/src/main/java/org/schabi/newpipe/database/feed/model/FeedLastUpdatedEntity.kt index 78b2550a5..78adf5f96 100644 --- a/app/src/main/java/org/schabi/newpipe/database/feed/model/FeedLastUpdatedEntity.kt +++ b/app/src/main/java/org/schabi/newpipe/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.newpipe.database.feed.model.FeedLastUpdatedEntity.Companion.FEED_LAST_UPDATED_TABLE import org.schabi.newpipe.database.feed.model.FeedLastUpdatedEntity.Companion.SUBSCRIPTION_ID import org.schabi.newpipe.database.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/newpipe/database/history/dao/StreamHistoryDAO.java b/app/src/main/java/org/schabi/newpipe/database/history/dao/StreamHistoryDAO.java index c716a2d91..8a9cab182 100644 --- a/app/src/main/java/org/schabi/newpipe/database/history/dao/StreamHistoryDAO.java +++ b/app/src/main/java/org/schabi/newpipe/database/history/dao/StreamHistoryDAO.java @@ -20,6 +20,9 @@ import static org.schabi.newpipe.database.stream.StreamStatisticsEntry.STREAM_WATCH_COUNT; import static org.schabi.newpipe.database.stream.model.StreamEntity.STREAM_ID; import static org.schabi.newpipe.database.stream.model.StreamEntity.STREAM_TABLE; +import static org.schabi.newpipe.database.stream.model.StreamStateEntity.JOIN_STREAM_ID_ALIAS; +import static org.schabi.newpipe.database.stream.model.StreamStateEntity.STREAM_PROGRESS_TIME; +import static org.schabi.newpipe.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/newpipe/database/history/model/StreamHistoryEntry.kt b/app/src/main/java/org/schabi/newpipe/database/history/model/StreamHistoryEntry.kt index c653e6c6f..e52046552 100644 --- a/app/src/main/java/org/schabi/newpipe/database/history/model/StreamHistoryEntry.kt +++ b/app/src/main/java/org/schabi/newpipe/database/history/model/StreamHistoryEntry.kt @@ -2,8 +2,8 @@ package org.schabi.newpipe.database.history.model import androidx.room.ColumnInfo import androidx.room.Embedded -import java.util.Date import org.schabi.newpipe.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/newpipe/database/playlist/PlaylistStreamEntry.kt b/app/src/main/java/org/schabi/newpipe/database/playlist/PlaylistStreamEntry.kt index c349a3761..aff6205f2 100644 --- a/app/src/main/java/org/schabi/newpipe/database/playlist/PlaylistStreamEntry.kt +++ b/app/src/main/java/org/schabi/newpipe/database/playlist/PlaylistStreamEntry.kt @@ -5,12 +5,17 @@ import androidx.room.Embedded import org.schabi.newpipe.database.LocalItem import org.schabi.newpipe.database.playlist.model.PlaylistStreamEntity import org.schabi.newpipe.database.stream.model.StreamEntity +import org.schabi.newpipe.database.stream.model.StreamStateEntity import org.schabi.newpipe.extractor.stream.StreamInfoItem +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/newpipe/database/playlist/dao/PlaylistStreamDAO.java b/app/src/main/java/org/schabi/newpipe/database/playlist/dao/PlaylistStreamDAO.java index 2c0f7e506..a41f36d72 100644 --- a/app/src/main/java/org/schabi/newpipe/database/playlist/dao/PlaylistStreamDAO.java +++ b/app/src/main/java/org/schabi/newpipe/database/playlist/dao/PlaylistStreamDAO.java @@ -24,6 +24,9 @@ import static org.schabi.newpipe.database.playlist.model.PlaylistStreamEntity.PLAYLIST_STREAM_JOIN_TABLE; import static org.schabi.newpipe.database.stream.model.StreamEntity.STREAM_ID; import static org.schabi.newpipe.database.stream.model.StreamEntity.STREAM_TABLE; +import static org.schabi.newpipe.database.stream.model.StreamStateEntity.JOIN_STREAM_ID_ALIAS; +import static org.schabi.newpipe.database.stream.model.StreamStateEntity.STREAM_PROGRESS_TIME; +import static org.schabi.newpipe.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/newpipe/database/stream/StreamStatisticsEntry.kt b/app/src/main/java/org/schabi/newpipe/database/stream/StreamStatisticsEntry.kt index dde1f0392..b4f9a3f3a 100644 --- a/app/src/main/java/org/schabi/newpipe/database/stream/StreamStatisticsEntry.kt +++ b/app/src/main/java/org/schabi/newpipe/database/stream/StreamStatisticsEntry.kt @@ -2,16 +2,20 @@ package org.schabi.newpipe.database.stream import androidx.room.ColumnInfo import androidx.room.Embedded -import java.util.Date import org.schabi.newpipe.database.LocalItem import org.schabi.newpipe.database.history.model.StreamHistoryEntity import org.schabi.newpipe.database.stream.model.StreamEntity +import org.schabi.newpipe.database.stream.model.StreamStateEntity.STREAM_PROGRESS_TIME import org.schabi.newpipe.extractor.stream.StreamInfoItem +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/newpipe/database/stream/dao/StreamDAO.kt b/app/src/main/java/org/schabi/newpipe/database/stream/dao/StreamDAO.kt index 921c08b46..d254b34ff 100644 --- a/app/src/main/java/org/schabi/newpipe/database/stream/dao/StreamDAO.kt +++ b/app/src/main/java/org/schabi/newpipe/database/stream/dao/StreamDAO.kt @@ -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.database.BasicDAO import org.schabi.newpipe.database.stream.model.StreamEntity import org.schabi.newpipe.database.stream.model.StreamEntity.Companion.STREAM_ID import org.schabi.newpipe.extractor.stream.StreamType import org.schabi.newpipe.extractor.stream.StreamType.AUDIO_LIVE_STREAM import org.schabi.newpipe.extractor.stream.StreamType.LIVE_STREAM +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/newpipe/database/stream/model/StreamEntity.kt b/app/src/main/java/org/schabi/newpipe/database/stream/model/StreamEntity.kt index d13f5cc2d..7d9b76e02 100644 --- a/app/src/main/java/org/schabi/newpipe/database/stream/model/StreamEntity.kt +++ b/app/src/main/java/org/schabi/newpipe/database/stream/model/StreamEntity.kt @@ -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.database.stream.model.StreamEntity.Companion.STREAM_SERVICE_ID import org.schabi.newpipe.database.stream.model.StreamEntity.Companion.STREAM_TABLE import org.schabi.newpipe.database.stream.model.StreamEntity.Companion.STREAM_URL @@ -16,11 +13,15 @@ import org.schabi.newpipe.extractor.stream.StreamInfo import org.schabi.newpipe.extractor.stream.StreamInfoItem import org.schabi.newpipe.extractor.stream.StreamType import org.schabi.newpipe.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) @@ -63,27 +64,27 @@ data class StreamEntity( @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 +96,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/newpipe/database/stream/model/StreamStateEntity.java b/app/src/main/java/org/schabi/newpipe/database/stream/model/StreamStateEntity.java index d275d9a71..1ce834a82 100644 --- a/app/src/main/java/org/schabi/newpipe/database/stream/model/StreamStateEntity.java +++ b/app/src/main/java/org/schabi/newpipe/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/newpipe/database/subscription/SubscriptionDAO.kt b/app/src/main/java/org/schabi/newpipe/database/subscription/SubscriptionDAO.kt index 60dd343b9..f0805bb49 100644 --- a/app/src/main/java/org/schabi/newpipe/database/subscription/SubscriptionDAO.kt +++ b/app/src/main/java/org/schabi/newpipe/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/newpipe/download/DownloadActivity.java b/app/src/main/java/org/schabi/newpipe/download/DownloadActivity.java index e46ded40d..f45da5d75 100644 --- a/app/src/main/java/org/schabi/newpipe/download/DownloadActivity.java +++ b/app/src/main/java/org/schabi/newpipe/download/DownloadActivity.java @@ -14,6 +14,7 @@ import org.schabi.newpipe.R; import org.schabi.newpipe.util.AndroidTvUtils; +import org.schabi.newpipe.util.NavigationHelper; import org.schabi.newpipe.util.ThemeHelper; import org.schabi.newpipe.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/newpipe/download/DownloadDialog.java b/app/src/main/java/org/schabi/newpipe/download/DownloadDialog.java index 9b083f1b3..2e93f000a 100644 --- a/app/src/main/java/org/schabi/newpipe/download/DownloadDialog.java +++ b/app/src/main/java/org/schabi/newpipe/download/DownloadDialog.java @@ -10,7 +10,6 @@ import android.os.Bundle; import android.os.Environment; import android.os.IBinder; -import android.preference.PreferenceManager; import android.util.Log; import android.util.SparseArray; import android.view.LayoutInflater; @@ -34,6 +33,7 @@ import androidx.appcompat.widget.Toolbar; import androidx.documentfile.provider.DocumentFile; import androidx.fragment.app.DialogFragment; +import androidx.preference.PreferenceManager; import com.nononsenseapps.filepicker.Utils; @@ -295,9 +295,9 @@ public void onViewCreated(@NonNull final View view, @Nullable final Bundle saved initToolbar(view.findViewById(R.id.toolbar)); setupDownloadOptions(); - prefs = PreferenceManager.getDefaultSharedPreferences(getContext()); + prefs = PreferenceManager.getDefaultSharedPreferences(requireContext()); - int threads = prefs.getInt(getString(R.string.default_download_threads), 3); + int threads = prefs.getInt(getString(R.string.default_download_threads), 5); threadsCountTextView.setText(String.valueOf(threads)); threadsSeekBar.setProgress(threads - 1); threadsSeekBar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() { @@ -516,7 +516,23 @@ protected void setupDownloadOptions() { videoButton.setVisibility(isVideoStreamsAvailable ? View.VISIBLE : View.GONE); subtitleButton.setVisibility(isSubtitleStreamsAvailable ? View.VISIBLE : View.GONE); - if (isVideoStreamsAvailable) { + prefs = PreferenceManager.getDefaultSharedPreferences(requireContext()); + final String defaultMedia = prefs.getString(getString(R.string.last_used_download_type), + getString(R.string.last_download_type_video_key)); + + if (isVideoStreamsAvailable + && (defaultMedia.equals(getString(R.string.last_download_type_video_key)))) { + videoButton.setChecked(true); + setupVideoSpinner(); + } else if (isAudioStreamsAvailable + && (defaultMedia.equals(getString(R.string.last_download_type_audio_key)))) { + audioButton.setChecked(true); + setupAudioSpinner(); + } else if (isSubtitleStreamsAvailable + && (defaultMedia.equals(getString(R.string.last_download_type_subtitle_key)))) { + subtitleButton.setChecked(true); + setupSubtitleSpinner(); + } else if (isVideoStreamsAvailable) { videoButton.setChecked(true); setupVideoSpinner(); } else if (isAudioStreamsAvailable) { @@ -592,9 +608,10 @@ private void showErrorActivity(final Exception e) { } private void prepareSelectedDownload() { - StoredDirectoryHelper mainStorage; - MediaFormat format; - String mime; + final StoredDirectoryHelper mainStorage; + final MediaFormat format; + final String mime; + final String selectedMediaType; // first, build the filename and get the output folder (if possible) // later, run a very very very large file checking logic @@ -603,6 +620,7 @@ private void prepareSelectedDownload() { switch (radioStreamsGroup.getCheckedRadioButtonId()) { case R.id.audio_button: + selectedMediaType = getString(R.string.last_download_type_audio_key); mainStorage = mainStorageAudio; format = audioStreamsAdapter.getItem(selectedAudioIndex).getFormat(); switch (format) { @@ -617,12 +635,14 @@ private void prepareSelectedDownload() { } break; case R.id.video_button: + selectedMediaType = getString(R.string.last_download_type_video_key); mainStorage = mainStorageVideo; format = videoStreamsAdapter.getItem(selectedVideoIndex).getFormat(); mime = format.mimeType; filename += format.suffix; break; case R.id.subtitle_button: + selectedMediaType = getString(R.string.last_download_type_subtitle_key); mainStorage = mainStorageVideo; // subtitle & video files go together format = subtitleStreamsAdapter.getItem(selectedSubtitleIndex).getFormat(); mime = format.mimeType; @@ -664,6 +684,11 @@ private void prepareSelectedDownload() { // check for existing file with the same name checkSelectedDownload(mainStorage, mainStorage.findFile(filename), filename, mime); + + // remember the last media type downloaded by the user + prefs.edit() + .putString(getString(R.string.last_used_download_type), selectedMediaType) + .apply(); } private void checkSelectedDownload(final StoredDirectoryHelper mainStorage, @@ -749,7 +774,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/newpipe/fragments/BaseStateFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/BaseStateFragment.java index aec404825..74216639c 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/BaseStateFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/BaseStateFragment.java @@ -19,9 +19,15 @@ import org.schabi.newpipe.MainActivity; import org.schabi.newpipe.R; import org.schabi.newpipe.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.newpipe.report.ErrorActivity; import org.schabi.newpipe.report.ErrorInfo; import org.schabi.newpipe.report.UserAction; @@ -211,12 +217,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/newpipe/fragments/MainFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/MainFragment.java index 558440f9e..583eecab8 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/MainFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/MainFragment.java @@ -3,7 +3,6 @@ import android.content.Context; import android.content.res.ColorStateList; import android.os.Bundle; -import android.preference.PreferenceManager; import android.util.Log; import android.view.LayoutInflater; import android.view.Menu; @@ -19,6 +18,7 @@ import androidx.fragment.app.Fragment; import androidx.fragment.app.FragmentManager; import androidx.fragment.app.FragmentStatePagerAdapterMenuWorkaround; +import androidx.preference.PreferenceManager; import androidx.viewpager.widget.ViewPager; import com.google.android.material.tabs.TabLayout; @@ -75,7 +75,7 @@ public void onCreate(final Bundle savedInstanceState) { youtubeRestrictedModeEnabledKey = getString(R.string.youtube_restricted_mode_enabled); previousYoutubeRestrictedModeEnabled = - PreferenceManager.getDefaultSharedPreferences(getContext()) + PreferenceManager.getDefaultSharedPreferences(requireContext()) .getBoolean(youtubeRestrictedModeEnabledKey, false); } @@ -106,7 +106,7 @@ public void onResume() { super.onResume(); boolean youtubeRestrictedModeEnabled = - PreferenceManager.getDefaultSharedPreferences(getContext()) + PreferenceManager.getDefaultSharedPreferences(requireContext()) .getBoolean(youtubeRestrictedModeEnabledKey, false); if (previousYoutubeRestrictedModeEnabled != youtubeRestrictedModeEnabled) { previousYoutubeRestrictedModeEnabled = youtubeRestrictedModeEnabled; @@ -149,10 +149,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/newpipe/fragments/detail/TabAdaptor.java b/app/src/main/java/org/schabi/newpipe/fragments/detail/TabAdapter.java similarity index 95% rename from app/src/main/java/org/schabi/newpipe/fragments/detail/TabAdaptor.java rename to app/src/main/java/org/schabi/newpipe/fragments/detail/TabAdapter.java index 38f013200..91900df4c 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/detail/TabAdaptor.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/detail/TabAdapter.java @@ -10,12 +10,12 @@ import java.util.ArrayList; import java.util.List; -public class TabAdaptor extends FragmentPagerAdapter { +public class TabAdapter extends FragmentPagerAdapter { private final List mFragmentList = new ArrayList<>(); private final List mFragmentTitleList = new ArrayList<>(); private final FragmentManager fragmentManager; - public TabAdaptor(final FragmentManager fm) { + public TabAdapter(final FragmentManager fm) { super(fm); this.fragmentManager = fm; } diff --git a/app/src/main/java/org/schabi/newpipe/fragments/detail/VideoDetailFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/detail/VideoDetailFragment.java index f883ff916..9edf3acb4 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/detail/VideoDetailFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/detail/VideoDetailFragment.java @@ -8,8 +8,8 @@ import android.net.Uri; import android.os.Build; import android.os.Bundle; -import android.preference.PreferenceManager; -import android.text.Html; +import androidx.preference.PreferenceManager; +import androidx.core.text.HtmlCompat; import android.text.Spanned; import android.text.TextUtils; import android.text.util.Linkify; @@ -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; @@ -191,7 +192,7 @@ public class VideoDetailFragment extends BaseStateFragment private AppBarLayout appBarLayout; private ViewPager viewPager; - private TabAdaptor pageAdapter; + private TabAdapter pageAdapter; private TabLayout tabLayout; private FrameLayout relatedStreamsLayout; @@ -252,7 +253,7 @@ public void onPause() { if (currentWorker != null) { currentWorker.dispose(); } - PreferenceManager.getDefaultSharedPreferences(getContext()) + PreferenceManager.getDefaultSharedPreferences(requireContext()) .edit() .putString(getString(R.string.stream_info_selected_tab_key), pageAdapter.getItemTitle(viewPager.getCurrentItem())) @@ -329,7 +330,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"); } @@ -410,9 +411,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: @@ -451,11 +452,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); } @@ -468,6 +466,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; @@ -486,7 +487,7 @@ public boolean onLongClick(final View v) { } break; case R.id.detail_title_root_layout: - ShareUtils.copyToClipboard(getContext(), videoTitleTextView.getText().toString()); + ShareUtils.copyToClipboard(requireContext(), videoTitleTextView.getText().toString()); break; } @@ -557,7 +558,7 @@ protected void initViews(final View rootView, final Bundle savedInstanceState) { appBarLayout = rootView.findViewById(R.id.appbarlayout); viewPager = rootView.findViewById(R.id.viewpager); - pageAdapter = new TabAdaptor(getChildFragmentManager()); + pageAdapter = new TabAdapter(getChildFragmentManager()); viewPager.setAdapter(pageAdapter); tabLayout = rootView.findViewById(R.id.tablayout); tabLayout.setupWithViewPager(viewPager); @@ -568,7 +569,7 @@ protected void initViews(final View rootView, final Bundle savedInstanceState) { thumbnailBackgroundButton.requestFocus(); - if (AndroidTvUtils.isTv(getContext())) { + if (AndroidTvUtils.isTv(requireContext())) { // remove ripple effects from detail controls final int transparent = getResources().getColor(R.color.transparent_background_color); detailControlsAddToPlaylist.setBackgroundColor(transparent); @@ -591,6 +592,7 @@ protected void initListeners() { detailControlsBackground.setOnClickListener(this); detailControlsPopup.setOnClickListener(this); detailControlsAddToPlaylist.setOnClickListener(this); + detailControlsAddToPlaylist.setOnLongClickListener(this); detailControlsDownload.setOnClickListener(this); detailControlsDownload.setOnLongClickListener(this); @@ -695,6 +697,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()); @@ -928,10 +942,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) { @@ -1015,6 +1029,20 @@ 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.EMPTY_DESCRIPTION) { @@ -1023,24 +1051,17 @@ private void prepareDescription(final Description description) { if (description.getType() == Description.HTML) { disposables.add(Single.just(description.getContent()) - .map((@NonNull String descriptionText) -> { - Spanned parsedDescription; - if (Build.VERSION.SDK_INT >= 24) { - parsedDescription = Html.fromHtml(descriptionText, 0); - } else { - //noinspection deprecation - parsedDescription = Html.fromHtml(descriptionText); - } - return parsedDescription; - }) + .map((@NonNull final String descriptionText) -> + HtmlCompat.fromHtml(descriptionText, + HtmlCompat.FROM_HTML_MODE_LEGACY)) .subscribeOn(Schedulers.computation()) .observeOn(AndroidSchedulers.mainThread()) - .subscribe((@NonNull Spanned spanned) -> { + .subscribe((@NonNull final Spanned spanned) -> { videoDescriptionView.setText(spanned); videoDescriptionView.setVisibility(View.VISIBLE); })); } else if (description.getType() == Description.MARKDOWN) { - final Markwon markwon = Markwon.builder(getContext()) + final Markwon markwon = Markwon.builder(requireContext()) .usePlugin(LinkifyPlugin.create()) .build(); markwon.setMarkdown(videoDescriptionView, description.getContent()); @@ -1066,6 +1087,11 @@ private void setHeightThumbnail() { private void showContent() { contentRootLayoutHiding.setVisibility(View.VISIBLE); + final boolean showDescriptionOnLoad = PreferenceManager.getDefaultSharedPreferences(activity) + .getBoolean(getString(R.string.always_expand_description_key), false); + if (showDescriptionOnLoad) { + toggleTitleAndDescription(); + } } protected void setInitialData(final int sid, final String u, final String title) { @@ -1261,33 +1287,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, - CompatibilityUtil.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, + CompatibilityUtil.getNameOfService(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 diff --git a/app/src/main/java/org/schabi/newpipe/fragments/list/BaseListFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/list/BaseListFragment.java index 9ce62a0df..14224310e 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/list/BaseListFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/list/BaseListFragment.java @@ -6,7 +6,6 @@ import android.content.res.Configuration; import android.content.res.Resources; import android.os.Bundle; -import android.preference.PreferenceManager; import android.util.Log; import android.view.Menu; import android.view.MenuInflater; @@ -15,6 +14,7 @@ import androidx.annotation.NonNull; import androidx.appcompat.app.ActionBar; import androidx.appcompat.app.AppCompatActivity; +import androidx.preference.PreferenceManager; import androidx.recyclerview.widget.GridLayoutManager; import androidx.recyclerview.widget.RecyclerView; diff --git a/app/src/main/java/org/schabi/newpipe/fragments/list/BaseListInfoFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/list/BaseListInfoFragment.java index 86b093e45..e934b11a5 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/list/BaseListInfoFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/list/BaseListInfoFragment.java @@ -208,6 +208,8 @@ public void handleResult(@NonNull final I result) { if (result.getRelatedItems().size() > 0) { infoListAdapter.addInfoItemList(result.getRelatedItems()); showListFooter(hasMoreItems()); + } else if (hasMoreItems()) { + loadMoreItems(); } else { infoListAdapter.clearStreamItemList(); showEmptyState(); diff --git a/app/src/main/java/org/schabi/newpipe/fragments/list/channel/ChannelFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/list/channel/ChannelFragment.java index 8db02c764..ce589a419 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/list/channel/ChannelFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/list/channel/ChannelFragment.java @@ -75,6 +75,8 @@ public class ChannelFragment extends BaseListInfoFragment private final CompositeDisposable disposables = new CompositeDisposable(); private Disposable subscribeButtonMonitor; + private boolean channelContentNotSupported; + /*////////////////////////////////////////////////////////////////////////// // Views //////////////////////////////////////////////////////////////////////////*/ @@ -137,6 +139,9 @@ public void onViewCreated(final View rootView, final Bundle savedInstanceState) contentNotSupportedTextView = rootView.findViewById(R.id.error_content_not_supported); kaomojiTextView = rootView.findViewById(R.id.channel_kaomoji); noVideosTextView = rootView.findViewById(R.id.channel_no_videos); + if (channelContentNotSupported) { + showContentNotSupported(); + } } @Override @@ -427,8 +432,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); @@ -476,8 +481,8 @@ public void handleResult(@NonNull final ChannelInfo result) { if (!TextUtils.isEmpty(currentInfo.getParentChannelName())) { headerSubChannelTitleView.setText(String.format( - getString(R.string.channel_created_by), - currentInfo.getParentChannelName()) + getString(R.string.channel_created_by), + currentInfo.getParentChannelName()) ); headerSubChannelTitleView.setVisibility(View.VISIBLE); headerSubChannelAvatarView.setVisibility(View.VISIBLE); @@ -491,6 +496,7 @@ public void handleResult(@NonNull final ChannelInfo result) { playlistCtrl.setVisibility(View.VISIBLE); + channelContentNotSupported = false; List errors = new ArrayList<>(result.getErrors()); if (!errors.isEmpty()) { @@ -499,7 +505,12 @@ public void handleResult(@NonNull final ChannelInfo result) { for (Iterator it = errors.iterator(); it.hasNext();) { Throwable throwable = it.next(); if (throwable instanceof ContentNotSupportedException) { - showContentNotSupported(); + /* + channelBinding might not be initialized when handleResult() is called + (e.g. after rotating the screen, https://github.com/TeamNewPipe/NewPipe/issues/6696) + showContentNotSupported() will be called later + */ + channelContentNotSupported = true; it.remove(); } } diff --git a/app/src/main/java/org/schabi/newpipe/fragments/list/comments/CommentsFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/list/comments/CommentsFragment.java index cc4691c11..9d9074018 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/list/comments/CommentsFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/list/comments/CommentsFragment.java @@ -7,6 +7,7 @@ import android.view.MenuInflater; import android.view.View; import android.view.ViewGroup; +import android.widget.TextView; import androidx.annotation.NonNull; import androidx.annotation.Nullable; @@ -29,6 +30,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(); @@ -36,6 +39,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 //////////////////////////////////////////////////////////////////////////*/ @@ -93,7 +103,12 @@ public void showLoading() { public void handleResult(@NonNull final CommentsInfo result) { super.handleResult(result); - AnimationUtils.slideUp(getView(), 120, 150, 0.06f); + emptyStateDesc.setText( + result.isCommentsDisabled() + ? R.string.comments_are_disabled + : R.string.no_comments); + + AnimationUtils.slideUp(requireView(), 120, 150, 0.06f); if (!result.getErrors().isEmpty()) { showSnackBarError(result.getErrors(), UserAction.REQUESTED_COMMENTS, diff --git a/app/src/main/java/org/schabi/newpipe/fragments/list/playlist/PlaylistFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/list/playlist/PlaylistFragment.java index bc81db767..95820ee48 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/list/playlist/PlaylistFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/list/playlist/PlaylistFragment.java @@ -11,12 +11,12 @@ import android.view.MenuItem; import android.view.View; import android.view.ViewGroup; -import android.widget.ImageView; import android.widget.TextView; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.appcompat.app.AppCompatActivity; +import androidx.appcompat.content.res.AppCompatResources; import org.reactivestreams.Subscriber; import org.reactivestreams.Subscription; @@ -26,8 +26,10 @@ import org.schabi.newpipe.extractor.InfoItem; import org.schabi.newpipe.extractor.ListExtractor; import org.schabi.newpipe.extractor.NewPipe; +import org.schabi.newpipe.extractor.ServiceList; import org.schabi.newpipe.extractor.exceptions.ExtractionException; import org.schabi.newpipe.extractor.playlist.PlaylistInfo; +import org.schabi.newpipe.extractor.services.youtube.YoutubeParsingHelper; import org.schabi.newpipe.extractor.stream.StreamInfoItem; import org.schabi.newpipe.extractor.stream.StreamType; import org.schabi.newpipe.fragments.list.BaseListInfoFragment; @@ -44,12 +46,12 @@ import org.schabi.newpipe.util.NavigationHelper; import org.schabi.newpipe.util.ShareUtils; import org.schabi.newpipe.util.StreamDialogEntry; -import org.schabi.newpipe.util.ThemeHelper; import java.util.ArrayList; import java.util.List; import java.util.concurrent.atomic.AtomicBoolean; +import de.hdodenhof.circleimageview.CircleImageView; import io.reactivex.Flowable; import io.reactivex.Single; import io.reactivex.android.schedulers.AndroidSchedulers; @@ -58,6 +60,7 @@ import io.reactivex.disposables.Disposables; import static org.schabi.newpipe.util.AnimationUtils.animateView; +import static org.schabi.newpipe.util.ThemeHelper.resolveResourceIdFromAttr; public class PlaylistFragment extends BaseListInfoFragment { private CompositeDisposable disposables; @@ -74,7 +77,7 @@ public class PlaylistFragment extends BaseListInfoFragment { private TextView headerTitleView; private View headerUploaderLayout; private TextView headerUploaderName; - private ImageView headerUploaderAvatar; + private CircleImageView headerUploaderAvatar; private TextView headerStreamCount; private View playlistCtrl; @@ -287,10 +290,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); } @@ -302,8 +303,22 @@ public void handleResult(@NonNull final PlaylistInfo result) { playlistCtrl.setVisibility(View.VISIBLE); - IMAGE_LOADER.displayImage(result.getUploaderAvatarUrl(), headerUploaderAvatar, - ImageDisplayConstants.DISPLAY_AVATAR_OPTIONS); + final String avatarUrl = result.getUploaderAvatarUrl(); + if (result.getServiceId() == ServiceList.YouTube.getServiceId() + && (YoutubeParsingHelper.isYoutubeMixId(result.getId()) + || YoutubeParsingHelper.isYoutubeMusicMixId(result.getId()))) { + // this is an auto-generated playlist (e.g. Youtube mix), so a radio is shown + headerUploaderAvatar.setDisableCircularTransformation(true); + headerUploaderAvatar.setBorderColor( + getResources().getColor(R.color.transparent_background_color)); + headerUploaderAvatar.setImageDrawable(AppCompatResources.getDrawable(requireContext(), + resolveResourceIdFromAttr(requireContext(), R.attr.ic_radio))); + + } else { + IMAGE_LOADER.displayImage(avatarUrl, headerUploaderAvatar, + ImageDisplayConstants.DISPLAY_AVATAR_OPTIONS); + } + headerStreamCount.setText(Localization .localizeStreamCount(getContext(), result.getStreamCount())); @@ -478,7 +493,7 @@ private void updateBookmarkButtons() { final int titleRes = playlistEntity == null ? R.string.bookmark_playlist : R.string.unbookmark_playlist; - playlistBookmarkButton.setIcon(ThemeHelper.resolveResourceIdFromAttr(activity, iconAttr)); + playlistBookmarkButton.setIcon(resolveResourceIdFromAttr(activity, iconAttr)); playlistBookmarkButton.setTitle(titleRes); } } diff --git a/app/src/main/java/org/schabi/newpipe/fragments/list/search/SearchFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/list/search/SearchFragment.java index acc8376c2..de31287a8 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/list/search/SearchFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/list/search/SearchFragment.java @@ -5,11 +5,11 @@ import android.content.Intent; import android.content.SharedPreferences; import android.os.Bundle; -import android.preference.PreferenceManager; import android.text.Editable; import android.text.Html; import android.text.TextUtils; import android.text.TextWatcher; +import android.text.style.CharacterStyle; import android.util.Log; import android.view.KeyEvent; import android.view.LayoutInflater; @@ -29,6 +29,7 @@ import androidx.appcompat.app.ActionBar; import androidx.appcompat.app.AlertDialog; import androidx.appcompat.widget.TooltipCompat; +import androidx.preference.PreferenceManager; import androidx.recyclerview.widget.ItemTouchHelper; import androidx.recyclerview.widget.RecyclerView; @@ -43,6 +44,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.newpipe.fragments.BackPressable; import org.schabi.newpipe.fragments.list.BaseListFragment; import org.schabi.newpipe.local.history.HistoryRecordManager; @@ -53,6 +56,7 @@ import org.schabi.newpipe.util.AnimationUtils; import org.schabi.newpipe.util.CompatibilityUtil; import org.schabi.newpipe.util.Constants; +import org.schabi.newpipe.util.ExceptionUtils; import org.schabi.newpipe.util.ExtractorHelper; import org.schabi.newpipe.util.NavigationHelper; import org.schabi.newpipe.util.ServiceHelper; @@ -75,12 +79,12 @@ import io.reactivex.schedulers.Schedulers; import io.reactivex.subjects.PublishSubject; -import static android.text.Html.escapeHtml; +import androidx.core.text.HtmlCompat; import static androidx.recyclerview.widget.ItemTouchHelper.Callback.makeMovementFlags; import static java.util.Arrays.asList; import static org.schabi.newpipe.util.AnimationUtils.animateView; -public class SearchFragment extends BaseListFragment +public class SearchFragment extends BaseListFragment> implements BackPressable { /*////////////////////////////////////////////////////////////////////////// // Search @@ -156,6 +160,7 @@ public class SearchFragment extends BaseListFragment 0 && !isLoading.get()) { hideSuggestionsPanel(); @@ -724,7 +733,7 @@ private void initSuggestionObserver() { suggestionDisposable = observable .switchMap(query -> { final Flowable> flowable = historyRecordManager - .getRelatedSearches(query, 3, 25); + .getRelatedSearches(query, 60, 100); final Observable> local = flowable.toObservable() .map(searchHistoryEntries -> { List result = new ArrayList<>(); @@ -742,6 +751,13 @@ private void initSuggestionObserver() { final Observable> network = ExtractorHelper .suggestionsFor(serviceId, query) + .onErrorReturn(throwable -> { + if (!ExceptionUtils.isNetworkRelated(throwable)) { + showSnackBarError(throwable, UserAction.GET_SUGGESTIONS, + CompatibilityUtil.getNameOfService(serviceId), searchString, 0); + } + return new ArrayList<>(); + }) .toObservable() .map(strings -> { List result = new ArrayList<>(); @@ -791,28 +807,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) { @@ -820,29 +838,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, - CompatibilityUtil.getNameOfService(serviceId), ss, 0) - ); - suggestionPublisher.onNext(ss); + CompatibilityUtil.getNameOfService(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(); } @@ -881,8 +897,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 @@ -895,22 +910,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; } /*////////////////////////////////////////////////////////////////////////// @@ -924,7 +942,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(); } } @@ -1005,10 +1023,9 @@ private void handleSearchSuggestion() { : R.string.did_you_mean); final String highlightedSearchSuggestion = - "" + escapeHtml(searchSuggestion) + ""; - correctSuggestion.setText( - Html.fromHtml(String.format(helperText, highlightedSearchSuggestion))); - + "" + Html.escapeHtml(searchSuggestion) + ""; + final String text = String.format(helperText, highlightedSearchSuggestion); + correctSuggestion.setText(HtmlCompat.fromHtml(text, HtmlCompat.FROM_HTML_MODE_LEGACY)); correctSuggestion.setOnClickListener(v -> { correctSuggestion.setVisibility(View.GONE); @@ -1028,7 +1045,7 @@ 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(); @@ -1067,8 +1084,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; @@ -1079,8 +1095,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/newpipe/fragments/list/videos/RelatedVideosFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/list/videos/RelatedVideosFragment.java index e4d221e47..bc622b379 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/list/videos/RelatedVideosFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/list/videos/RelatedVideosFragment.java @@ -3,7 +3,6 @@ import android.content.Context; import android.content.SharedPreferences; import android.os.Bundle; -import android.preference.PreferenceManager; import android.view.LayoutInflater; import android.view.Menu; import android.view.MenuInflater; @@ -13,6 +12,7 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import androidx.preference.PreferenceManager; import org.schabi.newpipe.R; import org.schabi.newpipe.extractor.ListExtractor; @@ -79,11 +79,11 @@ protected View getListHeader() { autoplaySwitch = headerRootLayout.findViewById(R.id.autoplay_switch); final SharedPreferences pref = PreferenceManager - .getDefaultSharedPreferences(getContext()); + .getDefaultSharedPreferences(requireContext()); final boolean autoplay = pref.getBoolean(getString(R.string.auto_queue_key), false); autoplaySwitch.setChecked(autoplay); autoplaySwitch.setOnCheckedChangeListener((compoundButton, b) -> - PreferenceManager.getDefaultSharedPreferences(getContext()).edit() + PreferenceManager.getDefaultSharedPreferences(requireContext()).edit() .putBoolean(getString(R.string.auto_queue_key), b).apply()); return headerRootLayout; } else { @@ -202,7 +202,7 @@ protected void onRestoreInstanceState(@NonNull final Bundle savedState) { @Override public void onSharedPreferenceChanged(final SharedPreferences sharedPreferences, final String s) { - SharedPreferences pref = PreferenceManager.getDefaultSharedPreferences(getContext()); + SharedPreferences pref = PreferenceManager.getDefaultSharedPreferences(requireContext()); boolean autoplay = pref.getBoolean(getString(R.string.auto_queue_key), false); if (autoplaySwitch != null) { autoplaySwitch.setChecked(autoplay); diff --git a/app/src/main/java/org/schabi/newpipe/info_list/holder/CommentsInfoItemHolder.java b/app/src/main/java/org/schabi/newpipe/info_list/holder/CommentsInfoItemHolder.java index 842d9c455..4fc2d9f84 100644 --- a/app/src/main/java/org/schabi/newpipe/info_list/holder/CommentsInfoItemHolder.java +++ b/app/src/main/java/org/schabi/newpipe/info_list/holder/CommentsInfoItemHolder.java @@ -1,6 +1,8 @@ package org.schabi.newpipe.info_list.holder; +import android.view.View; import android.view.ViewGroup; +import android.widget.ImageView; import android.widget.TextView; import org.schabi.newpipe.R; @@ -31,11 +33,15 @@ public class CommentsInfoItemHolder extends CommentsMiniInfoItemHolder { public final TextView itemTitleView; + private final ImageView itemHeartView; + private final ImageView itemPinnedView; 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); + itemPinnedView = itemView.findViewById(R.id.detail_pinned_view); } @Override @@ -49,5 +55,9 @@ public void updateFromItem(final InfoItem infoItem, final CommentsInfoItem item = (CommentsInfoItem) infoItem; itemTitleView.setText(item.getUploaderName()); + + itemHeartView.setVisibility(item.isHeartedByUploader() ? View.VISIBLE : View.GONE); + + itemPinnedView.setVisibility(item.isPinned() ? View.VISIBLE : View.GONE); } } diff --git a/app/src/main/java/org/schabi/newpipe/info_list/holder/CommentsMiniInfoItemHolder.java b/app/src/main/java/org/schabi/newpipe/info_list/holder/CommentsMiniInfoItemHolder.java index 863273a88..bf524b9f5 100644 --- a/app/src/main/java/org/schabi/newpipe/info_list/holder/CommentsMiniInfoItemHolder.java +++ b/app/src/main/java/org/schabi/newpipe/info_list/holder/CommentsMiniInfoItemHolder.java @@ -1,13 +1,18 @@ package org.schabi.newpipe.info_list.holder; +import android.content.SharedPreferences; import android.text.TextUtils; import android.text.method.LinkMovementMethod; import android.text.style.URLSpan; import android.text.util.Linkify; +import android.view.View; import android.view.ViewGroup; +import android.widget.RelativeLayout; import android.widget.TextView; import androidx.appcompat.app.AppCompatActivity; +import androidx.core.text.util.LinkifyCompat; +import androidx.preference.PreferenceManager; import org.schabi.newpipe.R; import org.schabi.newpipe.extractor.InfoItem; @@ -22,6 +27,7 @@ import org.schabi.newpipe.util.NavigationHelper; import org.schabi.newpipe.util.ShareUtils; +import java.io.IOException; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -31,7 +37,12 @@ public class CommentsMiniInfoItemHolder extends InfoItemHolder { private static final int COMMENT_DEFAULT_LINES = 2; private static final int COMMENT_EXPANDED_LINES = 1000; private static final Pattern PATTERN = Pattern.compile("(\\d+:)?(\\d+)?:(\\d+)"); + private final String downloadThumbnailKey; + private final int commentHorizontalPadding; + private final int commentVerticalPadding; + private SharedPreferences preferences = null; + private final RelativeLayout itemRoot; public final CircleImageView itemThumbnailView; private final TextView itemContentView; private final TextView itemLikesCountView; @@ -65,11 +76,20 @@ public String transformUrl(final Matcher match, final String url) { final ViewGroup parent) { super(infoItemBuilder, layoutId, parent); + itemRoot = itemView.findViewById(R.id.itemRoot); itemThumbnailView = itemView.findViewById(R.id.itemThumbnailView); itemLikesCountView = itemView.findViewById(R.id.detail_thumbs_up_count_view); itemDislikesCountView = itemView.findViewById(R.id.detail_thumbs_down_count_view); itemPublishedTime = itemView.findViewById(R.id.itemPublishedTime); itemContentView = itemView.findViewById(R.id.itemCommentContentView); + + downloadThumbnailKey = infoItemBuilder.getContext(). + getString(R.string.download_thumbnail_key); + + commentHorizontalPadding = (int) infoItemBuilder.getContext() + .getResources().getDimension(R.dimen.comments_horizontal_padding); + commentVerticalPadding = (int) infoItemBuilder.getContext() + .getResources().getDimension(R.dimen.comments_vertical_padding); } public CommentsMiniInfoItemHolder(final InfoItemBuilder infoItemBuilder, @@ -85,17 +105,36 @@ public void updateFromItem(final InfoItem infoItem, } final CommentsInfoItem item = (CommentsInfoItem) infoItem; + preferences = PreferenceManager.getDefaultSharedPreferences(itemBuilder.getContext()); + itemBuilder.getImageLoader() .displayImage(item.getUploaderAvatarUrl(), itemThumbnailView, ImageDisplayConstants.DISPLAY_THUMBNAIL_OPTIONS); + if (preferences.getBoolean(downloadThumbnailKey, true)) { + itemThumbnailView.setVisibility(View.VISIBLE); + itemRoot.setPadding(commentVerticalPadding, commentVerticalPadding, + commentVerticalPadding, commentVerticalPadding); + } else { + itemThumbnailView.setVisibility(View.GONE); + itemRoot.setPadding(commentHorizontalPadding, commentVerticalPadding, + commentHorizontalPadding, commentVerticalPadding); + } + itemThumbnailView.setOnClickListener(view -> openCommentAuthor(item)); streamUrl = item.getUrl(); itemContentView.setLines(COMMENT_DEFAULT_LINES); - commentText = item.getCommentText(); + // commentText = item.getCommentText(); + try { + commentText = item.getCommentText().getContent().replace(" ", "") + .replace("
", " ").replace("", " ") + .replace("
", " ").replace("
", " "); + } catch (Exception e) { + commentText = " "; + } itemContentView.setText(commentText); itemContentView.setOnTouchListener(CommentTextOnTouchListener.INSTANCE); @@ -106,7 +145,10 @@ public void updateFromItem(final InfoItem infoItem, } if (item.getLikeCount() >= 0) { - itemLikesCountView.setText(String.valueOf(item.getLikeCount())); + itemLikesCountView.setText( + Localization.shortCount( + itemBuilder.getContext(), + item.getLikeCount())); } else { itemLikesCountView.setText("-"); } diff --git a/app/src/main/java/org/schabi/newpipe/info_list/holder/StreamInfoItemHolder.java b/app/src/main/java/org/schabi/newpipe/info_list/holder/StreamInfoItemHolder.java index 5fa0904de..c89ca6c90 100644 --- a/app/src/main/java/org/schabi/newpipe/info_list/holder/StreamInfoItemHolder.java +++ b/app/src/main/java/org/schabi/newpipe/info_list/holder/StreamInfoItemHolder.java @@ -1,10 +1,11 @@ package org.schabi.newpipe.info_list.holder; -import android.preference.PreferenceManager; import android.text.TextUtils; import android.view.ViewGroup; import android.widget.TextView; +import androidx.preference.PreferenceManager; + import org.schabi.newpipe.R; import org.schabi.newpipe.extractor.InfoItem; import org.schabi.newpipe.extractor.stream.StreamInfoItem; diff --git a/app/src/main/java/org/schabi/newpipe/info_list/holder/StreamMiniInfoItemHolder.java b/app/src/main/java/org/schabi/newpipe/info_list/holder/StreamMiniInfoItemHolder.java index 962ccef54..824612fd2 100644 --- a/app/src/main/java/org/schabi/newpipe/info_list/holder/StreamMiniInfoItemHolder.java +++ b/app/src/main/java/org/schabi/newpipe/info_list/holder/StreamMiniInfoItemHolder.java @@ -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)); diff --git a/app/src/main/java/org/schabi/newpipe/local/BaseLocalListFragment.java b/app/src/main/java/org/schabi/newpipe/local/BaseLocalListFragment.java index 650953bea..8e3eb9ff0 100644 --- a/app/src/main/java/org/schabi/newpipe/local/BaseLocalListFragment.java +++ b/app/src/main/java/org/schabi/newpipe/local/BaseLocalListFragment.java @@ -4,7 +4,6 @@ import android.content.res.Configuration; import android.content.res.Resources; import android.os.Bundle; -import android.preference.PreferenceManager; import android.util.Log; import android.view.Menu; import android.view.MenuInflater; @@ -12,6 +11,7 @@ import androidx.appcompat.app.ActionBar; import androidx.fragment.app.Fragment; +import androidx.preference.PreferenceManager; import androidx.recyclerview.widget.GridLayoutManager; import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.RecyclerView; diff --git a/app/src/main/java/org/schabi/newpipe/local/feed/FeedDatabaseManager.kt b/app/src/main/java/org/schabi/newpipe/local/feed/FeedDatabaseManager.kt index d319c9fa3..4415d25e0 100644 --- a/app/src/main/java/org/schabi/newpipe/local/feed/FeedDatabaseManager.kt +++ b/app/src/main/java/org/schabi/newpipe/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.MainActivity.DEBUG import org.schabi.newpipe.NewPipeDatabase import org.schabi.newpipe.database.feed.model.FeedEntity @@ -18,6 +16,8 @@ import org.schabi.newpipe.database.stream.model.StreamEntity import org.schabi.newpipe.extractor.stream.StreamInfoItem import org.schabi.newpipe.extractor.stream.StreamType import org.schabi.newpipe.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/newpipe/local/feed/FeedFragment.kt b/app/src/main/java/org/schabi/newpipe/local/feed/FeedFragment.kt index 8018e2cd8..09b1c9783 100644 --- a/app/src/main/java/org/schabi/newpipe/local/feed/FeedFragment.kt +++ b/app/src/main/java/org/schabi/newpipe/local/feed/FeedFragment.kt @@ -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.newpipe.local.feed.service.FeedLoadService import org.schabi.newpipe.report.UserAction import org.schabi.newpipe.util.AnimationUtils.animateView import org.schabi.newpipe.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 } @@ -187,6 +190,7 @@ class FeedFragment : BaseListFragment() { empty_state_view?.let { animateView(it, false, 0) } animateView(error_panel, false, 0) + swipeRefreshLayout.isRefreshing = false } override fun showEmptyState() { @@ -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/newpipe/local/feed/FeedState.kt b/app/src/main/java/org/schabi/newpipe/local/feed/FeedState.kt index de3dd3113..00ca76b8e 100644 --- a/app/src/main/java/org/schabi/newpipe/local/feed/FeedState.kt +++ b/app/src/main/java/org/schabi/newpipe/local/feed/FeedState.kt @@ -1,8 +1,8 @@ package org.schabi.newpipe.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/newpipe/local/feed/FeedViewModel.kt b/app/src/main/java/org/schabi/newpipe/local/feed/FeedViewModel.kt index da2b5ffa4..fca09ee09 100644 --- a/app/src/main/java/org/schabi/newpipe/local/feed/FeedViewModel.kt +++ b/app/src/main/java/org/schabi/newpipe/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.database.feed.model.FeedGroupEntity import org.schabi.newpipe.extractor.stream.StreamInfoItem import org.schabi.newpipe.local.feed.service.FeedEventManager @@ -20,6 +17,9 @@ import org.schabi.newpipe.local.feed.service.FeedEventManager.Event.IdleEvent import org.schabi.newpipe.local.feed.service.FeedEventManager.Event.ProgressEvent import org.schabi.newpipe.local.feed.service.FeedEventManager.Event.SuccessResultEvent import org.schabi.newpipe.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/newpipe/local/feed/service/FeedEventManager.kt b/app/src/main/java/org/schabi/newpipe/local/feed/service/FeedEventManager.kt index b72098345..1759d56a5 100644 --- a/app/src/main/java/org/schabi/newpipe/local/feed/service/FeedEventManager.kt +++ b/app/src/main/java/org/schabi/newpipe/local/feed/service/FeedEventManager.kt @@ -3,8 +3,8 @@ package org.schabi.newpipe.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.newpipe.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/newpipe/local/feed/service/FeedLoadService.kt b/app/src/main/java/org/schabi/newpipe/local/feed/service/FeedLoadService.kt index 65860096c..cc3d5caa2 100644 --- a/app/src/main/java/org/schabi/newpipe/local/feed/service/FeedLoadService.kt +++ b/app/src/main/java/org/schabi/newpipe/local/feed/service/FeedLoadService.kt @@ -27,10 +27,10 @@ import android.content.Intent import android.content.IntentFilter import android.os.Build import android.os.IBinder -import android.preference.PreferenceManager import android.util.Log import androidx.core.app.NotificationCompat import androidx.core.app.NotificationManagerCompat +import androidx.preference.PreferenceManager import io.reactivex.Flowable import io.reactivex.Notification import io.reactivex.Single @@ -40,13 +40,9 @@ 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.App import org.schabi.newpipe.MainActivity.DEBUG import org.schabi.newpipe.R import org.schabi.newpipe.database.feed.model.FeedGroupEntity @@ -62,12 +58,17 @@ import org.schabi.newpipe.local.feed.service.FeedEventManager.postEvent import org.schabi.newpipe.local.subscription.SubscriptionManager import org.schabi.newpipe.util.ExceptionUtils import org.schabi.newpipe.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/newpipe/local/history/HistoryRecordManager.java b/app/src/main/java/org/schabi/newpipe/local/history/HistoryRecordManager.java index 96a385ca8..0e0e1e897 100644 --- a/app/src/main/java/org/schabi/newpipe/local/history/HistoryRecordManager.java +++ b/app/src/main/java/org/schabi/newpipe/local/history/HistoryRecordManager.java @@ -20,7 +20,7 @@ import android.content.Context; import android.content.SharedPreferences; -import android.preference.PreferenceManager; +import androidx.preference.PreferenceManager; import androidx.annotation.NonNull; diff --git a/app/src/main/java/org/schabi/newpipe/local/holder/LocalPlaylistStreamItemHolder.java b/app/src/main/java/org/schabi/newpipe/local/holder/LocalPlaylistStreamItemHolder.java index 61de65b41..b29643ba9 100644 --- a/app/src/main/java/org/schabi/newpipe/local/holder/LocalPlaylistStreamItemHolder.java +++ b/app/src/main/java/org/schabi/newpipe/local/holder/LocalPlaylistStreamItemHolder.java @@ -11,7 +11,6 @@ import org.schabi.newpipe.R; import org.schabi.newpipe.database.LocalItem; import org.schabi.newpipe.database.playlist.PlaylistStreamEntry; -import org.schabi.newpipe.database.stream.model.StreamStateEntity; import org.schabi.newpipe.extractor.NewPipe; import org.schabi.newpipe.local.LocalItemBuilder; import org.schabi.newpipe.local.history.HistoryRecordManager; @@ -22,7 +21,6 @@ import org.schabi.newpipe.views.AnimatedProgressBar; import java.text.DateFormat; -import java.util.ArrayList; import java.util.concurrent.TimeUnit; public class LocalPlaylistStreamItemHolder extends LocalItemHolder { @@ -71,15 +69,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); } @@ -105,7 +99,6 @@ public void updateFromItem(final LocalItem localItem, return true; }); - itemThumbnailView.setOnTouchListener(getOnTouchListener(item)); itemHandleView.setOnTouchListener(getOnTouchListener(item)); } @@ -117,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/newpipe/local/holder/LocalStatisticStreamItemHolder.java b/app/src/main/java/org/schabi/newpipe/local/holder/LocalStatisticStreamItemHolder.java index dd6abe8ba..b75603b99 100644 --- a/app/src/main/java/org/schabi/newpipe/local/holder/LocalStatisticStreamItemHolder.java +++ b/app/src/main/java/org/schabi/newpipe/local/holder/LocalStatisticStreamItemHolder.java @@ -11,7 +11,6 @@ import org.schabi.newpipe.R; import org.schabi.newpipe.database.LocalItem; import org.schabi.newpipe.database.stream.StreamStatisticsEntry; -import org.schabi.newpipe.database.stream.model.StreamStateEntity; import org.schabi.newpipe.extractor.NewPipe; import org.schabi.newpipe.local.LocalItemBuilder; import org.schabi.newpipe.local.history.HistoryRecordManager; @@ -22,7 +21,6 @@ import org.schabi.newpipe.views.AnimatedProgressBar; import java.text.DateFormat; -import java.util.ArrayList; import java.util.concurrent.TimeUnit; /* @@ -99,15 +97,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); } @@ -147,18 +141,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/newpipe/local/playlist/LocalPlaylistFragment.java b/app/src/main/java/org/schabi/newpipe/local/playlist/LocalPlaylistFragment.java index 485d3f391..b764226b8 100644 --- a/app/src/main/java/org/schabi/newpipe/local/playlist/LocalPlaylistFragment.java +++ b/app/src/main/java/org/schabi/newpipe/local/playlist/LocalPlaylistFragment.java @@ -96,7 +96,6 @@ public class LocalPlaylistFragment extends BaseLocalListFragment 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; } diff --git a/app/src/main/java/org/schabi/newpipe/local/subscription/SubscriptionFragment.kt b/app/src/main/java/org/schabi/newpipe/local/subscription/SubscriptionFragment.kt index 7fea3b5d8..427252e2c 100644 --- a/app/src/main/java/org/schabi/newpipe/local/subscription/SubscriptionFragment.kt +++ b/app/src/main/java/org/schabi/newpipe/local/subscription/SubscriptionFragment.kt @@ -11,7 +11,6 @@ import android.content.res.Configuration import android.os.Bundle import android.os.Environment import android.os.Parcelable -import android.preference.PreferenceManager import android.view.LayoutInflater import android.view.Menu import android.view.MenuInflater @@ -20,6 +19,7 @@ import android.view.ViewGroup import android.widget.Toast import androidx.lifecycle.ViewModelProviders import androidx.localbroadcastmanager.content.LocalBroadcastManager +import androidx.preference.PreferenceManager import androidx.recyclerview.widget.GridLayoutManager import com.nononsenseapps.filepicker.Utils import com.xwray.groupie.Group @@ -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.newpipe.util.NavigationHelper import org.schabi.newpipe.util.OnClickGesture import org.schabi.newpipe.util.ShareUtils import org.schabi.newpipe.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/newpipe/local/subscription/SubscriptionManager.kt b/app/src/main/java/org/schabi/newpipe/local/subscription/SubscriptionManager.kt index 2740591e6..b36ae110e 100644 --- a/app/src/main/java/org/schabi/newpipe/local/subscription/SubscriptionManager.kt +++ b/app/src/main/java/org/schabi/newpipe/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/newpipe/local/subscription/SubscriptionViewModel.kt b/app/src/main/java/org/schabi/newpipe/local/subscription/SubscriptionViewModel.kt index b7f16c319..37c1211b2 100644 --- a/app/src/main/java/org/schabi/newpipe/local/subscription/SubscriptionViewModel.kt +++ b/app/src/main/java/org/schabi/newpipe/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.newpipe.local.feed.FeedDatabaseManager import org.schabi.newpipe.local.subscription.item.ChannelItem import org.schabi.newpipe.local.subscription.item.FeedGroupCardItem import org.schabi.newpipe.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/newpipe/local/subscription/dialog/FeedGroupDialog.kt b/app/src/main/java/org/schabi/newpipe/local/subscription/dialog/FeedGroupDialog.kt index 66387d298..905d12240 100644 --- a/app/src/main/java/org/schabi/newpipe/local/subscription/dialog/FeedGroupDialog.kt +++ b/app/src/main/java/org/schabi/newpipe/local/subscription/dialog/FeedGroupDialog.kt @@ -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.newpipe.R @@ -42,6 +40,8 @@ import org.schabi.newpipe.local.subscription.item.PickerIconItem import org.schabi.newpipe.local.subscription.item.PickerSubscriptionItem import org.schabi.newpipe.util.AndroidTvUtils import org.schabi.newpipe.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/newpipe/local/subscription/dialog/FeedGroupDialogViewModel.kt b/app/src/main/java/org/schabi/newpipe/local/subscription/dialog/FeedGroupDialogViewModel.kt index e9a7e4eb7..f03803024 100644 --- a/app/src/main/java/org/schabi/newpipe/local/subscription/dialog/FeedGroupDialogViewModel.kt +++ b/app/src/main/java/org/schabi/newpipe/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/newpipe/local/subscription/dialog/FeedGroupReorderDialog.kt b/app/src/main/java/org/schabi/newpipe/local/subscription/dialog/FeedGroupReorderDialog.kt index 92c063b4b..0d0c0ae65 100644 --- a/app/src/main/java/org/schabi/newpipe/local/subscription/dialog/FeedGroupReorderDialog.kt +++ b/app/src/main/java/org/schabi/newpipe/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.newpipe.R @@ -25,6 +24,7 @@ import org.schabi.newpipe.local.subscription.dialog.FeedGroupReorderDialogViewMo import org.schabi.newpipe.local.subscription.dialog.FeedGroupReorderDialogViewModel.DialogEvent.SuccessEvent import org.schabi.newpipe.local.subscription.item.FeedGroupReorderItem import org.schabi.newpipe.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/newpipe/local/subscription/dialog/FeedGroupReorderDialogViewModel.kt b/app/src/main/java/org/schabi/newpipe/local/subscription/dialog/FeedGroupReorderDialogViewModel.kt index ea2cbe98f..5b42c3163 100644 --- a/app/src/main/java/org/schabi/newpipe/local/subscription/dialog/FeedGroupReorderDialogViewModel.kt +++ b/app/src/main/java/org/schabi/newpipe/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/newpipe/local/subscription/item/ChannelItem.kt b/app/src/main/java/org/schabi/newpipe/local/subscription/item/ChannelItem.kt index f33c58f43..8089f6480 100644 --- a/app/src/main/java/org/schabi/newpipe/local/subscription/item/ChannelItem.kt +++ b/app/src/main/java/org/schabi/newpipe/local/subscription/item/ChannelItem.kt @@ -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/newpipe/local/subscription/item/FeedGroupReorderItem.kt b/app/src/main/java/org/schabi/newpipe/local/subscription/item/FeedGroupReorderItem.kt index 717e2410a..e56bb408c 100644 --- a/app/src/main/java/org/schabi/newpipe/local/subscription/item/FeedGroupReorderItem.kt +++ b/app/src/main/java/org/schabi/newpipe/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/newpipe/local/subscription/item/FeedImportExportItem.kt b/app/src/main/java/org/schabi/newpipe/local/subscription/item/FeedImportExportItem.kt index 5478dcac4..5fd70a684 100644 --- a/app/src/main/java/org/schabi/newpipe/local/subscription/item/FeedImportExportItem.kt +++ b/app/src/main/java/org/schabi/newpipe/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/newpipe/local/subscription/item/HeaderWithMenuItem.kt b/app/src/main/java/org/schabi/newpipe/local/subscription/item/HeaderWithMenuItem.kt index 324932256..0f5bdeb94 100644 --- a/app/src/main/java/org/schabi/newpipe/local/subscription/item/HeaderWithMenuItem.kt +++ b/app/src/main/java/org/schabi/newpipe/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/newpipe/local/subscription/item/PickerSubscriptionItem.kt b/app/src/main/java/org/schabi/newpipe/local/subscription/item/PickerSubscriptionItem.kt index 7d33da71f..9fa8c652b 100644 --- a/app/src/main/java/org/schabi/newpipe/local/subscription/item/PickerSubscriptionItem.kt +++ b/app/src/main/java/org/schabi/newpipe/local/subscription/item/PickerSubscriptionItem.kt @@ -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/newpipe/local/subscription/services/SubscriptionsExportService.java b/app/src/main/java/org/schabi/newpipe/local/subscription/services/SubscriptionsExportService.java index 12b64d89d..8a7d7d6ea 100644 --- a/app/src/main/java/org/schabi/newpipe/local/subscription/services/SubscriptionsExportService.java +++ b/app/src/main/java/org/schabi/newpipe/local/subscription/services/SubscriptionsExportService.java @@ -27,6 +27,7 @@ import org.reactivestreams.Subscriber; import org.reactivestreams.Subscription; +import org.schabi.newpipe.App; import org.schabi.newpipe.R; import org.schabi.newpipe.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.newpipe.local.subscription" + 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/newpipe/local/subscription/services/SubscriptionsImportService.java b/app/src/main/java/org/schabi/newpipe/local/subscription/services/SubscriptionsImportService.java index 06ba55106..5fb278d9c 100644 --- a/app/src/main/java/org/schabi/newpipe/local/subscription/services/SubscriptionsImportService.java +++ b/app/src/main/java/org/schabi/newpipe/local/subscription/services/SubscriptionsImportService.java @@ -20,15 +20,18 @@ package org.schabi.newpipe.local.subscription.services; import android.content.Intent; +import android.net.Uri; import android.text.TextUtils; import android.util.Log; import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import androidx.documentfile.provider.DocumentFile; import androidx.localbroadcastmanager.content.LocalBroadcastManager; import org.reactivestreams.Subscriber; import org.reactivestreams.Subscription; +import org.schabi.newpipe.App; import org.schabi.newpipe.R; import org.schabi.newpipe.database.subscription.SubscriptionEntity; import org.schabi.newpipe.extractor.NewPipe; @@ -66,7 +69,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.newpipe.local.subscription" + public static final String IMPORT_COMPLETE_ACTION = App.PACKAGE_NAME + ".local.subscription" + ".services.SubscriptionsImportService.IMPORT_COMPLETE"; /** @@ -87,6 +90,9 @@ public class SubscriptionsImportService extends BaseImportExportService { private String channelUrl; @Nullable private InputStream inputStream; + @Nullable + private String inputStreamType; + private Uri uri; @Override public int onStartCommand(final Intent intent, final int flags, final int startId) { @@ -110,6 +116,9 @@ public int onStartCommand(final Intent intent, final int flags, final int startI try { inputStream = new FileInputStream(new File(filePath)); + + final DocumentFile documentFile = DocumentFile.fromSingleUri(this, uri); + inputStreamType = documentFile.getType(); } catch (FileNotFoundException e) { handleError(e); return START_NOT_STICKY; @@ -279,7 +288,7 @@ private Flowable> importFromChannelUrl() { private Flowable> importFromInputStream() { return Flowable.fromCallable(() -> NewPipe.getService(currentServiceId) .getSubscriptionExtractor() - .fromInputStream(inputStream)); + .fromInputStream(inputStream, inputStreamType)); } private Flowable> importFromPreviousExport() { diff --git a/app/src/main/java/org/schabi/newpipe/player/BackgroundPlayer.java b/app/src/main/java/org/schabi/newpipe/player/BackgroundPlayer.java index 57df398b6..9972d4018 100644 --- a/app/src/main/java/org/schabi/newpipe/player/BackgroundPlayer.java +++ b/app/src/main/java/org/schabi/newpipe/player/BackgroundPlayer.java @@ -32,7 +32,7 @@ import android.os.Build.VERSION; import android.os.Build.VERSION_CODES; import android.os.IBinder; -import android.preference.PreferenceManager; +import androidx.preference.PreferenceManager; import android.util.Log; import android.view.View; import android.widget.RemoteViews; @@ -50,6 +50,7 @@ import com.nostra13.universalimageloader.core.assist.FailReason; import org.schabi.newpipe.BuildConfig; +import org.schabi.newpipe.App; import org.schabi.newpipe.R; import org.schabi.newpipe.extractor.stream.StreamInfo; import org.schabi.newpipe.player.event.PlayerEventListener; @@ -70,19 +71,19 @@ */ public final class BackgroundPlayer extends Service { public static final String ACTION_CLOSE - = "org.schabi.newpipe.player.BackgroundPlayer.CLOSE"; + = App.PACKAGE_NAME + ".player.BackgroundPlayer.CLOSE"; public static final String ACTION_PLAY_PAUSE - = "org.schabi.newpipe.player.BackgroundPlayer.PLAY_PAUSE"; + = App.PACKAGE_NAME + ".player.BackgroundPlayer.PLAY_PAUSE"; public static final String ACTION_REPEAT - = "org.schabi.newpipe.player.BackgroundPlayer.REPEAT"; + = App.PACKAGE_NAME + ".player.BackgroundPlayer.REPEAT"; public static final String ACTION_PLAY_NEXT - = "org.schabi.newpipe.player.BackgroundPlayer.ACTION_PLAY_NEXT"; + = App.PACKAGE_NAME + ".player.BackgroundPlayer.ACTION_PLAY_NEXT"; public static final String ACTION_PLAY_PREVIOUS - = "org.schabi.newpipe.player.BackgroundPlayer.ACTION_PLAY_PREVIOUS"; + = App.PACKAGE_NAME + ".player.BackgroundPlayer.ACTION_PLAY_PREVIOUS"; public static final String ACTION_FAST_REWIND - = "org.schabi.newpipe.player.BackgroundPlayer.ACTION_FAST_REWIND"; + = App.PACKAGE_NAME + ".player.BackgroundPlayer.ACTION_FAST_REWIND"; public static final String ACTION_FAST_FORWARD - = "org.schabi.newpipe.player.BackgroundPlayer.ACTION_FAST_FORWARD"; + = App.PACKAGE_NAME + ".player.BackgroundPlayer.ACTION_FAST_FORWARD"; public static final String SET_IMAGE_RESOURCE_METHOD = "setImageResource"; private static final String TAG = "BackgroundPlayer"; diff --git a/app/src/main/java/org/schabi/newpipe/player/BackgroundPlayerActivity.java b/app/src/main/java/org/schabi/newpipe/player/BackgroundPlayerActivity.java index 9da3c3c86..9af5d6c49 100644 --- a/app/src/main/java/org/schabi/newpipe/player/BackgroundPlayerActivity.java +++ b/app/src/main/java/org/schabi/newpipe/player/BackgroundPlayerActivity.java @@ -1,6 +1,7 @@ package org.schabi.newpipe.player; import android.content.Intent; +import android.view.Menu; import android.view.MenuItem; import org.schabi.newpipe.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/newpipe/player/BasePlayer.java b/app/src/main/java/org/schabi/newpipe/player/BasePlayer.java index 61c5d9e68..23e3adc42 100644 --- a/app/src/main/java/org/schabi/newpipe/player/BasePlayer.java +++ b/app/src/main/java/org/schabi/newpipe/player/BasePlayer.java @@ -27,7 +27,7 @@ import android.graphics.Bitmap; import android.graphics.BitmapFactory; import android.media.AudioManager; -import android.preference.PreferenceManager; +import androidx.preference.PreferenceManager; import android.util.Log; import android.view.View; import android.widget.Toast; @@ -56,8 +56,10 @@ import org.schabi.newpipe.BuildConfig; import org.schabi.newpipe.DownloaderImpl; +import org.schabi.newpipe.App; import org.schabi.newpipe.R; import org.schabi.newpipe.extractor.stream.StreamInfo; +import org.schabi.newpipe.extractor.stream.StreamType; import org.schabi.newpipe.local.history.HistoryRecordManager; import org.schabi.newpipe.player.helper.AudioReactor; import org.schabi.newpipe.player.helper.LoadController; @@ -134,7 +136,7 @@ public abstract class BasePlayer implements // Playback //////////////////////////////////////////////////////////////////////////*/ - protected static final float[] PLAYBACK_SPEEDS = {0.5f, 0.75f, 1f, 1.25f, 1.5f, 1.75f, 2f}; + protected static final float[] PLAYBACK_SPEEDS = {0.5f, 0.75f, 1f, 1.25f, 1.5f, 1.75f, 2f, 2.25f, 2.5f, 2.75f, 3.0f}; protected PlayQueue playQueue; protected PlayQueueAdapter playQueueAdapter; @@ -157,7 +159,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; @@ -447,10 +449,19 @@ public void onLoadingFailed(final String imageUri, final View view, @Override public void onLoadingComplete(final String imageUri, final View view, final Bitmap loadedImage) { + final float width = Math.min( + context.getResources().getDimension(R.dimen.player_notification_thumbnail_width), + loadedImage.getWidth()); + currentThumbnail = Bitmap.createScaledBitmap(loadedImage, + (int) width, + (int) (loadedImage.getHeight() / (loadedImage.getWidth() / width)), true); + if (DEBUG) { Log.d(TAG, "Thumbnail - onLoadingComplete() called with: " + "imageUri = [" + imageUri + "], view = [" + view + "], " - + "loadedImage = [" + loadedImage + "]"); + + "loadedImage = [" + loadedImage + "], " + + loadedImage.getWidth() + "x" + loadedImage.getHeight() + + ", scaled width = " + width); } currentThumbnail = loadedImage; } @@ -648,9 +659,22 @@ public void triggerProgressUpdate() { if (simpleExoPlayer == null) { return; } + // Use duration of currentItem for non-live streams, + // because HLS streams are fragmented + // and thus the whole duration is not available to the player + // TODO: revert #6307 when introducing proper HLS support + final int duration; + if (currentItem != null + && currentItem.getStreamType() != StreamType.AUDIO_LIVE_STREAM + && currentItem.getStreamType() != StreamType.LIVE_STREAM) { + // convert seconds to milliseconds + duration = (int) (currentItem.getDuration() * 1000); + } else { + duration = (int) simpleExoPlayer.getDuration(); + } onUpdateProgress( Math.max((int) simpleExoPlayer.getCurrentPosition(), 0), - (int) simpleExoPlayer.getDuration(), + duration, simpleExoPlayer.getBufferedPercentage() ); } diff --git a/app/src/main/java/org/schabi/newpipe/player/MainVideoPlayer.java b/app/src/main/java/org/schabi/newpipe/player/MainVideoPlayer.java index 56744d858..f373a5e5e 100644 --- a/app/src/main/java/org/schabi/newpipe/player/MainVideoPlayer.java +++ b/app/src/main/java/org/schabi/newpipe/player/MainVideoPlayer.java @@ -33,7 +33,7 @@ import android.os.Build; import android.os.Bundle; import android.os.Handler; -import android.preference.PreferenceManager; +import androidx.preference.PreferenceManager; import android.provider.Settings; import android.util.DisplayMetrics; import android.util.Log; @@ -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.newpipe.util.ListHelper; import org.schabi.newpipe.util.NavigationHelper; import org.schabi.newpipe.util.PermissionHelper; +import org.schabi.newpipe.util.ServiceHelper; import org.schabi.newpipe.util.ShareUtils; import org.schabi.newpipe.util.StateSaver; import org.schabi.newpipe.util.ThemeHelper; @@ -222,6 +224,13 @@ public boolean onKeyDown(final int keyCode, final KeyEvent event) { switch (event.getKeyCode()) { default: break; + case KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE: + if (playerImpl.isPlaying()) { + break; + } + case KeyEvent.KEYCODE_MEDIA_PLAY: + playerImpl.hideControls(DEFAULT_CONTROLS_DURATION, DEFAULT_CONTROLS_HIDE_TIME); + break; case KeyEvent.KEYCODE_BACK: if (AndroidTvUtils.isTv(getApplicationContext()) && playerImpl.isControlsVisible()) { @@ -517,6 +526,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 +571,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 +630,7 @@ public void initListeners() { gestureDetector.setIsLongpressEnabled(false); getRootView().setOnTouchListener(listener); + metadata.setOnClickListener(this); queueButton.setOnClickListener(this); repeatButton.setOnClickListener(this); shuffleButton.setOnClickListener(this); @@ -702,6 +714,12 @@ public void onShuffleClicked() { updatePlaybackButtons(); } + @Override + public void onPlay() { + super.onPlay(); + showControlsThenHide(); + } + /*////////////////////////////////////////////////////////////////////////// // Playback Listener //////////////////////////////////////////////////////////////////////////*/ @@ -814,6 +832,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 +877,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) { @@ -1101,7 +1130,7 @@ public void safeHideControls(final long duration, final long delay) { } View controlsRoot = getControlsRoot(); - if (controlsRoot.isInTouchMode()) { + if (controlsRoot.isInTouchMode() || isPlaying()) { getControlsVisibilityHandler().removeCallbacksAndMessages(null); getControlsVisibilityHandler().postDelayed(() -> animateView(controlsRoot, false, duration, 0, diff --git a/app/src/main/java/org/schabi/newpipe/player/PopupVideoPlayer.java b/app/src/main/java/org/schabi/newpipe/player/PopupVideoPlayer.java index 0ccec3067..96b7146ca 100644 --- a/app/src/main/java/org/schabi/newpipe/player/PopupVideoPlayer.java +++ b/app/src/main/java/org/schabi/newpipe/player/PopupVideoPlayer.java @@ -34,7 +34,7 @@ import android.graphics.PixelFormat; import android.os.Build; import android.os.IBinder; -import android.preference.PreferenceManager; +import androidx.preference.PreferenceManager; import android.util.DisplayMetrics; import android.util.Log; import android.view.GestureDetector; @@ -65,6 +65,7 @@ import com.nostra13.universalimageloader.core.assist.FailReason; import org.schabi.newpipe.BuildConfig; +import org.schabi.newpipe.App; import org.schabi.newpipe.R; import org.schabi.newpipe.extractor.stream.VideoStream; import org.schabi.newpipe.player.event.PlayerEventListener; @@ -89,10 +90,10 @@ * @author mauriciocolli */ public final class PopupVideoPlayer extends Service { - public static final String ACTION_CLOSE = "org.schabi.newpipe.player.PopupVideoPlayer.CLOSE"; + public static final String ACTION_CLOSE = App.PACKAGE_NAME + ".player.PopupVideoPlayer.CLOSE"; public static final String ACTION_PLAY_PAUSE - = "org.schabi.newpipe.player.PopupVideoPlayer.PLAY_PAUSE"; - public static final String ACTION_REPEAT = "org.schabi.newpipe.player.PopupVideoPlayer.REPEAT"; + = App.PACKAGE_NAME + ".player.PopupVideoPlayer.PLAY_PAUSE"; + public static final String ACTION_REPEAT = App.PACKAGE_NAME + ".player.PopupVideoPlayer.REPEAT"; private static final String TAG = ".PopupVideoPlayer"; private static final boolean DEBUG = BasePlayer.DEBUG; private static final int NOTIFICATION_ID = 40028922; @@ -133,6 +134,8 @@ public final class PopupVideoPlayer extends Service { private VideoPlayerImpl playerImpl; private boolean isPopupClosing = false; + private static final float MAXIMUM_OPACITY_ALLOWED_FOR_R_AND_HIGHER = 0.8f; + /*////////////////////////////////////////////////////////////////////////// // Service-Activity Binder //////////////////////////////////////////////////////////////////////////*/ @@ -224,7 +227,7 @@ private void initPopup() { popupWidth = popupRememberSizeAndPos ? sharedPreferences.getFloat(POPUP_SAVED_WIDTH, defaultSize) : defaultSize; - final int layoutParamType = Build.VERSION.SDK_INT < android.os.Build.VERSION_CODES.O + final int layoutParamType = Build.VERSION.SDK_INT < Build.VERSION_CODES.O ? WindowManager.LayoutParams.TYPE_PHONE : WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY; @@ -274,6 +277,13 @@ private void initPopupCloseOverlay() { layoutParamType, flags, PixelFormat.TRANSLUCENT); + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { + // Setting maximum opacity allowed for touch events to other apps for Android 12 and + // higher to prevent non interaction when using other apps with the popup player + closeOverlayLayoutParams.alpha = MAXIMUM_OPACITY_ALLOWED_FOR_R_AND_HIGHER; + } + closeOverlayLayoutParams.gravity = Gravity.LEFT | Gravity.TOP; closeOverlayLayoutParams.softInputMode = WindowManager .LayoutParams.SOFT_INPUT_ADJUST_RESIZE; diff --git a/app/src/main/java/org/schabi/newpipe/player/ServicePlayerActivity.java b/app/src/main/java/org/schabi/newpipe/player/ServicePlayerActivity.java index 72becef8f..debc6704e 100644 --- a/app/src/main/java/org/schabi/newpipe/player/ServicePlayerActivity.java +++ b/app/src/main/java/org/schabi/newpipe/player/ServicePlayerActivity.java @@ -1,5 +1,6 @@ package org.schabi.newpipe.player; +import android.app.AlertDialog; import android.content.ComponentName; import android.content.Intent; import android.content.ServiceConnection; @@ -17,9 +18,12 @@ import android.widget.ProgressBar; import android.widget.SeekBar; import android.widget.TextView; +import android.widget.Toast; +import androidx.annotation.Nullable; import androidx.appcompat.app.AppCompatActivity; import androidx.appcompat.widget.Toolbar; +import androidx.core.app.ActivityCompat; import androidx.recyclerview.widget.ItemTouchHelper; import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.RecyclerView; @@ -40,10 +44,14 @@ import org.schabi.newpipe.player.playqueue.PlayQueueItemTouchCallback; import org.schabi.newpipe.util.Localization; import org.schabi.newpipe.util.NavigationHelper; +import org.schabi.newpipe.util.ServiceHelper; import org.schabi.newpipe.util.ThemeHelper; import java.util.Collections; +import java.util.Date; import java.util.List; +import java.util.Timer; +import java.util.TimerTask; import static org.schabi.newpipe.player.helper.PlayerHelper.formatSpeed; import static org.schabi.newpipe.util.Localization.assureCorrectAppLanguage; @@ -115,6 +123,11 @@ public abstract class ServicePlayerActivity extends AppCompatActivity // Activity Lifecycle //////////////////////////////////////////////////////////////////////////// + @Override + public void openOptionsMenu() { + super.openOptionsMenu(); + } + @Override protected void onCreate(final Bundle savedInstanceState) { assureCorrectAppLanguage(this); @@ -138,7 +151,7 @@ protected void onCreate(final Bundle savedInstanceState) { protected void onResume() { super.onResume(); if (redraw) { - recreate(); + ActivityCompat.recreate(this); redraw = false; } } @@ -149,6 +162,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; } @@ -181,10 +198,57 @@ public boolean onOptionsItemSelected(final MenuItem item) { .putExtra(BasePlayer.START_PAUSED, !this.player.isPlaying()) ); return true; + case R.id.action_set_timer: + setTimer(this); + return true; } return onPlayerOptionSelected(item) || super.onOptionsItemSelected(item); } + private void setTimer(ServicePlayerActivity servicePlayerActivity) { + String[] listItems = {"5 Minutes", "10 Minutes", "20 Minutes", "30 Minutes", "45 Minutes", "1 Hour"}; + + AlertDialog.Builder builder = new AlertDialog.Builder(servicePlayerActivity); + builder.setTitle("Choose item"); + + builder.setItems(listItems, (dialog, which) -> { + long time = getTimerTime(which); + Timer timer = new Timer(); + timer.schedule(new TimerTask() { + @Override + public void run() { + runOnUiThread(() -> player.onPause()); + } + }, time); + Date d = new Date(); + d.setTime(System.currentTimeMillis() + time); + Toast.makeText(servicePlayerActivity, "Player will pause after : " + listItems[which] + " at : " + d, Toast.LENGTH_LONG).show(); + }); + + AlertDialog dialog = builder.create(); + dialog.show(); + + } + + protected long getTimerTime(int index) { + switch (index) { + case 0: + return 5 * 60 * 1000; + case 1: + return 10 * 60 * 1000; + case 2: + return 20 * 60 * 1000; + case 3: + return 30 * 60 * 1000; + case 4: + return 45 * 60 * 1000; + case 5: + return 60 * 60 * 1000; + default: + return 1000; + } + } + @Override protected void onDestroy() { super.onDestroy(); @@ -485,7 +549,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 +573,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 +755,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/newpipe/player/VideoPlayer.java b/app/src/main/java/org/schabi/newpipe/player/VideoPlayer.java index 576d42a00..9eac7e79b 100644 --- a/app/src/main/java/org/schabi/newpipe/player/VideoPlayer.java +++ b/app/src/main/java/org/schabi/newpipe/player/VideoPlayer.java @@ -32,7 +32,7 @@ import android.graphics.PorterDuff; import android.os.Build; import android.os.Handler; -import android.preference.PreferenceManager; +import androidx.preference.PreferenceManager; import android.util.Log; import android.view.Menu; import android.view.MenuItem; @@ -98,8 +98,8 @@ 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 DEFAULT_CONTROLS_HIDE_TIME = 2000; // 2 seconds + public static final int DPAD_CONTROLS_HIDE_TIME = 5000; // 5 seconds protected static final int RENDERER_UNAVAILABLE = -1; @@ -252,7 +252,7 @@ public void initPlayer(final boolean playOnReady) { simpleExoPlayer.addTextOutput(cues -> subtitleView.onCues(cues)); // Setup audio session with onboard equalizer - if (Build.VERSION.SDK_INT >= 21) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { trackSelector.setParameters(trackSelector.buildUponParameters() .setTunnelingAudioSessionId(C.generateAudioSessionIdV21(context))); } diff --git a/app/src/main/java/org/schabi/newpipe/player/helper/AudioReactor.java b/app/src/main/java/org/schabi/newpipe/player/helper/AudioReactor.java index 369e3236e..a49e998a3 100644 --- a/app/src/main/java/org/schabi/newpipe/player/helper/AudioReactor.java +++ b/app/src/main/java/org/schabi/newpipe/player/helper/AudioReactor.java @@ -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,16 @@ public void onAnimationEnd(final Animator animation) { @Override public void onAudioSessionId(final EventTime eventTime, final int audioSessionId) { + notifyAudioSessionUpdate(true, audioSessionId); + } + + 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/newpipe/player/helper/LoadController.java b/app/src/main/java/org/schabi/newpipe/player/helper/LoadController.java index 92ae009f6..d2e509144 100644 --- a/app/src/main/java/org/schabi/newpipe/player/helper/LoadController.java +++ b/app/src/main/java/org/schabi/newpipe/player/helper/LoadController.java @@ -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/newpipe/player/helper/PlaybackParameterDialog.java b/app/src/main/java/org/schabi/newpipe/player/helper/PlaybackParameterDialog.java index 0d511d565..b63a9cd87 100644 --- a/app/src/main/java/org/schabi/newpipe/player/helper/PlaybackParameterDialog.java +++ b/app/src/main/java/org/schabi/newpipe/player/helper/PlaybackParameterDialog.java @@ -1,9 +1,11 @@ package org.schabi.newpipe.player.helper; +import static org.schabi.newpipe.player.BasePlayer.DEBUG; +import static org.schabi.newpipe.util.Localization.assureCorrectAppLanguage; import android.app.Dialog; import android.content.Context; import android.os.Bundle; -import android.preference.PreferenceManager; +import androidx.preference.PreferenceManager; import android.util.Log; import android.view.View; import android.widget.CheckBox; @@ -18,9 +20,6 @@ import org.schabi.newpipe.R; import org.schabi.newpipe.util.SliderStrategy; -import static org.schabi.newpipe.player.BasePlayer.DEBUG; -import static org.schabi.newpipe.util.Localization.assureCorrectAppLanguage; - public class PlaybackParameterDialog extends DialogFragment { // Minimum allowable range in ExoPlayer private static final double MINIMUM_PLAYBACK_VALUE = 0.10f; @@ -120,7 +119,7 @@ public void onAttach(final Context context) { @Override public void onCreate(@Nullable final Bundle savedInstanceState) { - assureCorrectAppLanguage(getContext()); + assureCorrectAppLanguage(requireContext()); super.onCreate(savedInstanceState); if (savedInstanceState != null) { initialTempo = savedInstanceState.getDouble(INITIAL_TEMPO_KEY, DEFAULT_TEMPO); @@ -150,12 +149,11 @@ public void onSaveInstanceState(final Bundle outState) { @NonNull @Override public Dialog onCreateDialog(@Nullable final Bundle savedInstanceState) { - assureCorrectAppLanguage(getContext()); - final View view = View.inflate(getContext(), R.layout.dialog_playback_parameter, null); + assureCorrectAppLanguage(requireContext()); + final View view = View.inflate(requireContext(), R.layout.dialog_playback_parameter, null); 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) -> @@ -237,12 +235,12 @@ private void setupHookingControl(@NonNull final View rootView) { unhookingCheckbox = rootView.findViewById(R.id.unhookCheckbox); if (unhookingCheckbox != null) { // restore whether pitch and tempo are unhooked or not - unhookingCheckbox.setChecked(PreferenceManager.getDefaultSharedPreferences(getContext()) + unhookingCheckbox.setChecked(PreferenceManager.getDefaultSharedPreferences(requireContext()) .getBoolean(getString(R.string.playback_unhook_key), true)); unhookingCheckbox.setOnCheckedChangeListener((compoundButton, isChecked) -> { // save whether pitch and tempo are unhooked or not - PreferenceManager.getDefaultSharedPreferences(getContext()) + PreferenceManager.getDefaultSharedPreferences(requireContext()) .edit() .putBoolean(getString(R.string.playback_unhook_key), isChecked) .apply(); diff --git a/app/src/main/java/org/schabi/newpipe/player/helper/PlayerDataSource.java b/app/src/main/java/org/schabi/newpipe/player/helper/PlayerDataSource.java index 5fea4761b..447cf9012 100644 --- a/app/src/main/java/org/schabi/newpipe/player/helper/PlayerDataSource.java +++ b/app/src/main/java/org/schabi/newpipe/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/newpipe/player/helper/PlayerHelper.java b/app/src/main/java/org/schabi/newpipe/player/helper/PlayerHelper.java index e63e56bf9..a700647b1 100644 --- a/app/src/main/java/org/schabi/newpipe/player/helper/PlayerHelper.java +++ b/app/src/main/java/org/schabi/newpipe/player/helper/PlayerHelper.java @@ -3,7 +3,7 @@ import android.content.Context; import android.content.SharedPreferences; import android.os.Build; -import android.preference.PreferenceManager; +import androidx.preference.PreferenceManager; import android.view.accessibility.CaptioningManager; import androidx.annotation.IntDef; @@ -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/newpipe/player/playqueue/PlayQueueItemBuilder.java b/app/src/main/java/org/schabi/newpipe/player/playqueue/PlayQueueItemBuilder.java index 30a959784..fafb6c87f 100644 --- a/app/src/main/java/org/schabi/newpipe/player/playqueue/PlayQueueItemBuilder.java +++ b/app/src/main/java/org/schabi/newpipe/player/playqueue/PlayQueueItemBuilder.java @@ -53,7 +53,6 @@ public void buildStreamInfoItem(final PlayQueueItemHolder holder, final PlayQueu return false; }); - holder.itemThumbnailView.setOnTouchListener(getOnTouchListener(holder)); holder.itemHandle.setOnTouchListener(getOnTouchListener(holder)); } diff --git a/app/src/main/java/org/schabi/newpipe/report/AcraReportSenderFactory.java b/app/src/main/java/org/schabi/newpipe/report/AcraReportSenderFactory.java index f4c1c4ac8..2655ea672 100644 --- a/app/src/main/java/org/schabi/newpipe/report/AcraReportSenderFactory.java +++ b/app/src/main/java/org/schabi/newpipe/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.newpipe.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/newpipe/report/ErrorActivity.java b/app/src/main/java/org/schabi/newpipe/report/ErrorActivity.java index 8946f866c..63db7782c 100644 --- a/app/src/main/java/org/schabi/newpipe/report/ErrorActivity.java +++ b/app/src/main/java/org/schabi/newpipe/report/ErrorActivity.java @@ -407,9 +407,8 @@ private String buildMarkdown() { .append("\n* __Content Language:__ ").append(getContentLanguageString()) .append("\n* __App Language:__ ").append(getAppLanguage()) .append("\n* __Service:__ ").append(errorInfo.getServiceName()) - .append("\n* __Version:__ ").append(BuildConfig.VERSION_NAME) - .append("\n* __OS:__ ").append(getOsString()).append("\n"); - + .append("\n* __Version:__ ").append(BuildConfig.VERSION_NAME) + .append("\n* __OS:__ ").append(getOsString()).append("\n"); // Collapse all logs to a single paragraph when there are more than one // to keep the GitHub issue clean. diff --git a/app/src/main/java/org/schabi/newpipe/settings/AppearanceSettingsFragment.java b/app/src/main/java/org/schabi/newpipe/settings/AppearanceSettingsFragment.java index a9531693c..ededf965e 100644 --- a/app/src/main/java/org/schabi/newpipe/settings/AppearanceSettingsFragment.java +++ b/app/src/main/java/org/schabi/newpipe/settings/AppearanceSettingsFragment.java @@ -8,6 +8,7 @@ import android.widget.Toast; import androidx.annotation.Nullable; +import androidx.core.app.ActivityCompat; import androidx.preference.Preference; import org.schabi.newpipe.R; @@ -31,7 +32,7 @@ public boolean onPreferenceChange(final Preference preference, final Object newV if (!newValue.equals(startThemeKey) && getActivity() != null) { // If it's not the current theme - getActivity().recreate(); + ActivityCompat.recreate(requireActivity()); } return false; diff --git a/app/src/main/java/org/schabi/newpipe/settings/BasePreferenceFragment.java b/app/src/main/java/org/schabi/newpipe/settings/BasePreferenceFragment.java index 125931ee1..2687da2bf 100644 --- a/app/src/main/java/org/schabi/newpipe/settings/BasePreferenceFragment.java +++ b/app/src/main/java/org/schabi/newpipe/settings/BasePreferenceFragment.java @@ -2,7 +2,7 @@ import android.content.SharedPreferences; import android.os.Bundle; -import android.preference.PreferenceManager; +import androidx.preference.PreferenceManager; import android.view.View; import androidx.annotation.Nullable; @@ -20,7 +20,7 @@ public abstract class BasePreferenceFragment extends PreferenceFragmentCompat { @Override public void onCreate(@Nullable final Bundle savedInstanceState) { - defaultPreferences = PreferenceManager.getDefaultSharedPreferences(getActivity()); + defaultPreferences = PreferenceManager.getDefaultSharedPreferences(requireActivity()); super.onCreate(savedInstanceState); } diff --git a/app/src/main/java/org/schabi/newpipe/settings/ContentSettingsFragment.java b/app/src/main/java/org/schabi/newpipe/settings/ContentSettingsFragment.java index 55bbe6c68..a193ec758 100644 --- a/app/src/main/java/org/schabi/newpipe/settings/ContentSettingsFragment.java +++ b/app/src/main/java/org/schabi/newpipe/settings/ContentSettingsFragment.java @@ -7,13 +7,16 @@ import android.content.Intent; import android.content.SharedPreferences; import android.os.Bundle; -import android.preference.PreferenceManager; +import android.text.InputType; import android.util.Log; +import android.widget.EditText; import android.widget.Toast; import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import androidx.core.content.ContextCompat; import androidx.preference.Preference; +import androidx.preference.PreferenceManager; import com.nononsenseapps.filepicker.Utils; import com.nostra13.universalimageloader.core.ImageLoader; @@ -21,6 +24,7 @@ import org.schabi.newpipe.DownloaderImpl; import org.schabi.newpipe.NewPipeDatabase; import org.schabi.newpipe.R; +import org.schabi.newpipe.ReCaptchaActivity; import org.schabi.newpipe.extractor.NewPipe; import org.schabi.newpipe.extractor.localization.ContentCountry; import org.schabi.newpipe.extractor.localization.Localization; @@ -76,7 +80,23 @@ public void onCreate(@Nullable final Bundle savedInstanceState) { initialSelectedContentCountry = org.schabi.newpipe.util.Localization .getPreferredContentCountry(requireContext()); initialLanguage = PreferenceManager - .getDefaultSharedPreferences(getContext()).getString("app_language_key", "en"); + .getDefaultSharedPreferences(requireContext()).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 @@ -105,15 +125,14 @@ public boolean onPreferenceTreeClick(final Preference preference) { @Override public void onCreatePreferences(final Bundle savedInstanceState, final String rootKey) { - - String homeDir = getActivity().getApplicationInfo().dataDir; - databasesDir = new File(homeDir + "/databases"); - newpipeDb = new File(homeDir + "/databases/newpipe.db"); - newpipeDbJournal = new File(homeDir + "/databases/newpipe.db-journal"); - newpipeDbShm = new File(homeDir + "/databases/newpipe.db-shm"); - newpipeDbWal = new File(homeDir + "/databases/newpipe.db-wal"); - - newpipeSettings = new File(homeDir + "/databases/newpipe.settings"); + final File homeDir = ContextCompat.getDataDir(requireContext()); + databasesDir = new File(homeDir, "/databases"); + newpipeDb = new File(homeDir, "/databases/newpipe.db"); + newpipeDbJournal = new File(homeDir, "/databases/newpipe.db-journal"); + newpipeDbShm = new File(homeDir, "/databases/newpipe.db-shm"); + newpipeDbWal = new File(homeDir, "/databases/newpipe.db-wal"); + + newpipeSettings = new File(homeDir, "/databases/newpipe.settings"); newpipeSettings.delete(); addPreferencesFromResource(R.xml.content_settings); @@ -150,7 +169,7 @@ public void onDestroy() { final ContentCountry selectedContentCountry = org.schabi.newpipe.util.Localization .getPreferredContentCountry(requireContext()); final String selectedLanguage = PreferenceManager - .getDefaultSharedPreferences(getContext()).getString("app_language_key", "en"); + .getDefaultSharedPreferences(requireContext()).getString("app_language_key", "en"); if (!selectedLocalization.equals(initialSelectedLocalization) || !selectedContentCountry.equals(initialSelectedContentCountry) @@ -178,8 +197,7 @@ public void onActivityResult(final int requestCode, final int resultCode, && resultCode == Activity.RESULT_OK && data.getData() != null) { String path = Utils.getFileForUri(data.getData()).getAbsolutePath(); if (requestCode == REQUEST_EXPORT_PATH) { - SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMdd_HHmmss", Locale.US); - exportDatabase(path + "/NewPipeData-" + sdf.format(new Date()) + ".zip"); + showExportDialog(requireActivity(), path); } else { AlertDialog.Builder builder = new AlertDialog.Builder(getActivity()); builder.setMessage(R.string.override_current_data) @@ -192,6 +210,33 @@ public void onActivityResult(final int requestCode, final int resultCode, } } + private void showExportDialog(final Context c, final String path) { + final EditText fileNameET = new EditText(c); + + final SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMdd_HHmmss", Locale.US); + fileNameET.setText("NewPipeData-" + sdf.format(new Date()) + ".zip"); + + fileNameET.setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_URI); + fileNameET.setHint(R.string.export_data_file_name_hint); + + final AlertDialog dialog = new AlertDialog.Builder(c) + .setTitle(R.string.export_data_file_name) + .setIcon(R.drawable.ic_import_export_white_24dp) + .setNegativeButton(R.string.cancel, null) + .setPositiveButton(R.string.export_button, (dialog1, which) -> { + final String fileName = fileNameET.getText().toString(); + if (fileName.matches("[-_. A-Za-z0-9]+\\.zip")) { + exportDatabase(path + "/" + fileName); + } else { + Toast.makeText(getContext(), R.string.no_valid_zip_file_name, + Toast.LENGTH_SHORT).show(); + } + }) + .create(); + dialog.setView(fileNameET, 50, 0, 50, 0); + dialog.show(); + } + private void exportDatabase(final String path) { try { //checkpoint before export @@ -218,7 +263,7 @@ private void saveSharedPreferencesToFile(final File dst) { ObjectOutputStream output = null; try { output = new ObjectOutputStream(new FileOutputStream(dst)); - SharedPreferences pref = PreferenceManager.getDefaultSharedPreferences(getContext()); + SharedPreferences pref = PreferenceManager.getDefaultSharedPreferences(requireContext()); output.writeObject(pref.getAll()); } catch (FileNotFoundException e) { @@ -302,7 +347,7 @@ private void loadSharedPreferences(final File src) { try { input = new ObjectInputStream(new FileInputStream(src)); SharedPreferences.Editor prefEdit = PreferenceManager - .getDefaultSharedPreferences(getContext()).edit(); + .getDefaultSharedPreferences(requireContext()).edit(); prefEdit.clear(); Map entries = (Map) input.readObject(); for (Map.Entry entry : entries.entrySet()) { diff --git a/app/src/main/java/org/schabi/newpipe/settings/PeertubeInstanceListFragment.java b/app/src/main/java/org/schabi/newpipe/settings/PeertubeInstanceListFragment.java index 7435880df..cfca77efb 100644 --- a/app/src/main/java/org/schabi/newpipe/settings/PeertubeInstanceListFragment.java +++ b/app/src/main/java/org/schabi/newpipe/settings/PeertubeInstanceListFragment.java @@ -4,7 +4,7 @@ import android.content.Context; import android.content.SharedPreferences; import android.os.Bundle; -import android.preference.PreferenceManager; +import androidx.preference.PreferenceManager; import android.text.InputType; import android.view.LayoutInflater; import android.view.Menu; diff --git a/app/src/main/java/org/schabi/newpipe/settings/VideoAudioSettingsFragment.java b/app/src/main/java/org/schabi/newpipe/settings/VideoAudioSettingsFragment.java index bef9a7b56..86f5ef2d6 100644 --- a/app/src/main/java/org/schabi/newpipe/settings/VideoAudioSettingsFragment.java +++ b/app/src/main/java/org/schabi/newpipe/settings/VideoAudioSettingsFragment.java @@ -16,11 +16,14 @@ import org.schabi.newpipe.R; import org.schabi.newpipe.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/newpipe/settings/tabs/TabsManager.java b/app/src/main/java/org/schabi/newpipe/settings/tabs/TabsManager.java index c76df7047..8f7a995b3 100644 --- a/app/src/main/java/org/schabi/newpipe/settings/tabs/TabsManager.java +++ b/app/src/main/java/org/schabi/newpipe/settings/tabs/TabsManager.java @@ -2,7 +2,7 @@ import android.content.Context; import android.content.SharedPreferences; -import android.preference.PreferenceManager; +import androidx.preference.PreferenceManager; import android.widget.Toast; import org.schabi.newpipe.R; diff --git a/app/src/main/java/org/schabi/newpipe/util/ExceptionUtils.kt b/app/src/main/java/org/schabi/newpipe/util/ExceptionUtils.kt index 528912ceb..0addb26fb 100644 --- a/app/src/main/java/org/schabi/newpipe/util/ExceptionUtils.kt +++ b/app/src/main/java/org/schabi/newpipe/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,8 +22,10 @@ class ExceptionUtils { */ @JvmStatic fun isNetworkRelated(throwable: Throwable): Boolean { - return hasAssignableCause(throwable, - IOException::class.java) + return hasAssignableCause( + throwable, + IOException::class.java + ) } /** diff --git a/app/src/main/java/org/schabi/newpipe/util/ExtractorHelper.java b/app/src/main/java/org/schabi/newpipe/util/ExtractorHelper.java index a23bce425..edd4cc16b 100644 --- a/app/src/main/java/org/schabi/newpipe/util/ExtractorHelper.java +++ b/app/src/main/java/org/schabi/newpipe/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; @@ -289,6 +295,21 @@ 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) { diff --git a/app/src/main/java/org/schabi/newpipe/util/FilenameUtils.java b/app/src/main/java/org/schabi/newpipe/util/FilenameUtils.java index 3179662ba..e7174f2bd 100644 --- a/app/src/main/java/org/schabi/newpipe/util/FilenameUtils.java +++ b/app/src/main/java/org/schabi/newpipe/util/FilenameUtils.java @@ -2,7 +2,7 @@ import android.content.Context; import android.content.SharedPreferences; -import android.preference.PreferenceManager; +import androidx.preference.PreferenceManager; import org.schabi.newpipe.R; diff --git a/app/src/main/java/org/schabi/newpipe/util/KioskTranslator.java b/app/src/main/java/org/schabi/newpipe/util/KioskTranslator.java index b676a1a88..a42d4913f 100644 --- a/app/src/main/java/org/schabi/newpipe/util/KioskTranslator.java +++ b/app/src/main/java/org/schabi/newpipe/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/newpipe/util/ListHelper.java b/app/src/main/java/org/schabi/newpipe/util/ListHelper.java index 189b6823e..8fe411870 100644 --- a/app/src/main/java/org/schabi/newpipe/util/ListHelper.java +++ b/app/src/main/java/org/schabi/newpipe/util/ListHelper.java @@ -3,7 +3,7 @@ import android.content.Context; import android.content.SharedPreferences; import android.net.ConnectivityManager; -import android.preference.PreferenceManager; +import androidx.preference.PreferenceManager; import androidx.annotation.Nullable; import androidx.annotation.StringRes; diff --git a/app/src/main/java/org/schabi/newpipe/util/Localization.java b/app/src/main/java/org/schabi/newpipe/util/Localization.java index 7e336f02d..f7301c275 100644 --- a/app/src/main/java/org/schabi/newpipe/util/Localization.java +++ b/app/src/main/java/org/schabi/newpipe/util/Localization.java @@ -5,7 +5,9 @@ import android.content.SharedPreferences; import android.content.res.Configuration; import android.content.res.Resources; -import android.preference.PreferenceManager; +import android.icu.text.CompactDecimalFormat; +import android.os.Build; +import androidx.preference.PreferenceManager; import android.text.TextUtils; import android.util.DisplayMetrics; @@ -184,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/newpipe/util/NavigationHelper.java b/app/src/main/java/org/schabi/newpipe/util/NavigationHelper.java index ccaa79f98..1a83318cb 100644 --- a/app/src/main/java/org/schabi/newpipe/util/NavigationHelper.java +++ b/app/src/main/java/org/schabi/newpipe/util/NavigationHelper.java @@ -7,7 +7,7 @@ import android.content.Intent; import android.net.Uri; import android.os.Build; -import android.preference.PreferenceManager; +import androidx.preference.PreferenceManager; import android.util.Log; import android.widget.Toast; @@ -52,6 +52,7 @@ import org.schabi.newpipe.player.MainVideoPlayer; import org.schabi.newpipe.player.PopupVideoPlayer; import org.schabi.newpipe.player.PopupVideoPlayerActivity; +import org.schabi.newpipe.player.ServicePlayerActivity; import org.schabi.newpipe.player.VideoPlayer; import org.schabi.newpipe.player.playqueue.PlayQueue; import org.schabi.newpipe.settings.SettingsActivity; @@ -124,7 +125,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 +134,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 +147,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 +485,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/newpipe/util/PeertubeHelper.java b/app/src/main/java/org/schabi/newpipe/util/PeertubeHelper.java index e89cbf5db..69bbc05a2 100644 --- a/app/src/main/java/org/schabi/newpipe/util/PeertubeHelper.java +++ b/app/src/main/java/org/schabi/newpipe/util/PeertubeHelper.java @@ -2,7 +2,7 @@ import android.content.Context; import android.content.SharedPreferences; -import android.preference.PreferenceManager; +import androidx.preference.PreferenceManager; import com.grack.nanojson.JsonArray; import com.grack.nanojson.JsonObject; diff --git a/app/src/main/java/org/schabi/newpipe/util/ServiceHelper.java b/app/src/main/java/org/schabi/newpipe/util/ServiceHelper.java index 668895222..71480fe15 100644 --- a/app/src/main/java/org/schabi/newpipe/util/ServiceHelper.java +++ b/app/src/main/java/org/schabi/newpipe/util/ServiceHelper.java @@ -2,7 +2,7 @@ import android.content.Context; import android.content.SharedPreferences; -import android.preference.PreferenceManager; +import androidx.preference.PreferenceManager; import androidx.annotation.DrawableRes; import androidx.annotation.StringRes; @@ -38,6 +38,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 +50,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": diff --git a/app/src/main/java/org/schabi/newpipe/util/ThemeHelper.java b/app/src/main/java/org/schabi/newpipe/util/ThemeHelper.java index 74ea34fcc..f78450883 100644 --- a/app/src/main/java/org/schabi/newpipe/util/ThemeHelper.java +++ b/app/src/main/java/org/schabi/newpipe/util/ThemeHelper.java @@ -21,7 +21,7 @@ import android.content.Context; import android.content.res.TypedArray; -import android.preference.PreferenceManager; +import androidx.preference.PreferenceManager; import android.util.TypedValue; import android.view.ContextThemeWrapper; diff --git a/app/src/main/java/org/schabi/newpipe/views/FocusOverlayView.java b/app/src/main/java/org/schabi/newpipe/views/FocusOverlayView.java index bd5ae10e8..4f26de544 100644 --- a/app/src/main/java/org/schabi/newpipe/views/FocusOverlayView.java +++ b/app/src/main/java/org/schabi/newpipe/views/FocusOverlayView.java @@ -246,7 +246,7 @@ private static void fixFocusHierarchy(final View decor) { // keyboard META key for moving between clusters). We have to fix this unfortunate accident // While we are at it, let's deal with touchscreenBlocksFocus too. - if (Build.VERSION.SDK_INT < 26) { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) { return; } diff --git a/app/src/main/java/org/schabi/newpipe/views/NewPipeRecyclerView.java b/app/src/main/java/org/schabi/newpipe/views/NewPipeRecyclerView.java index 655b86818..1f2d5de8f 100644 --- a/app/src/main/java/org/schabi/newpipe/views/NewPipeRecyclerView.java +++ b/app/src/main/java/org/schabi/newpipe/views/NewPipeRecyclerView.java @@ -142,7 +142,7 @@ public boolean dispatchUnhandledMove(final View focused, final int direction) { } private boolean tryFocusFinder(final int direction) { - if (Build.VERSION.SDK_INT >= 28) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { // Android 9 implemented bunch of handy changes to focus, that render code below less // useful, and also broke findNextFocusFromRect in way, that render this hack useless return false; diff --git a/app/src/main/java/us/shandian/giga/get/DownloadInitializer.java b/app/src/main/java/us/shandian/giga/get/DownloadInitializer.java index 618200f27..3d509eed7 100644 --- a/app/src/main/java/us/shandian/giga/get/DownloadInitializer.java +++ b/app/src/main/java/us/shandian/giga/get/DownloadInitializer.java @@ -54,12 +54,12 @@ public void run() { long lowestSize = Long.MAX_VALUE; for (int i = 0; i < mMission.urls.length && mMission.running; i++) { - mConn = mMission.openConnection(mMission.urls[i], true, -1, -1); + mConn = mMission.openConnection(mMission.urls[i], true, 0, 0); mMission.establishConnection(mId, mConn); dispose(); if (Thread.interrupted()) return; - long length = Utility.getContentLength(mConn); + long length = Utility.getTotalContentLength(mConn); if (i == 0) { httpCode = mConn.getResponseCode(); @@ -84,14 +84,14 @@ public void run() { } } else { // ask for the current resource length - mConn = mMission.openConnection(true, -1, -1); + mConn = mMission.openConnection(true, 0, 0); mMission.establishConnection(mId, mConn); dispose(); if (!mMission.running || Thread.interrupted()) return; httpCode = mConn.getResponseCode(); - mMission.length = Utility.getContentLength(mConn); + mMission.length = Utility.getTotalContentLength(mConn); } if (mMission.length == 0 || httpCode == 204) { 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 b110d038b..f088991bd 100644 --- a/app/src/main/java/us/shandian/giga/get/DownloadMissionRecover.java +++ b/app/src/main/java/us/shandian/giga/get/DownloadMissionRecover.java @@ -1,313 +1,313 @@ -package us.shandian.giga.get; - -import android.util.Log; - -import org.schabi.newpipe.extractor.NewPipe; -import org.schabi.newpipe.extractor.StreamingService; -import org.schabi.newpipe.extractor.exceptions.ExtractionException; -import org.schabi.newpipe.extractor.stream.AudioStream; -import org.schabi.newpipe.extractor.stream.StreamExtractor; -import org.schabi.newpipe.extractor.stream.SubtitlesStream; -import org.schabi.newpipe.extractor.stream.VideoStream; - -import java.io.IOException; -import java.io.InterruptedIOException; -import java.net.HttpURLConnection; -import java.nio.channels.ClosedByInterruptException; -import java.util.List; - -import us.shandian.giga.get.DownloadMission.HttpError; - -import static us.shandian.giga.get.DownloadMission.ERROR_RESOURCE_GONE; - -public class DownloadMissionRecover extends Thread { - private static final String TAG = "DownloadMissionRecover"; - static final int mID = -3; - - private final DownloadMission mMission; - private final boolean mNotInitialized; - - private final int mErrCode; - - private HttpURLConnection mConn; - private MissionRecoveryInfo mRecovery; - private StreamExtractor mExtractor; - - DownloadMissionRecover(DownloadMission mission, int errCode) { - mMission = mission; - mNotInitialized = mission.blocks == null && mission.current == 0; - mErrCode = errCode; - } - - @Override - public void run() { - if (mMission.source == null) { - mMission.notifyError(mErrCode, null); - return; - } - - Exception err = null; - int attempt = 0; - - while (attempt++ < mMission.maxRetry) { - try { - tryRecover(); - return; - } catch (InterruptedIOException | ClosedByInterruptException e) { - return; - } catch (Exception e) { - if (!mMission.running || super.isInterrupted()) return; - err = e; - } - } - - // give up - mMission.notifyError(mErrCode, err); - } - - private void tryRecover() throws ExtractionException, IOException, HttpError { - if (mExtractor == null) { - try { - StreamingService svr = NewPipe.getServiceByUrl(mMission.source); - mExtractor = svr.getStreamExtractor(mMission.source); - mExtractor.fetchPage(); - } catch (ExtractionException e) { - mExtractor = null; - throw e; - } - } - - // maybe the following check is redundant - if (!mMission.running || super.isInterrupted()) return; - - if (!mNotInitialized) { - // set the current download url to null in case if the recovery - // process is canceled. Next time start() method is called the - // recovery will be executed, saving time - mMission.urls[mMission.current] = null; - - mRecovery = mMission.recoveryInfo[mMission.current]; - resolveStream(); - return; - } - - Log.w(TAG, "mission is not fully initialized, this will take a while"); - - try { - for (; mMission.current < mMission.urls.length; mMission.current++) { - mRecovery = mMission.recoveryInfo[mMission.current]; - - if (test()) continue; - if (!mMission.running) return; - - resolveStream(); - if (!mMission.running) return; - - // before continue, check if the current stream was resolved - if (mMission.urls[mMission.current] == null) { - break; - } - } - } finally { - mMission.current = 0; - } - - mMission.writeThisToFile(); - - if (!mMission.running || super.isInterrupted()) return; - - mMission.running = false; - mMission.start(); - } - - private void resolveStream() throws IOException, ExtractionException, HttpError { - // FIXME: this getErrorMessage() always returns "video is unavailable" - /*if (mExtractor.getErrorMessage() != null) { - mMission.notifyError(mErrCode, new ExtractionException(mExtractor.getErrorMessage())); - return; - }*/ - - String url = null; - - switch (mRecovery.kind) { - case 'a': - for (AudioStream audio : mExtractor.getAudioStreams()) { - if (audio.getAverageBitrate() == mRecovery.desiredBitrate && audio.getFormat() == mRecovery.format) { - url = audio.getUrl(); - break; - } - } - break; - case 'v': - List videoStreams; - if (mRecovery.desired2) - videoStreams = mExtractor.getVideoOnlyStreams(); - else - videoStreams = mExtractor.getVideoStreams(); - for (VideoStream video : videoStreams) { - if (video.resolution.equals(mRecovery.desired) && video.getFormat() == mRecovery.format) { - url = video.getUrl(); - break; - } - } - break; - case 's': - for (SubtitlesStream subtitles : mExtractor.getSubtitles(mRecovery.format)) { - String tag = subtitles.getLanguageTag(); - if (tag.equals(mRecovery.desired) && subtitles.isAutoGenerated() == mRecovery.desired2) { - url = subtitles.getUrl(); - break; - } - } - break; - default: - throw new RuntimeException("Unknown stream type"); - } - - resolve(url); - } - - private void resolve(String url) throws IOException, HttpError { - if (mRecovery.validateCondition == null) { - Log.w(TAG, "validation condition not defined, the resource can be stale"); - } - - if (mMission.unknownLength || mRecovery.validateCondition == null) { - recover(url, false); - return; - } - - /////////////////////////////////////////////////////////////////////// - ////// Validate the http resource doing a range request - ///////////////////// - try { - mConn = mMission.openConnection(url, true, mMission.length - 10, mMission.length); - mConn.setRequestProperty("If-Range", mRecovery.validateCondition); - mMission.establishConnection(mID, mConn); - - int code = mConn.getResponseCode(); - - switch (code) { - case 200: - case 413: - // stale - recover(url, true); - return; - case 206: - // in case of validation using the Last-Modified date, check the resource length - long[] contentRange = parseContentRange(mConn.getHeaderField("Content-Range")); - boolean lengthMismatch = contentRange[2] != -1 && contentRange[2] != mMission.length; - - recover(url, lengthMismatch); - return; - } - - throw new HttpError(code); - } finally { - disconnect(); - } - } - - private void recover(String url, boolean stale) { - Log.i(TAG, - String.format("recover() name=%s isStale=%s url=%s", mMission.storage.getName(), stale, url) - ); - - mMission.urls[mMission.current] = url; - - if (url == null) { - mMission.urls = new String[0]; - mMission.notifyError(ERROR_RESOURCE_GONE, null); - return; - } - - if (mNotInitialized) return; - - if (stale) { - mMission.resetState(false, false, DownloadMission.ERROR_NOTHING); - } - - mMission.writeThisToFile(); - - if (!mMission.running || super.isInterrupted()) return; - - mMission.running = false; - mMission.start(); - } - - private long[] parseContentRange(String value) { - long[] range = new long[3]; - - if (value == null) { - // this never should happen - return range; - } - - try { - value = value.trim(); - - if (!value.startsWith("bytes")) { - return range;// unknown range type - } - - int space = value.lastIndexOf(' ') + 1; - int dash = value.indexOf('-', space) + 1; - int bar = value.indexOf('/', dash); - - // start - range[0] = Long.parseLong(value.substring(space, dash - 1)); - - // end - range[1] = Long.parseLong(value.substring(dash, bar)); - - // resource length - value = value.substring(bar + 1); - if (value.equals("*")) { - range[2] = -1;// unknown length received from the server but should be valid - } else { - range[2] = Long.parseLong(value); - } - } catch (Exception e) { - // nothing to do - } - - return range; - } - - private boolean test() { - if (mMission.urls[mMission.current] == null) return false; - - try { - mConn = mMission.openConnection(mMission.urls[mMission.current], true, -1, -1); - mMission.establishConnection(mID, mConn); - - if (mConn.getResponseCode() == 200) return true; - } catch (Exception e) { - // nothing to do - } finally { - disconnect(); - } - - return false; - } - - private void disconnect() { - try { - try { - mConn.getInputStream().close(); - } finally { - mConn.disconnect(); - } - } catch (Exception e) { - // nothing to do - } finally { - mConn = null; - } - } - - @Override - public void interrupt() { - super.interrupt(); - if (mConn != null) disconnect(); - } -} +package us.shandian.giga.get; + +import android.util.Log; + +import org.schabi.newpipe.extractor.NewPipe; +import org.schabi.newpipe.extractor.StreamingService; +import org.schabi.newpipe.extractor.exceptions.ExtractionException; +import org.schabi.newpipe.extractor.stream.AudioStream; +import org.schabi.newpipe.extractor.stream.StreamExtractor; +import org.schabi.newpipe.extractor.stream.SubtitlesStream; +import org.schabi.newpipe.extractor.stream.VideoStream; + +import java.io.IOException; +import java.io.InterruptedIOException; +import java.net.HttpURLConnection; +import java.nio.channels.ClosedByInterruptException; +import java.util.List; + +import us.shandian.giga.get.DownloadMission.HttpError; + +import static us.shandian.giga.get.DownloadMission.ERROR_RESOURCE_GONE; + +public class DownloadMissionRecover extends Thread { + private static final String TAG = "DownloadMissionRecover"; + static final int mID = -3; + + private final DownloadMission mMission; + private final boolean mNotInitialized; + + private final int mErrCode; + + private HttpURLConnection mConn; + private MissionRecoveryInfo mRecovery; + private StreamExtractor mExtractor; + + DownloadMissionRecover(DownloadMission mission, int errCode) { + mMission = mission; + mNotInitialized = mission.blocks == null && mission.current == 0; + mErrCode = errCode; + } + + @Override + public void run() { + if (mMission.source == null) { + mMission.notifyError(mErrCode, null); + return; + } + + Exception err = null; + int attempt = 0; + + while (attempt++ < mMission.maxRetry) { + try { + tryRecover(); + return; + } catch (InterruptedIOException | ClosedByInterruptException e) { + return; + } catch (Exception e) { + if (!mMission.running || super.isInterrupted()) return; + err = e; + } + } + + // give up + mMission.notifyError(mErrCode, err); + } + + private void tryRecover() throws ExtractionException, IOException, HttpError { + if (mExtractor == null) { + try { + StreamingService svr = NewPipe.getServiceByUrl(mMission.source); + mExtractor = svr.getStreamExtractor(mMission.source); + mExtractor.fetchPage(); + } catch (ExtractionException e) { + mExtractor = null; + throw e; + } + } + + // maybe the following check is redundant + if (!mMission.running || super.isInterrupted()) return; + + if (!mNotInitialized) { + // set the current download url to null in case if the recovery + // process is canceled. Next time start() method is called the + // recovery will be executed, saving time + mMission.urls[mMission.current] = null; + + mRecovery = mMission.recoveryInfo[mMission.current]; + resolveStream(); + return; + } + + Log.w(TAG, "mission is not fully initialized, this will take a while"); + + try { + for (; mMission.current < mMission.urls.length; mMission.current++) { + mRecovery = mMission.recoveryInfo[mMission.current]; + + if (test()) continue; + if (!mMission.running) return; + + resolveStream(); + if (!mMission.running) return; + + // before continue, check if the current stream was resolved + if (mMission.urls[mMission.current] == null) { + break; + } + } + } finally { + mMission.current = 0; + } + + mMission.writeThisToFile(); + + if (!mMission.running || super.isInterrupted()) return; + + mMission.running = false; + mMission.start(); + } + + private void resolveStream() throws IOException, ExtractionException, HttpError { + // FIXME: this getErrorMessage() always returns "video is unavailable" + /*if (mExtractor.getErrorMessage() != null) { + mMission.notifyError(mErrCode, new ExtractionException(mExtractor.getErrorMessage())); + return; + }*/ + + String url = null; + + switch (mRecovery.kind) { + case 'a': + for (AudioStream audio : mExtractor.getAudioStreams()) { + if (audio.getAverageBitrate() == mRecovery.desiredBitrate && audio.getFormat() == mRecovery.format) { + url = audio.getUrl(); + break; + } + } + break; + case 'v': + List videoStreams; + if (mRecovery.desired2) + videoStreams = mExtractor.getVideoOnlyStreams(); + else + videoStreams = mExtractor.getVideoStreams(); + for (VideoStream video : videoStreams) { + if (video.resolution.equals(mRecovery.desired) && video.getFormat() == mRecovery.format) { + url = video.getUrl(); + break; + } + } + break; + case 's': + for (SubtitlesStream subtitles : mExtractor.getSubtitles(mRecovery.format)) { + String tag = subtitles.getLanguageTag(); + if (tag.equals(mRecovery.desired) && subtitles.isAutoGenerated() == mRecovery.desired2) { + url = subtitles.getUrl(); + break; + } + } + break; + default: + throw new RuntimeException("Unknown stream type"); + } + + resolve(url); + } + + private void resolve(String url) throws IOException, HttpError { + if (mRecovery.validateCondition == null) { + Log.w(TAG, "validation condition not defined, the resource can be stale"); + } + + if (mMission.unknownLength || mRecovery.validateCondition == null) { + recover(url, false); + return; + } + + /////////////////////////////////////////////////////////////////////// + ////// Validate the http resource doing a range request + ///////////////////// + try { + mConn = mMission.openConnection(url, true, mMission.length - 10, mMission.length); + mConn.setRequestProperty("If-Range", mRecovery.validateCondition); + mMission.establishConnection(mID, mConn); + + int code = mConn.getResponseCode(); + + switch (code) { + case 200: + case 413: + // stale + recover(url, true); + return; + case 206: + // in case of validation using the Last-Modified date, check the resource length + long[] contentRange = parseContentRange(mConn.getHeaderField("Content-Range")); + boolean lengthMismatch = contentRange[2] != -1 && contentRange[2] != mMission.length; + + recover(url, lengthMismatch); + return; + } + + throw new HttpError(code); + } finally { + disconnect(); + } + } + + private void recover(String url, boolean stale) { + Log.i(TAG, + String.format("recover() name=%s isStale=%s url=%s", mMission.storage.getName(), stale, url) + ); + + mMission.urls[mMission.current] = url; + + if (url == null) { + mMission.urls = new String[0]; + mMission.notifyError(ERROR_RESOURCE_GONE, null); + return; + } + + if (mNotInitialized) return; + + if (stale) { + mMission.resetState(false, false, DownloadMission.ERROR_NOTHING); + } + + mMission.writeThisToFile(); + + if (!mMission.running || super.isInterrupted()) return; + + mMission.running = false; + mMission.start(); + } + + private long[] parseContentRange(String value) { + long[] range = new long[3]; + + if (value == null) { + // this never should happen + return range; + } + + try { + value = value.trim(); + + if (!value.startsWith("bytes")) { + return range;// unknown range type + } + + int space = value.lastIndexOf(' ') + 1; + int dash = value.indexOf('-', space) + 1; + int bar = value.indexOf('/', dash); + + // start + range[0] = Long.parseLong(value.substring(space, dash - 1)); + + // end + range[1] = Long.parseLong(value.substring(dash, bar)); + + // resource length + value = value.substring(bar + 1); + if (value.equals("*")) { + range[2] = -1;// unknown length received from the server but should be valid + } else { + range[2] = Long.parseLong(value); + } + } catch (Exception e) { + // nothing to do + } + + return range; + } + + private boolean test() { + if (mMission.urls[mMission.current] == null) return false; + + try { + mConn = mMission.openConnection(mMission.urls[mMission.current], true, -1, -1); + mMission.establishConnection(mID, mConn); + + if (mConn.getResponseCode() == 200) return true; + } catch (Exception e) { + // nothing to do + } finally { + disconnect(); + } + + return false; + } + + private void disconnect() { + try { + try { + mConn.getInputStream().close(); + } finally { + mConn.disconnect(); + } + } catch (Exception e) { + // nothing to do + } finally { + mConn = null; + } + } + + @Override + public void interrupt() { + super.interrupt(); + if (mConn != null) disconnect(); + } +} diff --git a/app/src/main/java/us/shandian/giga/get/FinishedMission.java b/app/src/main/java/us/shandian/giga/get/FinishedMission.java index 6bc5423b8..29f3c6296 100644 --- a/app/src/main/java/us/shandian/giga/get/FinishedMission.java +++ b/app/src/main/java/us/shandian/giga/get/FinishedMission.java @@ -1,18 +1,18 @@ -package us.shandian.giga.get; - -import androidx.annotation.NonNull; - -public class FinishedMission extends Mission { - - public FinishedMission() { - } - - public FinishedMission(@NonNull DownloadMission mission) { - source = mission.source; - length = mission.length; - timestamp = mission.timestamp; - kind = mission.kind; - storage = mission.storage; - } - -} +package us.shandian.giga.get; + +import androidx.annotation.NonNull; + +public class FinishedMission extends Mission { + + public FinishedMission() { + } + + public FinishedMission(@NonNull DownloadMission mission) { + source = mission.source; + length = mission.length; + timestamp = mission.timestamp; + kind = mission.kind; + storage = mission.storage; + } + +} diff --git a/app/src/main/java/us/shandian/giga/get/Mission.java b/app/src/main/java/us/shandian/giga/get/Mission.java index 8e814a2af..81cddb4ae 100644 --- a/app/src/main/java/us/shandian/giga/get/Mission.java +++ b/app/src/main/java/us/shandian/giga/get/Mission.java @@ -1,60 +1,60 @@ -package us.shandian.giga.get; - -import androidx.annotation.NonNull; - -import java.io.Serializable; -import java.util.Calendar; - -import us.shandian.giga.io.StoredFileHelper; - -public abstract class Mission implements Serializable { - private static final long serialVersionUID = 1L;// last bump: 27 march 2019 - - /** - * Source url of the resource - */ - public String source; - - /** - * Length of the current resource - */ - public long length; - - /** - * creation timestamp (and maybe unique identifier) - */ - public long timestamp; - - /** - * pre-defined content type - */ - public char kind; - - /** - * The downloaded file - */ - public StoredFileHelper storage; - - /** - * Delete the downloaded file - * - * @return {@code true] if and only if the file is successfully deleted, otherwise, {@code false} - */ - public boolean delete() { - if (storage != null) return storage.delete(); - return true; - } - - /** - * Indicate if this mission is deleted whatever is stored - */ - public transient boolean deleted = false; - - @NonNull - @Override - public String toString() { - Calendar calendar = Calendar.getInstance(); - calendar.setTimeInMillis(timestamp); - return "[" + calendar.getTime().toString() + "] " + (storage.isInvalid() ? storage.getName() : storage.getUri()); - } -} +package us.shandian.giga.get; + +import androidx.annotation.NonNull; + +import java.io.Serializable; +import java.util.Calendar; + +import us.shandian.giga.io.StoredFileHelper; + +public abstract class Mission implements Serializable { + private static final long serialVersionUID = 1L;// last bump: 27 march 2019 + + /** + * Source url of the resource + */ + public String source; + + /** + * Length of the current resource + */ + public long length; + + /** + * creation timestamp (and maybe unique identifier) + */ + public long timestamp; + + /** + * pre-defined content type + */ + public char kind; + + /** + * The downloaded file + */ + public StoredFileHelper storage; + + /** + * Delete the downloaded file + * + * @return {@code true] if and only if the file is successfully deleted, otherwise, {@code false} + */ + public boolean delete() { + if (storage != null) return storage.delete(); + return true; + } + + /** + * Indicate if this mission is deleted whatever is stored + */ + public transient boolean deleted = false; + + @NonNull + @Override + public String toString() { + Calendar calendar = Calendar.getInstance(); + calendar.setTimeInMillis(timestamp); + return "[" + calendar.getTime().toString() + "] " + (storage.isInvalid() ? storage.getName() : storage.getUri()); + } +} 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 c1912351a..b8304cd6b 100644 --- a/app/src/main/java/us/shandian/giga/get/MissionRecoveryInfo.java +++ b/app/src/main/java/us/shandian/giga/get/MissionRecoveryInfo.java @@ -1,115 +1,115 @@ -package us.shandian.giga.get; - -import android.os.Parcel; -import android.os.Parcelable; - -import androidx.annotation.NonNull; - -import org.schabi.newpipe.extractor.MediaFormat; -import org.schabi.newpipe.extractor.stream.AudioStream; -import org.schabi.newpipe.extractor.stream.Stream; -import org.schabi.newpipe.extractor.stream.SubtitlesStream; -import org.schabi.newpipe.extractor.stream.VideoStream; - -import java.io.Serializable; - -public class MissionRecoveryInfo implements Serializable, Parcelable { - private static final long serialVersionUID = 0L; - - MediaFormat format; - String desired; - boolean desired2; - int desiredBitrate; - byte kind; - String validateCondition = null; - - public MissionRecoveryInfo(@NonNull Stream stream) { - if (stream instanceof AudioStream) { - desiredBitrate = ((AudioStream) stream).getAverageBitrate(); - desired2 = false; - kind = 'a'; - } else if (stream instanceof VideoStream) { - desired = ((VideoStream) stream).getResolution(); - desired2 = ((VideoStream) stream).isVideoOnly(); - kind = 'v'; - } else if (stream instanceof SubtitlesStream) { - desired = ((SubtitlesStream) stream).getLanguageTag(); - desired2 = ((SubtitlesStream) stream).isAutoGenerated(); - kind = 's'; - } else { - throw new RuntimeException("Unknown stream kind"); - } - - format = stream.getFormat(); - if (format == null) throw new NullPointerException("Stream format cannot be null"); - } - - @NonNull - @Override - public String toString() { - String info; - StringBuilder str = new StringBuilder(); - str.append("{type="); - switch (kind) { - case 'a': - str.append("audio"); - info = "bitrate=" + desiredBitrate; - break; - case 'v': - str.append("video"); - info = "quality=" + desired + " videoOnly=" + desired2; - break; - case 's': - str.append("subtitles"); - info = "language=" + desired + " autoGenerated=" + desired2; - break; - default: - info = ""; - str.append("other"); - } - - str.append(" format=") - .append(format.getName()) - .append(' ') - .append(info) - .append('}'); - - return str.toString(); - } - - @Override - public int describeContents() { - return 0; - } - - @Override - public void writeToParcel(Parcel parcel, int flags) { - parcel.writeInt(this.format.ordinal()); - parcel.writeString(this.desired); - parcel.writeInt(this.desired2 ? 0x01 : 0x00); - parcel.writeInt(this.desiredBitrate); - parcel.writeByte(this.kind); - parcel.writeString(this.validateCondition); - } - - private MissionRecoveryInfo(Parcel parcel) { - this.format = MediaFormat.values()[parcel.readInt()]; - this.desired = parcel.readString(); - this.desired2 = parcel.readInt() != 0x00; - this.desiredBitrate = parcel.readInt(); - this.kind = parcel.readByte(); - this.validateCondition = parcel.readString(); - } - - public static final Parcelable.Creator CREATOR = new Parcelable.Creator() { - @Override - public MissionRecoveryInfo createFromParcel(Parcel source) { - return new MissionRecoveryInfo(source); - } - - @Override - public MissionRecoveryInfo[] newArray(int size) { - return new MissionRecoveryInfo[size]; - } - }; -} +package us.shandian.giga.get; + +import android.os.Parcel; +import android.os.Parcelable; + +import androidx.annotation.NonNull; + +import org.schabi.newpipe.extractor.MediaFormat; +import org.schabi.newpipe.extractor.stream.AudioStream; +import org.schabi.newpipe.extractor.stream.Stream; +import org.schabi.newpipe.extractor.stream.SubtitlesStream; +import org.schabi.newpipe.extractor.stream.VideoStream; + +import java.io.Serializable; + +public class MissionRecoveryInfo implements Serializable, Parcelable { + private static final long serialVersionUID = 0L; + + MediaFormat format; + String desired; + boolean desired2; + int desiredBitrate; + byte kind; + String validateCondition = null; + + public MissionRecoveryInfo(@NonNull Stream stream) { + if (stream instanceof AudioStream) { + desiredBitrate = ((AudioStream) stream).getAverageBitrate(); + desired2 = false; + kind = 'a'; + } else if (stream instanceof VideoStream) { + desired = ((VideoStream) stream).getResolution(); + desired2 = ((VideoStream) stream).isVideoOnly(); + kind = 'v'; + } else if (stream instanceof SubtitlesStream) { + desired = ((SubtitlesStream) stream).getLanguageTag(); + desired2 = ((SubtitlesStream) stream).isAutoGenerated(); + kind = 's'; + } else { + throw new RuntimeException("Unknown stream kind"); + } + + format = stream.getFormat(); + if (format == null) throw new NullPointerException("Stream format cannot be null"); + } + + @NonNull + @Override + public String toString() { + String info; + StringBuilder str = new StringBuilder(); + str.append("{type="); + switch (kind) { + case 'a': + str.append("audio"); + info = "bitrate=" + desiredBitrate; + break; + case 'v': + str.append("video"); + info = "quality=" + desired + " videoOnly=" + desired2; + break; + case 's': + str.append("subtitles"); + info = "language=" + desired + " autoGenerated=" + desired2; + break; + default: + info = ""; + str.append("other"); + } + + str.append(" format=") + .append(format.getName()) + .append(' ') + .append(info) + .append('}'); + + return str.toString(); + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel parcel, int flags) { + parcel.writeInt(this.format.ordinal()); + parcel.writeString(this.desired); + parcel.writeInt(this.desired2 ? 0x01 : 0x00); + parcel.writeInt(this.desiredBitrate); + parcel.writeByte(this.kind); + parcel.writeString(this.validateCondition); + } + + private MissionRecoveryInfo(Parcel parcel) { + this.format = MediaFormat.values()[parcel.readInt()]; + this.desired = parcel.readString(); + this.desired2 = parcel.readInt() != 0x00; + this.desiredBitrate = parcel.readInt(); + this.kind = parcel.readByte(); + this.validateCondition = parcel.readString(); + } + + public static final Parcelable.Creator CREATOR = new Parcelable.Creator() { + @Override + public MissionRecoveryInfo createFromParcel(Parcel source) { + return new MissionRecoveryInfo(source); + } + + @Override + public MissionRecoveryInfo[] newArray(int size) { + return new MissionRecoveryInfo[size]; + } + }; +} diff --git a/app/src/main/java/us/shandian/giga/io/ProgressReport.java b/app/src/main/java/us/shandian/giga/io/ProgressReport.java index 14ae9ded9..e382747f6 100644 --- a/app/src/main/java/us/shandian/giga/io/ProgressReport.java +++ b/app/src/main/java/us/shandian/giga/io/ProgressReport.java @@ -1,11 +1,11 @@ -package us.shandian.giga.io; - -public interface ProgressReport { - - /** - * Report the size of the new file - * - * @param progress the new size - */ - void report(long progress); +package us.shandian.giga.io; + +public interface ProgressReport { + + /** + * Report the size of the new file + * + * @param progress the new size + */ + void report(long progress); } \ No newline at end of file diff --git a/app/src/main/java/us/shandian/giga/io/StoredFileHelper.java b/app/src/main/java/us/shandian/giga/io/StoredFileHelper.java index ad3ceec3d..eba9437e1 100644 --- a/app/src/main/java/us/shandian/giga/io/StoredFileHelper.java +++ b/app/src/main/java/us/shandian/giga/io/StoredFileHelper.java @@ -315,6 +315,7 @@ public boolean equals(StoredFileHelper storage) { return false; if (this.isInvalid() || storage.isInvalid()) { + if (this.srcName == null || storage.srcName == null || this.srcType == null || storage.srcType == null) return false; return this.srcName.equalsIgnoreCase(storage.srcName) && this.srcType.equalsIgnoreCase(storage.srcType); } diff --git a/app/src/main/java/us/shandian/giga/postprocessing/OggFromWebmDemuxer.java b/app/src/main/java/us/shandian/giga/postprocessing/OggFromWebmDemuxer.java index 04958c495..dc46ced5d 100644 --- a/app/src/main/java/us/shandian/giga/postprocessing/OggFromWebmDemuxer.java +++ b/app/src/main/java/us/shandian/giga/postprocessing/OggFromWebmDemuxer.java @@ -1,44 +1,44 @@ -package us.shandian.giga.postprocessing; - -import androidx.annotation.NonNull; - -import org.schabi.newpipe.streams.OggFromWebMWriter; -import org.schabi.newpipe.streams.io.SharpStream; - -import java.io.IOException; -import java.nio.ByteBuffer; - -class OggFromWebmDemuxer extends Postprocessing { - - OggFromWebmDemuxer() { - super(true, true, ALGORITHM_OGG_FROM_WEBM_DEMUXER); - } - - @Override - boolean test(SharpStream... sources) throws IOException { - ByteBuffer buffer = ByteBuffer.allocate(4); - sources[0].read(buffer.array()); - - // youtube uses WebM as container, but the file extension (format suffix) is "*.opus" - // check if the file is a webm/mkv file before proceed - - switch (buffer.getInt()) { - case 0x1a45dfa3: - return true;// webm/mkv - case 0x4F676753: - return false;// ogg - } - - throw new UnsupportedOperationException("file not recognized, failed to demux the audio stream"); - } - - @Override - int process(SharpStream out, @NonNull SharpStream... sources) throws IOException { - OggFromWebMWriter demuxer = new OggFromWebMWriter(sources[0], out); - demuxer.parseSource(); - demuxer.selectTrack(0); - demuxer.build(); - - return OK_RESULT; - } -} +package us.shandian.giga.postprocessing; + +import androidx.annotation.NonNull; + +import org.schabi.newpipe.streams.OggFromWebMWriter; +import org.schabi.newpipe.streams.io.SharpStream; + +import java.io.IOException; +import java.nio.ByteBuffer; + +class OggFromWebmDemuxer extends Postprocessing { + + OggFromWebmDemuxer() { + super(true, true, ALGORITHM_OGG_FROM_WEBM_DEMUXER); + } + + @Override + boolean test(SharpStream... sources) throws IOException { + ByteBuffer buffer = ByteBuffer.allocate(4); + sources[0].read(buffer.array()); + + // youtube uses WebM as container, but the file extension (format suffix) is "*.opus" + // check if the file is a webm/mkv file before proceed + + switch (buffer.getInt()) { + case 0x1a45dfa3: + return true;// webm/mkv + case 0x4F676753: + return false;// ogg + } + + throw new UnsupportedOperationException("file not recognized, failed to demux the audio stream"); + } + + @Override + int process(SharpStream out, @NonNull SharpStream... sources) throws IOException { + OggFromWebMWriter demuxer = new OggFromWebMWriter(sources[0], out); + demuxer.parseSource(); + demuxer.selectTrack(0); + demuxer.build(); + + return OK_RESULT; + } +} diff --git a/app/src/main/java/us/shandian/giga/service/DownloadManagerService.java b/app/src/main/java/us/shandian/giga/service/DownloadManagerService.java old mode 100755 new mode 100644 index 3da0e75b8..828f1adaf --- a/app/src/main/java/us/shandian/giga/service/DownloadManagerService.java +++ b/app/src/main/java/us/shandian/giga/service/DownloadManagerService.java @@ -24,7 +24,7 @@ import android.os.IBinder; import android.os.Message; import android.os.Parcelable; -import android.preference.PreferenceManager; +import androidx.preference.PreferenceManager; import android.util.Log; import android.util.SparseArray; import android.widget.Toast; @@ -160,7 +160,7 @@ public void onCreate() { mNotificationManager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE); mConnectivityManager = (ConnectivityManager) getSystemService(Context.CONNECTIVITY_SERVICE); - if (android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { mNetworkStateListenerL = new ConnectivityManager.NetworkCallback() { @Override public void onAvailable(Network network) { @@ -240,7 +240,7 @@ public void onDestroy() { manageLock(false); - if (android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) mConnectivityManager.unregisterNetworkCallback(mNetworkStateListenerL); else unregisterReceiver(mNetworkStateListener); @@ -466,7 +466,7 @@ public void notifyFinishedDownload(String name) { if (downloadDoneCount < 1) { downloadDoneList.append(name); - if (android.os.Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) { downloadDoneNotification.setContentTitle(getString(R.string.app_name)); } else { downloadDoneNotification.setContentTitle(null); @@ -505,7 +505,7 @@ public void notifyFailedDownload(DownloadMission mission) { .setContentIntent(mOpenDownloadList); } - if (android.os.Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) { downloadFailedNotification.setContentTitle(getString(R.string.app_name)); downloadFailedNotification.setStyle(new NotificationCompat.BigTextStyle() .bigText(getString(R.string.download_failed).concat(": ").concat(mission.storage.getName()))); diff --git a/app/src/main/java/us/shandian/giga/ui/common/Deleter.java b/app/src/main/java/us/shandian/giga/ui/common/Deleter.java index a0828c23d..b005d0bf9 100644 --- a/app/src/main/java/us/shandian/giga/ui/common/Deleter.java +++ b/app/src/main/java/us/shandian/giga/ui/common/Deleter.java @@ -1,138 +1,138 @@ -package us.shandian.giga.ui.common; - -import android.content.Context; -import android.content.Intent; -import android.graphics.Color; -import android.os.Handler; -import android.view.View; - -import com.google.android.material.snackbar.Snackbar; - -import org.schabi.newpipe.R; - -import java.util.ArrayList; - -import us.shandian.giga.get.FinishedMission; -import us.shandian.giga.get.Mission; -import us.shandian.giga.service.DownloadManager; -import us.shandian.giga.service.DownloadManager.MissionIterator; -import us.shandian.giga.ui.adapter.MissionAdapter; - -public class Deleter { - private static final int TIMEOUT = 5000;// ms - private static final int DELAY = 350;// ms - private static final int DELAY_RESUME = 400;// ms - - private Snackbar snackbar; - private ArrayList items; - private boolean running = true; - - private Context mContext; - private MissionAdapter mAdapter; - private DownloadManager mDownloadManager; - private MissionIterator mIterator; - private Handler mHandler; - private View mView; - - private final Runnable rShow; - private final Runnable rNext; - private final Runnable rCommit; - - public Deleter(View v, Context c, MissionAdapter a, DownloadManager d, MissionIterator i, Handler h) { - mView = v; - mContext = c; - mAdapter = a; - mDownloadManager = d; - mIterator = i; - mHandler = h; - - // use variables to know the reference of the lambdas - rShow = this::show; - rNext = this::next; - rCommit = this::commit; - - items = new ArrayList<>(2); - } - - public void append(Mission item) { - mIterator.hide(item); - items.add(0, item); - - show(); - } - - private void forget() { - mIterator.unHide(items.remove(0)); - mAdapter.applyChanges(); - - show(); - } - - private void show() { - if (items.size() < 1) return; - - pause(); - running = true; - - mHandler.postDelayed(rNext, DELAY); - } - - private void next() { - if (items.size() < 1) return; - - String msg = mContext.getString(R.string.file_deleted).concat(":\n").concat(items.get(0).storage.getName()); - - snackbar = Snackbar.make(mView, msg, Snackbar.LENGTH_INDEFINITE); - snackbar.setAction(R.string.undo, s -> forget()); - snackbar.setActionTextColor(Color.YELLOW); - snackbar.show(); - - mHandler.postDelayed(rCommit, TIMEOUT); - } - - private void commit() { - if (items.size() < 1) return; - - while (items.size() > 0) { - Mission mission = items.remove(0); - if (mission.deleted) continue; - - mIterator.unHide(mission); - mDownloadManager.deleteMission(mission); - - if (mission instanceof FinishedMission) { - mContext.sendBroadcast(new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE, mission.storage.getUri())); - } - break; - } - - if (items.size() < 1) { - pause(); - return; - } - - show(); - } - - public void pause() { - running = false; - mHandler.removeCallbacks(rNext); - mHandler.removeCallbacks(rShow); - mHandler.removeCallbacks(rCommit); - if (snackbar != null) snackbar.dismiss(); - } - - public void resume() { - if (running) return; - mHandler.postDelayed(rShow, DELAY_RESUME); - } - - public void dispose() { - if (items.size() < 1) return; - - pause(); - - for (Mission mission : items) mDownloadManager.deleteMission(mission); - items = null; - } -} +package us.shandian.giga.ui.common; + +import android.content.Context; +import android.content.Intent; +import android.graphics.Color; +import android.os.Handler; +import android.view.View; + +import com.google.android.material.snackbar.Snackbar; + +import org.schabi.newpipe.R; + +import java.util.ArrayList; + +import us.shandian.giga.get.FinishedMission; +import us.shandian.giga.get.Mission; +import us.shandian.giga.service.DownloadManager; +import us.shandian.giga.service.DownloadManager.MissionIterator; +import us.shandian.giga.ui.adapter.MissionAdapter; + +public class Deleter { + private static final int TIMEOUT = 5000;// ms + private static final int DELAY = 350;// ms + private static final int DELAY_RESUME = 400;// ms + + private Snackbar snackbar; + private ArrayList items; + private boolean running = true; + + private Context mContext; + private MissionAdapter mAdapter; + private DownloadManager mDownloadManager; + private MissionIterator mIterator; + private Handler mHandler; + private View mView; + + private final Runnable rShow; + private final Runnable rNext; + private final Runnable rCommit; + + public Deleter(View v, Context c, MissionAdapter a, DownloadManager d, MissionIterator i, Handler h) { + mView = v; + mContext = c; + mAdapter = a; + mDownloadManager = d; + mIterator = i; + mHandler = h; + + // use variables to know the reference of the lambdas + rShow = this::show; + rNext = this::next; + rCommit = this::commit; + + items = new ArrayList<>(2); + } + + public void append(Mission item) { + mIterator.hide(item); + items.add(0, item); + + show(); + } + + private void forget() { + mIterator.unHide(items.remove(0)); + mAdapter.applyChanges(); + + show(); + } + + private void show() { + if (items.size() < 1) return; + + pause(); + running = true; + + mHandler.postDelayed(rNext, DELAY); + } + + private void next() { + if (items.size() < 1) return; + + String msg = mContext.getString(R.string.file_deleted).concat(":\n").concat(items.get(0).storage.getName()); + + snackbar = Snackbar.make(mView, msg, Snackbar.LENGTH_INDEFINITE); + snackbar.setAction(R.string.undo, s -> forget()); + snackbar.setActionTextColor(Color.YELLOW); + snackbar.show(); + + mHandler.postDelayed(rCommit, TIMEOUT); + } + + private void commit() { + if (items.size() < 1) return; + + while (items.size() > 0) { + Mission mission = items.remove(0); + if (mission.deleted) continue; + + mIterator.unHide(mission); + mDownloadManager.deleteMission(mission); + + if (mission instanceof FinishedMission) { + mContext.sendBroadcast(new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE, mission.storage.getUri())); + } + break; + } + + if (items.size() < 1) { + pause(); + return; + } + + show(); + } + + public void pause() { + running = false; + mHandler.removeCallbacks(rNext); + mHandler.removeCallbacks(rShow); + mHandler.removeCallbacks(rCommit); + if (snackbar != null) snackbar.dismiss(); + } + + public void resume() { + if (running) return; + mHandler.postDelayed(rShow, DELAY_RESUME); + } + + public void dispose() { + if (items.size() < 1) return; + + pause(); + + for (Mission mission : items) mDownloadManager.deleteMission(mission); + items = null; + } +} diff --git a/app/src/main/java/us/shandian/giga/ui/fragment/MissionsFragment.java b/app/src/main/java/us/shandian/giga/ui/fragment/MissionsFragment.java index 09f4d0c79..52e858903 100644 --- a/app/src/main/java/us/shandian/giga/ui/fragment/MissionsFragment.java +++ b/app/src/main/java/us/shandian/giga/ui/fragment/MissionsFragment.java @@ -11,7 +11,7 @@ import android.os.Bundle; import android.os.Environment; import android.os.IBinder; -import android.preference.PreferenceManager; +import androidx.preference.PreferenceManager; import android.view.LayoutInflater; import android.view.Menu; import android.view.MenuItem; diff --git a/app/src/main/java/us/shandian/giga/util/Utility.java b/app/src/main/java/us/shandian/giga/util/Utility.java index 551e80a3e..29f790e84 100644 --- a/app/src/main/java/us/shandian/giga/util/Utility.java +++ b/app/src/main/java/us/shandian/giga/util/Utility.java @@ -28,8 +28,10 @@ import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.util.Locale; +import java.util.Random; import us.shandian.giga.io.StoredFileHelper; +import us.shandian.giga.get.DownloadMission; public class Utility { @@ -278,6 +280,28 @@ public static long getContentLength(HttpURLConnection connection) { return -1; } + /** + * Get the content length of the entire file even if the HTTP response is partial + * (response code 206). + * @param connection http connection + * @return content length + */ + public static long getTotalContentLength(final HttpURLConnection connection) { + try { + if (connection.getResponseCode() == 206) { + final String rangeStr = connection.getHeaderField("Content-Range"); + final String bytesStr = rangeStr.split("/", 2)[1]; + return Long.parseLong(bytesStr); + } else { + return getContentLength(connection); + } + } catch (Exception err) { + // nothing to do + } + + return -1; + } + private static String pad(int number) { return number < 10 ? ("0" + number) : String.valueOf(number); } diff --git a/app/src/main/res/drawable-hdpi/ic_newpipe_update.png b/app/src/main/res/drawable-hdpi/ic_newpipe_update.png old mode 100755 new mode 100644 diff --git a/app/src/main/res/drawable-mdpi/ic_newpipe_update.png b/app/src/main/res/drawable-mdpi/ic_newpipe_update.png old mode 100755 new mode 100644 diff --git a/app/src/main/res/drawable-night/ic_heart.xml b/app/src/main/res/drawable-night/ic_heart.xml new file mode 100644 index 000000000..6128a3d0d --- /dev/null +++ b/app/src/main/res/drawable-night/ic_heart.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable-nodpi/not_available_monkey.png b/app/src/main/res/drawable-nodpi/not_available_monkey.png deleted file mode 100644 index ef0068bed..000000000 Binary files a/app/src/main/res/drawable-nodpi/not_available_monkey.png and /dev/null differ diff --git a/app/src/main/res/drawable-nodpi/place_holder_bandcamp.png b/app/src/main/res/drawable-nodpi/place_holder_bandcamp.png 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-xhdpi/ic_newpipe_update.png b/app/src/main/res/drawable-xhdpi/ic_newpipe_update.png old mode 100755 new mode 100644 diff --git a/app/src/main/res/drawable-xxhdpi/ic_newpipe_update.png b/app/src/main/res/drawable-xxhdpi/ic_newpipe_update.png old mode 100755 new mode 100644 diff --git a/app/src/main/res/drawable-xxxhdpi/ic_newpipe_update.png b/app/src/main/res/drawable-xxxhdpi/ic_newpipe_update.png old mode 100755 new mode 100644 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/drawable/ic_next_white_24dp.xml b/app/src/main/res/drawable/ic_next_white_24dp.xml new file mode 100644 index 000000000..603880c2b --- /dev/null +++ b/app/src/main/res/drawable/ic_next_white_24dp.xml @@ -0,0 +1,10 @@ + + + + diff --git a/app/src/main/res/drawable/ic_pin_black_24dp.xml b/app/src/main/res/drawable/ic_pin_black_24dp.xml new file mode 100644 index 000000000..161eed233 --- /dev/null +++ b/app/src/main/res/drawable/ic_pin_black_24dp.xml @@ -0,0 +1,19 @@ + + + + + + diff --git a/app/src/main/res/drawable/ic_pin_white_24dp.xml b/app/src/main/res/drawable/ic_pin_white_24dp.xml new file mode 100644 index 000000000..6be4f9acc --- /dev/null +++ b/app/src/main/res/drawable/ic_pin_white_24dp.xml @@ -0,0 +1,19 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_previous_white_24dp.xml b/app/src/main/res/drawable/ic_previous_white_24dp.xml new file mode 100644 index 000000000..14279ecb2 --- /dev/null +++ b/app/src/main/res/drawable/ic_previous_white_24dp.xml @@ -0,0 +1,10 @@ + + + + diff --git a/app/src/main/res/drawable/ic_timer_black_24dp.xml b/app/src/main/res/drawable/ic_timer_black_24dp.xml new file mode 100644 index 000000000..09d53a6e3 --- /dev/null +++ b/app/src/main/res/drawable/ic_timer_black_24dp.xml @@ -0,0 +1,10 @@ + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_timer_white_24dp.xml b/app/src/main/res/drawable/ic_timer_white_24dp.xml new file mode 100644 index 000000000..48e71ba7d --- /dev/null +++ b/app/src/main/res/drawable/ic_timer_white_24dp.xml @@ -0,0 +1,10 @@ + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/not_available_monkey.xml b/app/src/main/res/drawable/not_available_monkey.xml new file mode 100644 index 000000000..b15a381c5 --- /dev/null +++ b/app/src/main/res/drawable/not_available_monkey.xml @@ -0,0 +1,27 @@ + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout-land/activity_player_queue_control.xml b/app/src/main/res/layout-land/activity_player_queue_control.xml index 84a29e0c8..a7872a83a 100644 --- a/app/src/main/res/layout-land/activity_player_queue_control.xml +++ b/app/src/main/res/layout-land/activity_player_queue_control.xml @@ -185,7 +185,7 @@ android:orientation="horizontal" tools:ignore="RtlHardcoded"> - @@ -238,7 +238,7 @@ app:srcCompat="@drawable/ic_shuffle_white_24dp" tools:ignore="ContentDescription"/> - 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 16dcff639..a52f7896c 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-large-land/fragment_video_detail.xml b/app/src/main/res/layout-large-land/fragment_video_detail.xml index cb2b9ccfe..74f10556d 100644 --- a/app/src/main/res/layout-large-land/fragment_video_detail.xml +++ b/app/src/main/res/layout-large-land/fragment_video_detail.xml @@ -48,7 +48,7 @@ android:id="@+id/detail_thumbnail_image_view" android:layout_width="match_parent" android:layout_height="wrap_content" - android:background="@android:color/transparent" + android:background="?windowBackground" android:contentDescription="@string/detail_thumbnail_view_description" android:scaleType="fitCenter" tools:ignore="RtlHardcoded" diff --git a/app/src/main/res/layout/activity_main_player.xml b/app/src/main/res/layout/activity_main_player.xml index 4bae123ee..a86a76dec 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/activity_player_queue_control.xml b/app/src/main/res/layout/activity_player_queue_control.xml index c5b8e5743..35053c9c8 100644 --- a/app/src/main/res/layout/activity_player_queue_control.xml +++ b/app/src/main/res/layout/activity_player_queue_control.xml @@ -1,7 +1,7 @@ + app:title="@string/app_name" /> @@ -30,42 +30,55 @@ android:id="@+id/play_queue" android:layout_width="match_parent" android:layout_height="match_parent" + android:layout_above="@id/metadata" android:layout_below="@id/appbar" - android:layout_above="@id/center" android:scrollbars="vertical" app:layoutManager="LinearLayoutManager" - tools:listitem="@layout/play_queue_item"/> + tools:listitem="@layout/play_queue_item" /> - + + + android:layout_above="@id/progress_bar" + android:background="?attr/selectableItemBackground" + android:clickable="true" + android:focusable="true" + android:orientation="vertical" + android:padding="8dp" + tools:ignore="RtlHardcoded,RtlSymmetry"> - - - + android:ellipsize="marquee" + android:fadingEdge="horizontal" + android:marqueeRepeatLimit="marquee_forever" + android:scrollHorizontally="true" + android:singleLine="true" + android:textAppearance="?android:attr/textAppearanceLarge" + android:textSize="14sp" + tools:text="Lorem ipsum dolor sit amet, consectetur adipiscing elit. Duis nec aliquam augue, eget cursus est. Ut id tristique enim, ut scelerisque tellus. Sed ultricies ipsum non mauris ultricies, commodo malesuada velit porta." /> + tools:text="Duis posuere arcu condimentum lobortis mattis." /> - - - + android:layout_above="@+id/playback_controls"> + tools:text="1:06:29" /> + tools:secondaryProgress="50" /> + tools:text="1:23:49" /> + android:visibility="gone" /> + tools:ignore="ContentDescription" /> - + app:srcCompat="@drawable/ic_previous_white_24dp" + tools:ignore="ContentDescription" /> + android:src="@drawable/exo_controls_rewind" + android:tint="?attr/colorAccent" /> + tools:ignore="ContentDescription" /> + tools:visibility="visible" /> + android:src="@drawable/exo_controls_fastforward" + android:tint="?attr/colorAccent" /> - + app:srcCompat="@drawable/ic_next_white_24dp" + tools:ignore="ContentDescription" /> + tools:ignore="ContentDescription" /> 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..e88e42b12 100644 --- a/app/src/main/res/layout/dialog_feed_group_create.xml +++ b/app/src/main/res/layout/dialog_feed_group_create.xml @@ -207,7 +207,7 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_toStartOf="@+id/confirm_button" - android:text="@android:string/cancel" /> + android:text="@string/cancel" />