Merge pull request #8889 from TeamNewPipe/release-0.24.0

Release v0.24.0 (990)
This commit is contained in:
Stypox 2022-09-25 13:56:20 +02:00 committed by GitHub
commit 0c63950429
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
467 changed files with 9880 additions and 8928 deletions

View File

@ -1,6 +1,6 @@
name: Bug report name: Bug report
description: Create a bug report to help us improve description: Create a bug report to help us improve
labels: [bug] labels: [bug, needs triage]
body: body:
- type: markdown - type: markdown
attributes: attributes:
@ -18,6 +18,8 @@ body:
required: true required: true
- label: "I made sure that there are *no existing issues* - [open](https://github.com/TeamNewPipe/NewPipe/issues) or [closed](https://github.com/TeamNewPipe/NewPipe/issues?q=is%3Aissue+is%3Aclosed) - which I could contribute my information to." - label: "I made sure that there are *no existing issues* - [open](https://github.com/TeamNewPipe/NewPipe/issues) or [closed](https://github.com/TeamNewPipe/NewPipe/issues?q=is%3Aissue+is%3Aclosed) - which I could contribute my information to."
required: true required: true
- label: "I have read the [FAQ](https://newpipe.net/FAQ/) and my problem isn't listed."
required: true
- label: "I have taken the time to fill in all the required details. I understand that the bug report will be dismissed otherwise." - label: "I have taken the time to fill in all the required details. I understand that the bug report will be dismissed otherwise."
required: true required: true
- label: "This issue contains only one bug." - label: "This issue contains only one bug."
@ -40,7 +42,7 @@ body:
label: Steps to reproduce the bug label: Steps to reproduce the bug
description: | description: |
What did you do for the bug to show up? What did you do for the bug to show up?
If you can't cause the bug to show up again reliably (and hence don't have a proper set of steps to give us), please still try to give as many details as possible on how you think you encountered the bug. If you can't cause the bug to show up again reliably (and hence don't have a proper set of steps to give us), please still try to give as many details as possible on how you think you encountered the bug.
placeholder: | placeholder: |
1. Go to '...' 1. Go to '...'
@ -69,11 +71,11 @@ body:
label: Screenshots/Screen recordings label: Screenshots/Screen recordings
description: | description: |
A picture or video is worth a thousand words. A picture or video is worth a thousand words.
If applicable, add screenshots or a screen recording to help explain your problem. If applicable, add screenshots or a screen recording to help explain your problem.
GitHub supports uploading them directly in the text box. GitHub supports uploading them directly in the text box.
If your file is too big for Github to accept, try to compress it (ZIP-file) or feel free to paste a link to an image/video hoster here instead. If your file is too big for Github to accept, try to compress it (ZIP-file) or feel free to paste a link to an image/video hoster here instead.
:heavy_exclamation_mark: DON'T POST SCREENSHOTS OF THE ERROR PAGE. :heavy_exclamation_mark: DON'T POST SCREENSHOTS OF THE ERROR PAGE.
Instead, follow the instructions in the "Logs" section below. Instead, follow the instructions in the "Logs" section below.

View File

@ -1,6 +1,6 @@
name: Feature request name: Feature request
description: Suggest an idea for this project description: Suggest an idea for this project
labels: [enhancement] labels: [enhancement, needs triage]
body: body:
- type: markdown - type: markdown
attributes: attributes:
@ -8,7 +8,6 @@ body:
Thank you for helping to make NewPipe better by suggesting a feature. :hugs: Thank you for helping to make NewPipe better by suggesting a feature. :hugs:
Your ideas are highly welcome! The app is made for you, the users, after all. Your ideas are highly welcome! The app is made for you, the users, after all.
- type: checkboxes - type: checkboxes
id: checklist id: checklist
attributes: attributes:
@ -16,6 +15,8 @@ body:
options: options:
- label: "I made sure that there are *no existing issues* - [open](https://github.com/TeamNewPipe/NewPipe/issues) or [closed](https://github.com/TeamNewPipe/NewPipe/issues?q=is%3Aissue+is%3Aclosed) - which I could contribute my information to." - label: "I made sure that there are *no existing issues* - [open](https://github.com/TeamNewPipe/NewPipe/issues) or [closed](https://github.com/TeamNewPipe/NewPipe/issues?q=is%3Aissue+is%3Aclosed) - which I could contribute my information to."
required: true required: true
- label: "I have read the [FAQ](https://newpipe.net/FAQ/) and my problem isn't listed."
required: true
- label: "I'm aware that this is a request for NewPipe itself and that requests for adding a new service need to be made at [NewPipeExtractor](https://github.com/TeamNewPipe/NewPipeExtractor/issues)." - label: "I'm aware that this is a request for NewPipe itself and that requests for adding a new service need to be made at [NewPipeExtractor](https://github.com/TeamNewPipe/NewPipeExtractor/issues)."
required: true required: true
- label: "I have taken the time to fill in all the required details. I understand that the feature request will be dismissed otherwise." - label: "I have taken the time to fill in all the required details. I understand that the feature request will be dismissed otherwise."
@ -43,7 +44,7 @@ body:
Describe any problem or limitation you come across while using the app which would be solved by this feature. Describe any problem or limitation you come across while using the app which would be solved by this feature.
validations: validations:
required: true required: true
- type: textarea - type: textarea
id: additional-information id: additional-information
attributes: attributes:

View File

@ -1,6 +1,6 @@
name: Question name: Question
description: Ask about anything NewPipe-related description: Ask about anything NewPipe-related
labels: [question] labels: [question, needs triage]
body: body:
- type: markdown - type: markdown
attributes: attributes:
@ -16,6 +16,8 @@ body:
options: options:
- label: "I made sure that there are *no existing issues* - [open](https://github.com/TeamNewPipe/NewPipe/issues) or [closed](https://github.com/TeamNewPipe/NewPipe/issues?q=is%3Aissue+is%3Aclosed) - which I could contribute my information to." - label: "I made sure that there are *no existing issues* - [open](https://github.com/TeamNewPipe/NewPipe/issues) or [closed](https://github.com/TeamNewPipe/NewPipe/issues?q=is%3Aissue+is%3Aclosed) - which I could contribute my information to."
required: true required: true
- label: "I have read the [FAQ](https://newpipe.net/FAQ/) and my question isn't listed."
required: true
- label: "I have taken the time to fill in all the required details. I understand that the question will be dismissed otherwise." - label: "I have taken the time to fill in all the required details. I understand that the question will be dismissed otherwise."
required: true required: true
- label: "I have read and understood the [contribution guidelines](https://github.com/TeamNewPipe/NewPipe/blob/dev/.github/CONTRIBUTING.md)." - label: "I have read and understood the [contribution guidelines](https://github.com/TeamNewPipe/NewPipe/blob/dev/.github/CONTRIBUTING.md)."
@ -27,7 +29,7 @@ body:
label: What is/are your question(s)? label: What is/are your question(s)?
validations: validations:
required: true required: true
- type: textarea - type: textarea
id: additional-information id: additional-information
attributes: attributes:

View File

@ -25,7 +25,7 @@
<!-- Delete this if it doesn't apply to your PR. --> <!-- Delete this if it doesn't apply to your PR. -->
- -
#### APK testing #### APK testing
<!-- Use a new, meaningfully named branch. The name is used as a suffix for the app ID to allow installing and testing multiple versions of NewPipe, e.g. "commentfix", if your PR implements a bugfix for comments. (No names like "patch-0" and "feature-1".) --> <!-- Use a new, meaningfully named branch. The name is used as a suffix for the app ID to allow installing and testing multiple versions of NewPipe, e.g. "commentfix", if your PR implements a bugfix for comments. (No names like "patch-0" and "feature-1".) -->
<!-- Remove the following line if you directly link the APK created by the CI pipeline. Directly linking is preferred if you need to let users test.--> <!-- Remove the following line if you directly link the APK created by the CI pipeline. Directly linking is preferred if you need to let users test.-->
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. 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.

View File

@ -6,7 +6,7 @@ on:
branches: branches:
- dev - dev
- master - master
- release/** - release**
paths-ignore: paths-ignore:
- 'README.md' - 'README.md'
- 'doc/**' - 'doc/**'
@ -31,6 +31,10 @@ on:
jobs: jobs:
build-and-test-jvm: build-and-test-jvm:
runs-on: ubuntu-latest runs-on: ubuntu-latest
permissions:
contents: read
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v3
- uses: gradle/wrapper-validation-action@v1 - uses: gradle/wrapper-validation-action@v1
@ -64,6 +68,10 @@ jobs:
matrix: matrix:
# api-level 19 is min sdk, but throws errors related to desugaring # api-level 19 is min sdk, but throws errors related to desugaring
api-level: [ 21, 29 ] api-level: [ 21, 29 ]
permissions:
contents: read
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v3
@ -81,7 +89,7 @@ jobs:
# workaround to emulator bug: https://github.com/ReactiveCircus/android-emulator-runner/issues/160 # workaround to emulator bug: https://github.com/ReactiveCircus/android-emulator-runner/issues/160
emulator-build: 7425822 emulator-build: 7425822
script: ./gradlew connectedCheck --stacktrace script: ./gradlew connectedCheck --stacktrace
- name: Upload test report when tests fail # because the printed out stacktrace (console) is too short, see also #7553 - name: Upload test report when tests fail # because the printed out stacktrace (console) is too short, see also #7553
uses: actions/upload-artifact@v3 uses: actions/upload-artifact@v3
if: failure() if: failure()
@ -91,6 +99,10 @@ jobs:
sonar: sonar:
runs-on: ubuntu-latest runs-on: ubuntu-latest
permissions:
contents: read
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v3
with: with:

View File

@ -6,6 +6,10 @@ on:
issues: issues:
types: [opened, edited] types: [opened, edited]
permissions:
issues: write
pull-requests: write
jobs: jobs:
try-minimize: try-minimize:
runs-on: ubuntu-latest runs-on: ubuntu-latest

View File

@ -9,6 +9,10 @@ on:
# Run daily at midnight. # Run daily at midnight.
- cron: '0 0 * * *' - cron: '0 0 * * *'
permissions:
issues: write
pull-requests: write
jobs: jobs:
noResponse: noResponse:
runs-on: ubuntu-latest runs-on: ubuntu-latest
@ -17,4 +21,4 @@ jobs:
with: with:
token: ${{ github.token }} token: ${{ github.token }}
daysUntilClose: 14 daysUntilClose: 14
responseRequiredLabel: waiting-for-author responseRequiredLabel: waiting for author

124
README.md
View File

@ -1,6 +1,6 @@
<p align="center"><a href="https://newpipe.net"><img src="assets/new_pipe_icon_5.png" width="150"></a></p> <p align="center"><a href="https://newpipe.net"><img src="assets/new_pipe_icon_5.png" width="150"></a></p>
<h2 align="center"><b>NewPipe</b></h2> <h2 align="center"><b>NewPipe</b></h2>
<h4 align="center">A libre lightweight streaming frontend for Android.</h4> <h4 align="center">A libre lightweight streaming front-end for Android.</h4>
<p align="center"><a href="https://f-droid.org/packages/org.schabi.newpipe/"><img src="https://fdroid.gitlab.io/artwork/badge/get-it-on-en.svg" alt="Get it on F-Droid" height=80/></a></p> <p align="center"><a href="https://f-droid.org/packages/org.schabi.newpipe/"><img src="https://fdroid.gitlab.io/artwork/badge/get-it-on-en.svg" alt="Get it on F-Droid" height=80/></a></p>
@ -13,15 +13,15 @@
<a href="https://www.bountysource.com/teams/newpipe" alt="Bountysource bounties"><img src="https://img.shields.io/bountysource/team/newpipe/activity.svg?colorB=cd201f"></a> <a href="https://www.bountysource.com/teams/newpipe" alt="Bountysource bounties"><img src="https://img.shields.io/bountysource/team/newpipe/activity.svg?colorB=cd201f"></a>
</p> </p>
<hr> <hr>
<p align="center"><a href="#screenshots">Screenshots</a> &bull; <a href="#description">Description</a> &bull; <a href="#features">Features</a> &bull; <a href="#installation-and-updates">Installation and updates</a> &bull; <a href="#contribution">Contribution</a> &bull; <a href="#donate">Donate</a> &bull; <a href="#license">License</a></p> <p align="center"><a href="#screenshots">Screenshots</a> &bull; <a href="#supported-services">Supported Services</a> &bull; <a href="#description">Description</a> &bull; <a href="#features">Features</a> &bull; <a href="#installation-and-updates">Installation and updates</a> &bull; <a href="#contribution">Contribution</a> &bull; <a href="#donate">Donate</a> &bull; <a href="#license">License</a></p>
<p align="center"><a href="https://newpipe.net">Website</a> &bull; <a href="https://newpipe.net/blog/">Blog</a> &bull; <a href="https://newpipe.net/FAQ/">FAQ</a> &bull; <a href="https://newpipe.net/press/">Press</a></p> <p align="center"><a href="https://newpipe.net">Website</a> &bull; <a href="https://newpipe.net/blog/">Blog</a> &bull; <a href="https://newpipe.net/FAQ/">FAQ</a> &bull; <a href="https://newpipe.net/press/">Press</a></p>
<hr> <hr>
*Read this in other languages: [English](README.md), [Español](doc/README.es.md), [हिन्दी](doc/README.hi.md), [한국어](doc/README.ko.md), [Soomaali](doc/README.so.md), [Português Brasil](doc/README.pt_BR.md), [Polski](doc/README.pl.md), [日本語](doc/README.ja.md), [Română](doc/README.ro.md), [Türkçe](doc/README.tr.md), [正體中文](doc/README.zh_TW.md).* *Read this in other languages: [English](README.md), [Español](doc/README.es.md), [हिन्दी](doc/README.hi.md), [한국어](doc/README.ko.md), [Soomaali](doc/README.so.md), [Português Brasil](doc/README.pt_BR.md), [Polski](doc/README.pl.md), [日本語](doc/README.ja.md), [Română](doc/README.ro.md), [Türkçe](doc/README.tr.md), [正體中文](doc/README.zh_TW.md).*
<b>WARNING: THIS IS A BETA VERSION, THEREFORE YOU MAY ENCOUNTER BUGS. IF YOU DO, OPEN AN ISSUE VIA OUR GITHUB REPOSITORY.</b> <b>WARNING: THIS APP IS IN BETA, SO YOU MAY ENCOUNTER BUGS. IF YOU DO, OPEN AN ISSUE IN OUR GITHUB REPOSITORY BY FILLING OUT THE ISSUE TEMPLATE.</b>
<b>PUTTING NEWPIPE OR ANY FORK OF IT INTO THE GOOGLE PLAY STORE VIOLATES THEIR TERMS AND CONDITIONS.</b> <b>PUTTING NEWPIPE, OR ANY FORK OF IT, INTO THE GOOGLE PLAY STORE VIOLATES THEIR TERMS AND CONDITIONS.</b>
## Screenshots ## Screenshots
@ -38,62 +38,66 @@
[<img src="fastlane/metadata/android/en-US/images/tenInchScreenshots/shot_11.png" width=405>](fastlane/metadata/android/en-US/images/tenInchScreenshots/shot_11.png) [<img src="fastlane/metadata/android/en-US/images/tenInchScreenshots/shot_11.png" width=405>](fastlane/metadata/android/en-US/images/tenInchScreenshots/shot_11.png)
[<img src="fastlane/metadata/android/en-US/images/tenInchScreenshots/shot_12.png" width=405>](fastlane/metadata/android/en-US/images/tenInchScreenshots/shot_12.png) [<img src="fastlane/metadata/android/en-US/images/tenInchScreenshots/shot_12.png" width=405>](fastlane/metadata/android/en-US/images/tenInchScreenshots/shot_12.png)
### Supported Services
NewPipe currently supports these services:
<!-- We link to the service websites separately to avoid people accidentally opening a website they didn't want to. -->
* YouTube ([website](https://www.youtube.com/)) and YouTube Music ([website](https://music.youtube.com/)) ([wiki](https://en.wikipedia.org/wiki/YouTube))
* PeerTube ([website](https://joinpeertube.org/)) and all its instances (open the website to know what that means!) ([wiki](https://en.wikipedia.org/wiki/PeerTube))
* Bandcamp ([website](https://bandcamp.com/)) ([wiki](https://en.wikipedia.org/wiki/Bandcamp))
* SoundCloud ([website](https://soundcloud.com/)) ([wiki](https://en.wikipedia.org/wiki/SoundCloud))
* media.ccc.de ([website](https://media.ccc.de/)) ([wiki](https://en.wikipedia.org/wiki/Chaos_Computer_Club))
As you can see, NewPipe supports multiple video and audio services. Though it started off with YouTube, other people have added more services over the years, making NewPipe more and more versatile!
Partially due to circumstance, and partially due to its popularity, YouTube is the best supported out of these services. If you use or are familiar with any of these other services, please help us improve support for them! We're looking for maintainers for SoundCloud and PeerTube.
If you intend to add a new service, please get in touch with us first! Our [docs](https://teamnewpipe.github.io/documentation/) provide more information on how a new service can be added to the app and to the [NewPipe Extractor](https://github.com/TeamNewPipe/NewPipeExtractor).
## Description ## Description
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. NewPipe works by fetching the required data from the official API (e.g. PeerTube) of the service you're using. If the official API is restricted (e.g. YouTube) for our purposes, or is proprietary, the app parses the website or uses an internal API instead. This means that you don't need an account on any service to use NewPipe.
Also, since they are free and open source software, neither the app nor the Extractor use any proprietary libraries or frameworks, such as Google Play Services. This means you can use NewPipe on devices or custom ROMs that do not have Google apps installed.
### Features ### Features
* Search videos * Watch videos at resolutions up to 4K
* No Login Required * Listen to audio in the background, only loading the audio stream to save data
* Display general info about videos * Popup mode (floating player, aka Picture-in-Picture)
* Watch YouTube videos * Watch live streams
* Listen to YouTube videos * Show/hide subtitles/closed captions
* Popup mode (floating player) * Search videos and audios (on YouTube, you can specify the content language as well)
* Select streaming player to watch video with * Enqueue videos (and optionally save them as local playlists)
* Download videos * Show/hide general information about videos (such as description and tags)
* Download audio only * Show/hide next/related videos
* Open a video in Kodi * Show/hide comments
* Show next/related videos * Search videos, audios, channels, playlists and albums
* Search YouTube in a specific language * Browse videos and audios within a channel
* Watch/Block age restricted material * Subscribe to channels (yes, without logging into any account!)
* Display general info about channels * Get notifications about new videos from channels you're subscribed to
* Search channels * Create and edit channel groups (for easier browsing and management)
* Watch videos from a channel * Browse video feeds generated from your channel groups
* Orbot/Tor support (not yet directly) * View and search your watch history
* 1080p/2K/4K support * Search and watch playlists (these are remote playlists, which means they're fetched from the service you're browsing)
* View history * Create and edit local playlists (these are created and saved within the app, and have nothing to do with any service)
* Subscribe to channels * Download videos/audios/subtitles (closed captions)
* Search history * Open in Kodi
* Search/watch playlists * Watch/Block age-restricted material
* Watch as enqueued playlists
* Enqueue videos
* Local playlists
* Subtitles
* Livestream support
* Show comments
### Supported Services <!-- Hidden span to keep old links compatible. You should remove this span if you're translating the README into another language.-->
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:
* YouTube
* SoundCloud \[beta\]
* media.ccc.de \[beta\]
* PeerTube instances \[beta\]
* Bandcamp \[beta\]
<!-- Hidden span to keep old links compatible. -->
<span id="updates"></span> <span id="updates"></span>
## Installation and updates ## Installation and updates
You can install NewPipe using one of the following methods: You can install NewPipe using one of the following methods:
1. Add our custom repo to F-Droid and install it from there. The instructions are here: https://newpipe.net/FAQ/tutorials/install-add-fdroid-repo/ 1. Add our custom repo to F-Droid and install it from there. The instructions are here: https://newpipe.net/FAQ/tutorials/install-add-fdroid-repo/
2. Download the APK from [GitHub Releases](https://github.com/TeamNewPipe/NewPipe/releases) and install it. 2. Download the APK from [GitHub Releases](https://github.com/TeamNewPipe/NewPipe/releases) and install it.
3. 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. 3. Update via F-Droid. This is the slowest method of getting updates, as F-Droid must recognize changes, build the APK itself, sign it, and then push the update to users.
4. 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. 4. 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.
5. If you're interested in a specific feature or bugfix provided in a Pull Request in this repo, you can also download its APK from within the PR. Read the PR description for instructions. The great thing about PR-specific APKs is that they're installed side-by-side the official app, so you don't have to worry about losing your data or messing anything up.
We recommend method 1 for most users. APKs installed using method 1 or 2 are compatible with each other, but not with those installed using method 3. This is due to the same signing key (ours) being used for 1 and 2, but a different signing key (F-Droid's) being used for 3. Building a debug APK using method 4 excludes a key entirely. Signing keys help ensure that a user isn't tricked into installing a malicious update to an app. We recommend method 1 for most users. APKs installed using method 1 or 2 are compatible with each other (meaning that if you installed NewPipe using either method 1 or 2, you can also update NewPipe using the other), but not with those installed using method 3. This is due to the same signing key (ours) being used for 1 and 2, but a different signing key (F-Droid's) being used for 3. Building a debug APK using method 4 excludes a key entirely. Signing keys help ensure that a user isn't tricked into installing a malicious update to an app. When using method 5, each APK is signed with a different random key supplied by GitHub Actions, so you cannot even update it. You will have to backup and restore the app data each time you wish to use a new APK.
In the meanwhile, if you want to switch sources for some reason (e.g. NewPipe's core functionality breaks and F-Droid doesn't have the latest update yet), we recommend following this procedure: In the meanwhile, if you want to switch sources for some reason (e.g. NewPipe's core functionality breaks and F-Droid doesn't have the latest 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 1. Back up your data via Settings > Content > Export Database so you keep your history, subscriptions, and playlists
@ -101,30 +105,29 @@ In the meanwhile, if you want to switch sources for some reason (e.g. NewPipe's
3. Download the APK from the new source and install it 3. Download the APK from the new source and install it
4. Import the data from step 1 via Settings > Content > Import Database 4. Import the data from step 1 via Settings > Content > Import Database
## Contribution <b>Note: when you're importing a database into the official app, always make sure that it is the one you exported _from_ the official app. If you import a database exported from an APK other than the official app, it may break things. Such an action is unsupported, and you should only do so when you're absolutely certain you know what you're doing.</b>
Whether you have ideas, translations, design changes, code cleaning, or real heavy code changes, help is always welcome.
The more is done the better it gets!
If you'd like to get involved, check our [contribution notes](.github/CONTRIBUTING.md). ## Contribution
Whether you have ideas, translations, design changes, code cleaning, or even major code changes, help is always welcome. The app gets better and better with each contribution, no matter how big or small! If you'd like to get involved, check our [contribution notes](.github/CONTRIBUTING.md).
<a href="https://hosted.weblate.org/engage/newpipe/"> <a href="https://hosted.weblate.org/engage/newpipe/">
<img src="https://hosted.weblate.org/widgets/newpipe/-/287x66-grey.png" alt="Translation status" /> <img src="https://hosted.weblate.org/widgets/newpipe/-/287x66-grey.png" alt="Translation status" />
</a> </a>
## Donate ## 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.net/donate). If you like NewPipe, you're welcome to send a donation. We prefer Liberapay, as it is both open-source and non-profit. For further info on donating to NewPipe, please visit our [website](https://newpipe.net/donate).
<table> <table>
<tr>
<td><img src="https://bitcoin.org/img/icons/logotop.svg" alt="Bitcoin"></td>
<td><img src="assets/bitcoin_qr_code.png" alt="Bitcoin QR code" width="100px"></td>
<td><samp>16A9J59ahMRqkLSZjhYj33n9j3fMztFxnh</samp></td>
</tr>
<tr> <tr>
<td><a href="https://liberapay.com/TeamNewPipe/"><img src="https://upload.wikimedia.org/wikipedia/commons/2/27/Liberapay_logo_v2_white-on-yellow.svg" alt="Liberapay" width="80px" ></a></td> <td><a href="https://liberapay.com/TeamNewPipe/"><img src="https://upload.wikimedia.org/wikipedia/commons/2/27/Liberapay_logo_v2_white-on-yellow.svg" alt="Liberapay" width="80px" ></a></td>
<td><a href="https://liberapay.com/TeamNewPipe/"><img src="assets/liberapay_qr_code.png" alt="Visit NewPipe at liberapay.com" width="100px"></a></td> <td><a href="https://liberapay.com/TeamNewPipe/"><img src="assets/liberapay_qr_code.png" alt="Visit NewPipe at liberapay.com" width="100px"></a></td>
<td><a href="https://liberapay.com/TeamNewPipe/donate"><img src="assets/liberapay_donate_button.svg" alt="Donate via Liberapay" height="35px"></a></td> <td><a href="https://liberapay.com/TeamNewPipe/donate"><img src="assets/liberapay_donate_button.svg" alt="Donate via Liberapay" height="35px"></a></td>
</tr> </tr>
<tr>
<td><img src="https://bitcoin.org/img/icons/logotop.svg" alt="Bitcoin"></td>
<td><img src="assets/bitcoin_qr_code.png" alt="Bitcoin QR code" width="100px"></td>
<td><samp>16A9J59ahMRqkLSZjhYj33n9j3fMztFxnh</samp></td>
</tr>
<tr> <tr>
<td><a href="https://www.bountysource.com/teams/newpipe"><img src="https://upload.wikimedia.org/wikipedia/commons/thumb/2/22/Bountysource.png/320px-Bountysource.png" alt="Bountysource" width="190px"></a></td> <td><a href="https://www.bountysource.com/teams/newpipe"><img src="https://upload.wikimedia.org/wikipedia/commons/thumb/2/22/Bountysource.png/320px-Bountysource.png" alt="Bountysource" width="190px"></a></td>
<td><a href="https://www.bountysource.com/teams/newpipe"><img src="assets/bountysource_qr_code.png" alt="Visit NewPipe at bountysource.com" width="100px"></a></td> <td><a href="https://www.bountysource.com/teams/newpipe"><img src="assets/bountysource_qr_code.png" alt="Visit NewPipe at bountysource.com" width="100px"></a></td>
@ -134,14 +137,9 @@ If you like NewPipe we'd be happy about a donation. You can either send bitcoin
## Privacy Policy ## Privacy Policy
The NewPipe project aims to provide a private, anonymous experience for using media web services. The NewPipe project aims to provide a private, anonymous experience for using web-based media 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 leave a comment in our blog. You can find the document [here](https://newpipe.net/legal/privacy/).
Therefore, the app does not collect any data without your consent. NewPipe's privacy policy explains in detail what data is sent and stored when you send a crash report, or comment in our blog. You can find the document [here](https://newpipe.net/legal/privacy/).
## License ## License
[![GNU GPLv3 Image](https://www.gnu.org/graphics/gplv3-127x51.png)](https://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 NewPipe is Free Software: You can use, study, share, and improve it at 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.
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.

View File

@ -14,15 +14,12 @@ android {
defaultConfig { defaultConfig {
applicationId "org.schabi.newpipe" applicationId "org.schabi.newpipe"
resValue "string", "app_name", "NewPipe" resValue "string", "app_name", "NewPipe"
minSdk 19 minSdk 21
targetSdk 29 targetSdk 29
versionCode 989 versionCode 990
versionName "0.23.3" versionName "0.24.0"
multiDexEnabled true
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
vectorDrawables.useSupportLibrary = true
javaCompileOptions { javaCompileOptions {
annotationProcessorOptions { annotationProcessorOptions {
@ -98,14 +95,14 @@ android {
} }
ext { ext {
checkstyleVersion = '10.0' checkstyleVersion = '10.3.1'
androidxLifecycleVersion = '2.3.1' androidxLifecycleVersion = '2.5.1'
androidxRoomVersion = '2.4.2' androidxRoomVersion = '2.4.3'
androidxWorkVersion = '2.7.1' androidxWorkVersion = '2.7.1'
icepickVersion = '3.2.0' icepickVersion = '3.2.0'
exoPlayerVersion = '2.17.1' exoPlayerVersion = '2.18.1'
googleAutoServiceVersion = '1.0.1' googleAutoServiceVersion = '1.0.1'
groupieVersion = '2.10.1' groupieVersion = '2.10.1'
markwonVersion = '4.6.2' markwonVersion = '4.6.2'
@ -113,7 +110,7 @@ ext {
leakCanaryVersion = '2.5' leakCanaryVersion = '2.5'
stethoVersion = '1.6.0' stethoVersion = '1.6.0'
mockitoVersion = '4.0.0' mockitoVersion = '4.0.0'
assertJVersion = '3.22.0' assertJVersion = '3.23.1'
} }
configurations { configurations {
@ -182,7 +179,7 @@ sonarqube {
dependencies { dependencies {
/** Desugaring **/ /** Desugaring **/
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.1.5' coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.1.6'
/** NewPipe libraries **/ /** NewPipe libraries **/
// You can use a local version by uncommenting a few lines in settings.gradle // You can use a local version by uncommenting a few lines in settings.gradle
@ -190,27 +187,27 @@ dependencies {
// name and the commit hash with the commit hash of the (pushed) commit you want to test // name and the commit hash with the commit hash of the (pushed) commit you want to test
// This works thanks to JitPack: https://jitpack.io/ // This works thanks to JitPack: https://jitpack.io/
implementation 'com.github.TeamNewPipe:nanojson:1d9e1aea9049fc9f85e68b43ba39fe7be1c1f751' implementation 'com.github.TeamNewPipe:nanojson:1d9e1aea9049fc9f85e68b43ba39fe7be1c1f751'
implementation 'com.github.TeamNewPipe:NewPipeExtractor:6a858368c86bc9a55abee586eb6c733e86c26b97' implementation 'com.github.TeamNewPipe:NewPipeExtractor:5c710da160f488bb40ab2cf4469bec9bd4cefd38'
implementation 'com.github.TeamNewPipe:NoNonsense-FilePicker:5.0.0'
/** Checkstyle **/ /** Checkstyle **/
checkstyle "com.puppycrawl.tools:checkstyle:${checkstyleVersion}" checkstyle "com.puppycrawl.tools:checkstyle:${checkstyleVersion}"
ktlint 'com.pinterest:ktlint:0.44.0' ktlint 'com.pinterest:ktlint:0.45.2'
/** Kotlin **/ /** Kotlin **/
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:${kotlin_version}" implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:${kotlin_version}"
/** AndroidX **/ /** AndroidX **/
implementation 'androidx.appcompat:appcompat:1.3.1' implementation 'androidx.appcompat:appcompat:1.4.2'
implementation 'androidx.cardview:cardview:1.0.0' implementation 'androidx.cardview:cardview:1.0.0'
implementation 'androidx.constraintlayout:constraintlayout:2.1.3' implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
implementation 'androidx.core:core-ktx:1.6.0' implementation 'androidx.core:core-ktx:1.8.0'
implementation 'androidx.documentfile:documentfile:1.0.1' implementation 'androidx.documentfile:documentfile:1.0.1'
implementation 'androidx.fragment:fragment-ktx:1.3.6' implementation 'androidx.fragment:fragment-ktx:1.4.1'
implementation "androidx.lifecycle:lifecycle-livedata:${androidxLifecycleVersion}" implementation "androidx.lifecycle:lifecycle-livedata-ktx:${androidxLifecycleVersion}"
implementation "androidx.lifecycle:lifecycle-viewmodel:${androidxLifecycleVersion}" implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:${androidxLifecycleVersion}"
implementation 'androidx.localbroadcastmanager:localbroadcastmanager:1.1.0' implementation 'androidx.localbroadcastmanager:localbroadcastmanager:1.1.0'
implementation 'androidx.media:media:1.5.0' implementation 'androidx.media:media:1.6.0'
implementation 'androidx.multidex:multidex:2.0.1'
implementation 'androidx.preference:preference:1.2.0' implementation 'androidx.preference:preference:1.2.0'
implementation 'androidx.recyclerview:recyclerview:1.2.1' implementation 'androidx.recyclerview:recyclerview:1.2.1'
implementation "androidx.room:room-runtime:${androidxRoomVersion}" implementation "androidx.room:room-runtime:${androidxRoomVersion}"
@ -220,10 +217,9 @@ dependencies {
// Newer version specified to prevent accessibility regressions with RecyclerView, see: // Newer version specified to prevent accessibility regressions with RecyclerView, see:
// https://developer.android.com/jetpack/androidx/releases/viewpager2#1.1.0-alpha01 // https://developer.android.com/jetpack/androidx/releases/viewpager2#1.1.0-alpha01
implementation 'androidx.viewpager2:viewpager2:1.1.0-beta01' implementation 'androidx.viewpager2:viewpager2:1.1.0-beta01'
implementation 'androidx.webkit:webkit:1.4.0'
implementation "androidx.work:work-runtime-ktx:${androidxWorkVersion}" implementation "androidx.work:work-runtime-ktx:${androidxWorkVersion}"
implementation "androidx.work:work-rxjava3:${androidxWorkVersion}" implementation "androidx.work:work-rxjava3:${androidxWorkVersion}"
implementation 'com.google.android.material:material:1.5.0' implementation 'com.google.android.material:material:1.6.1'
/** Third-party libraries **/ /** Third-party libraries **/
// Instance state boilerplate elimination // Instance state boilerplate elimination
@ -234,11 +230,16 @@ dependencies {
implementation "org.jsoup:jsoup:1.15.3" implementation "org.jsoup:jsoup:1.15.3"
// HTTP client // HTTP client
//noinspection GradleDependency --> do not update okhttp to keep supporting Android 4.4 users implementation "com.squareup.okhttp3:okhttp:4.10.0"
implementation "com.squareup.okhttp3:okhttp:3.12.13"
// Media player // Media player
implementation "com.google.android.exoplayer:exoplayer:${exoPlayerVersion}" implementation "com.google.android.exoplayer:exoplayer-core:${exoPlayerVersion}"
implementation "com.google.android.exoplayer:exoplayer-dash:${exoPlayerVersion}"
implementation "com.google.android.exoplayer:exoplayer-database:${exoPlayerVersion}"
implementation "com.google.android.exoplayer:exoplayer-datasource:${exoPlayerVersion}"
implementation "com.google.android.exoplayer:exoplayer-hls:${exoPlayerVersion}"
implementation "com.google.android.exoplayer:exoplayer-smoothstreaming:${exoPlayerVersion}"
implementation "com.google.android.exoplayer:exoplayer-ui:${exoPlayerVersion}"
implementation "com.google.android.exoplayer:extension-mediasession:${exoPlayerVersion}" implementation "com.google.android.exoplayer:extension-mediasession:${exoPlayerVersion}"
// Metadata generator for service descriptors // Metadata generator for service descriptors
@ -257,9 +258,6 @@ dependencies {
implementation "io.noties.markwon:core:${markwonVersion}" implementation "io.noties.markwon:core:${markwonVersion}"
implementation "io.noties.markwon:linkify:${markwonVersion}" implementation "io.noties.markwon:linkify:${markwonVersion}"
// File picker
implementation "com.nononsenseapps:filepicker:4.2.1"
// Crash reporting // Crash reporting
implementation "ch.acra:acra-core:5.9.3" implementation "ch.acra:acra-core:5.9.3"
@ -273,7 +271,7 @@ dependencies {
implementation "com.jakewharton.rxbinding4:rxbinding:4.0.0" implementation "com.jakewharton.rxbinding4:rxbinding:4.0.0"
// Date and time formatting // Date and time formatting
implementation "org.ocpsoft.prettytime:prettytime:5.0.2.Final" implementation "org.ocpsoft.prettytime:prettytime:5.0.3.Final"
/** Debugging **/ /** Debugging **/
// Memory leak detection // Memory leak detection

View File

@ -18,7 +18,6 @@
-dontobfuscate -dontobfuscate
-keep class org.schabi.newpipe.extractor.timeago.patterns.** { *; } -keep class org.schabi.newpipe.extractor.timeago.patterns.** { *; }
-keep class org.ocpsoft.prettytime.i18n.** { *; }
-keep class org.mozilla.javascript.** { *; } -keep class org.mozilla.javascript.** { *; }
@ -26,9 +25,6 @@
-keep class com.google.android.exoplayer2.** { *; } -keep class com.google.android.exoplayer2.** { *; }
-dontwarn org.mozilla.javascript.tools.** -dontwarn org.mozilla.javascript.tools.**
-dontwarn android.arch.util.paging.CountedDataSource
-dontwarn android.arch.persistence.room.paging.LimitOffsetDataSource
# Rules for icepick. Copy paste from https://github.com/frankiesardo/icepick # Rules for icepick. Copy paste from https://github.com/frankiesardo/icepick
-dontwarn icepick.** -dontwarn icepick.**
@ -39,12 +35,11 @@
} }
-keepnames class * { @icepick.State *;} -keepnames class * { @icepick.State *;}
# Rules for OkHttp. Copy paste from https://github.com/square/okhttp ## Rules for OkHttp. Copy paste from https://github.com/square/okhttp
-dontwarn okhttp3.** -dontwarn okhttp3.**
-dontwarn okio.** -dontwarn okio.**
-dontwarn javax.annotation.** ##
# A resource is loaded with a relative path so the package of this class must be preserved.
-keepnames class okhttp3.internal.publicsuffix.PublicSuffixDatabase
-keepclassmembers class * implements java.io.Serializable { -keepclassmembers class * implements java.io.Serializable {
static final long serialVersionUID; static final long serialVersionUID;
!static !transient <fields>; !static !transient <fields>;

View File

@ -44,7 +44,7 @@
</receiver> </receiver>
<service <service
android:name=".player.MainPlayer" android:name=".player.PlayerService"
android:exported="false" android:exported="false"
android:foregroundServiceType="mediaPlayback"> android:foregroundServiceType="mediaPlayback">
<intent-filter> <intent-filter>

View File

@ -282,11 +282,9 @@ public abstract class FragmentStatePagerAdapterMenuWorkaround extends PagerAdapt
@Nullable @Nullable
public Parcelable saveState() { public Parcelable saveState() {
Bundle state = null; Bundle state = null;
if (mSavedState.size() > 0) { if (!mSavedState.isEmpty()) {
state = new Bundle(); state = new Bundle();
final Fragment.SavedState[] fss = new Fragment.SavedState[mSavedState.size()]; state.putParcelableArray("states", mSavedState.toArray(new Fragment.SavedState[0]));
mSavedState.toArray(fss);
state.putParcelableArray("states", fss);
} }
for (int i = 0; i < mFragments.size(); i++) { for (int i = 0; i < mFragments.size(); i++) {
final Fragment f = mFragments.get(i); final Fragment f = mFragments.get(i);

View File

@ -14,7 +14,6 @@ import androidx.coordinatorlayout.widget.CoordinatorLayout;
import org.schabi.newpipe.R; import org.schabi.newpipe.R;
import java.lang.reflect.Field; import java.lang.reflect.Field;
import java.util.Arrays;
import java.util.List; import java.util.List;
// See https://stackoverflow.com/questions/56849221#57997489 // See https://stackoverflow.com/questions/56849221#57997489
@ -27,7 +26,7 @@ public final class FlingBehavior extends AppBarLayout.Behavior {
private boolean allowScroll = true; private boolean allowScroll = true;
private final Rect globalRect = new Rect(); private final Rect globalRect = new Rect();
private final List<Integer> skipInterceptionOfElements = Arrays.asList( private final List<Integer> skipInterceptionOfElements = List.of(
R.id.itemsListPanel, R.id.playbackSeekBar, R.id.itemsListPanel, R.id.playbackSeekBar,
R.id.playPauseButton, R.id.playPreviousButton, R.id.playNextButton); R.id.playPauseButton, R.id.playPreviousButton, R.id.playNextButton);
@ -67,7 +66,7 @@ public final class FlingBehavior extends AppBarLayout.Behavior {
public boolean onInterceptTouchEvent(@NonNull final CoordinatorLayout parent, public boolean onInterceptTouchEvent(@NonNull final CoordinatorLayout parent,
@NonNull final AppBarLayout child, @NonNull final AppBarLayout child,
@NonNull final MotionEvent ev) { @NonNull final MotionEvent ev) {
for (final Integer element : skipInterceptionOfElements) { for (final int element : skipInterceptionOfElements) {
final View view = child.findViewById(element); final View view = child.findViewById(element);
if (view != null) { if (view != null) {
final boolean visible = view.getGlobalVisibleRect(globalRect); final boolean visible = view.getGlobalVisibleRect(globalRect);
@ -132,8 +131,8 @@ public final class FlingBehavior extends AppBarLayout.Behavior {
try { try {
final Class<?> headerBehaviorType = this.getClass().getSuperclass().getSuperclass(); final Class<?> headerBehaviorType = this.getClass().getSuperclass().getSuperclass();
if (headerBehaviorType != null) { if (headerBehaviorType != null) {
final Field field final Field field =
= headerBehaviorType.getDeclaredField("lastNestedScrollingChildRef"); headerBehaviorType.getDeclaredField("lastNestedScrollingChildRef");
field.setAccessible(true); field.setAccessible(true);
return field; return field;
} }

View File

@ -1,5 +1,6 @@
package org.schabi.newpipe; package org.schabi.newpipe;
import android.app.Application;
import android.content.Context; import android.content.Context;
import android.content.SharedPreferences; import android.content.SharedPreferences;
import android.util.Log; import android.util.Log;
@ -7,7 +8,6 @@ import android.util.Log;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.core.app.NotificationChannelCompat; import androidx.core.app.NotificationChannelCompat;
import androidx.core.app.NotificationManagerCompat; import androidx.core.app.NotificationManagerCompat;
import androidx.multidex.MultiDexApplication;
import androidx.preference.PreferenceManager; import androidx.preference.PreferenceManager;
import com.jakewharton.processphoenix.ProcessPhoenix; import com.jakewharton.processphoenix.ProcessPhoenix;
@ -27,9 +27,8 @@ import org.schabi.newpipe.util.StateSaver;
import java.io.IOException; import java.io.IOException;
import java.io.InterruptedIOException; import java.io.InterruptedIOException;
import java.net.SocketException; import java.net.SocketException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List; import java.util.List;
import java.util.Objects;
import io.reactivex.rxjava3.exceptions.CompositeException; import io.reactivex.rxjava3.exceptions.CompositeException;
import io.reactivex.rxjava3.exceptions.MissingBackpressureException; import io.reactivex.rxjava3.exceptions.MissingBackpressureException;
@ -56,7 +55,7 @@ import io.reactivex.rxjava3.plugins.RxJavaPlugins;
* along with NewPipe. If not, see <http://www.gnu.org/licenses/>. * along with NewPipe. If not, see <http://www.gnu.org/licenses/>.
*/ */
public class App extends MultiDexApplication { public class App extends Application {
public static final String PACKAGE_NAME = BuildConfig.APPLICATION_ID; public static final String PACKAGE_NAME = BuildConfig.APPLICATION_ID;
private static final String TAG = App.class.toString(); private static final String TAG = App.class.toString();
private static App app; private static App app;
@ -140,7 +139,7 @@ public class App extends MultiDexApplication {
if (throwable instanceof UndeliverableException) { if (throwable instanceof UndeliverableException) {
// As UndeliverableException is a wrapper, // As UndeliverableException is a wrapper,
// get the cause of it to get the "real" exception // get the cause of it to get the "real" exception
actualThrowable = throwable.getCause(); actualThrowable = Objects.requireNonNull(throwable.getCause());
} else { } else {
actualThrowable = throwable; actualThrowable = throwable;
} }
@ -149,7 +148,7 @@ public class App extends MultiDexApplication {
if (actualThrowable instanceof CompositeException) { if (actualThrowable instanceof CompositeException) {
errors = ((CompositeException) actualThrowable).getExceptions(); errors = ((CompositeException) actualThrowable).getExceptions();
} else { } else {
errors = Collections.singletonList(actualThrowable); errors = List.of(actualThrowable);
} }
for (final Throwable error : errors) { for (final Throwable error : errors) {
@ -213,41 +212,37 @@ public class App extends MultiDexApplication {
private void initNotificationChannels() { private void initNotificationChannels() {
// Keep the importance below DEFAULT to avoid making noise on every notification update for // Keep the importance below DEFAULT to avoid making noise on every notification update for
// the main and update channels // the main and update channels
final List<NotificationChannelCompat> notificationChannelCompats = new ArrayList<>(); final List<NotificationChannelCompat> notificationChannelCompats = List.of(
notificationChannelCompats.add(new NotificationChannelCompat new NotificationChannelCompat.Builder(getString(R.string.notification_channel_id),
.Builder(getString(R.string.notification_channel_id),
NotificationManagerCompat.IMPORTANCE_LOW) NotificationManagerCompat.IMPORTANCE_LOW)
.setName(getString(R.string.notification_channel_name)) .setName(getString(R.string.notification_channel_name))
.setDescription(getString(R.string.notification_channel_description)) .setDescription(getString(R.string.notification_channel_description))
.build()); .build(),
new NotificationChannelCompat
notificationChannelCompats.add(new NotificationChannelCompat .Builder(getString(R.string.app_update_notification_channel_id),
.Builder(getString(R.string.app_update_notification_channel_id),
NotificationManagerCompat.IMPORTANCE_LOW) NotificationManagerCompat.IMPORTANCE_LOW)
.setName(getString(R.string.app_update_notification_channel_name)) .setName(getString(R.string.app_update_notification_channel_name))
.setDescription(getString(R.string.app_update_notification_channel_description)) .setDescription(
.build()); getString(R.string.app_update_notification_channel_description))
.build(),
notificationChannelCompats.add(new NotificationChannelCompat new NotificationChannelCompat.Builder(getString(R.string.hash_channel_id),
.Builder(getString(R.string.hash_channel_id),
NotificationManagerCompat.IMPORTANCE_HIGH) NotificationManagerCompat.IMPORTANCE_HIGH)
.setName(getString(R.string.hash_channel_name)) .setName(getString(R.string.hash_channel_name))
.setDescription(getString(R.string.hash_channel_description)) .setDescription(getString(R.string.hash_channel_description))
.build()); .build(),
new NotificationChannelCompat.Builder(getString(R.string.error_report_channel_id),
notificationChannelCompats.add(new NotificationChannelCompat
.Builder(getString(R.string.error_report_channel_id),
NotificationManagerCompat.IMPORTANCE_LOW) NotificationManagerCompat.IMPORTANCE_LOW)
.setName(getString(R.string.error_report_channel_name)) .setName(getString(R.string.error_report_channel_name))
.setDescription(getString(R.string.error_report_channel_description)) .setDescription(getString(R.string.error_report_channel_description))
.build()); .build(),
new NotificationChannelCompat
notificationChannelCompats.add(new NotificationChannelCompat .Builder(getString(R.string.streams_notification_channel_id),
.Builder(getString(R.string.streams_notification_channel_id), NotificationManagerCompat.IMPORTANCE_DEFAULT)
NotificationManagerCompat.IMPORTANCE_DEFAULT) .setName(getString(R.string.streams_notification_channel_name))
.setName(getString(R.string.streams_notification_channel_name)) .setDescription(
.setDescription(getString(R.string.streams_notification_channel_description)) getString(R.string.streams_notification_channel_description))
.build()); .build()
);
final NotificationManagerCompat notificationManager = NotificationManagerCompat.from(this); final NotificationManagerCompat notificationManager = NotificationManagerCompat.from(this);
notificationManager.createNotificationChannelsCompat(notificationChannelCompats); notificationManager.createNotificationChannelsCompat(notificationChannelCompats);

View File

@ -1,7 +1,6 @@
package org.schabi.newpipe; package org.schabi.newpipe;
import android.content.Context; import android.content.Context;
import android.os.Build;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
@ -12,40 +11,27 @@ import org.schabi.newpipe.extractor.downloader.Downloader;
import org.schabi.newpipe.extractor.downloader.Request; import org.schabi.newpipe.extractor.downloader.Request;
import org.schabi.newpipe.extractor.downloader.Response; import org.schabi.newpipe.extractor.downloader.Response;
import org.schabi.newpipe.extractor.exceptions.ReCaptchaException; import org.schabi.newpipe.extractor.exceptions.ReCaptchaException;
import org.schabi.newpipe.util.CookieUtils;
import org.schabi.newpipe.util.InfoCache; import org.schabi.newpipe.util.InfoCache;
import org.schabi.newpipe.util.TLSSocketFactoryCompat;
import java.io.IOException; import java.io.IOException;
import java.security.KeyManagementException;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.HashMap; import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Objects;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import javax.net.ssl.SSLSocketFactory;
import javax.net.ssl.TrustManager;
import javax.net.ssl.TrustManagerFactory;
import javax.net.ssl.X509TrustManager;
import okhttp3.CipherSuite;
import okhttp3.ConnectionSpec;
import okhttp3.OkHttpClient; import okhttp3.OkHttpClient;
import okhttp3.RequestBody; import okhttp3.RequestBody;
import okhttp3.ResponseBody; import okhttp3.ResponseBody;
import static org.schabi.newpipe.MainActivity.DEBUG;
public final class DownloaderImpl extends Downloader { public final class DownloaderImpl extends Downloader {
public static final String USER_AGENT public static final String USER_AGENT =
= "Mozilla/5.0 (Windows NT 10.0; rv:91.0) Gecko/20100101 Firefox/91.0"; "Mozilla/5.0 (Windows NT 10.0; rv:91.0) Gecko/20100101 Firefox/91.0";
public static final String YOUTUBE_RESTRICTED_MODE_COOKIE_KEY public static final String YOUTUBE_RESTRICTED_MODE_COOKIE_KEY =
= "youtube_restricted_mode_key"; "youtube_restricted_mode_key";
public static final String YOUTUBE_RESTRICTED_MODE_COOKIE = "PREF=f2=8000000"; public static final String YOUTUBE_RESTRICTED_MODE_COOKIE = "PREF=f2=8000000";
public static final String YOUTUBE_DOMAIN = "youtube.com"; public static final String YOUTUBE_DOMAIN = "youtube.com";
@ -54,9 +40,6 @@ public final class DownloaderImpl extends Downloader {
private final OkHttpClient client; private final OkHttpClient client;
private DownloaderImpl(final OkHttpClient.Builder builder) { private DownloaderImpl(final OkHttpClient.Builder builder) {
if (Build.VERSION.SDK_INT == Build.VERSION_CODES.KITKAT) {
enableModernTLS(builder);
}
this.client = builder this.client = builder
.readTimeout(30, TimeUnit.SECONDS) .readTimeout(30, TimeUnit.SECONDS)
// .cache(new Cache(new File(context.getExternalCacheDir(), "okhttp"), // .cache(new Cache(new File(context.getExternalCacheDir(), "okhttp"),
@ -81,69 +64,16 @@ public final class DownloaderImpl extends Downloader {
return instance; return instance;
} }
/**
* Enable TLS 1.2 and 1.1 on Android Kitkat. This function is mostly taken
* from the documentation of OkHttpClient.Builder.sslSocketFactory(_,_).
* <p>
* If there is an error, the function will safely fall back to doing nothing
* and printing the error to the console.
* </p>
*
* @param builder The HTTPClient Builder on which TLS is enabled on (will be modified in-place)
*/
private static void enableModernTLS(final OkHttpClient.Builder builder) {
try {
// get the default TrustManager
final TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance(
TrustManagerFactory.getDefaultAlgorithm());
trustManagerFactory.init((KeyStore) null);
final TrustManager[] trustManagers = trustManagerFactory.getTrustManagers();
if (trustManagers.length != 1 || !(trustManagers[0] instanceof X509TrustManager)) {
throw new IllegalStateException("Unexpected default trust managers:"
+ Arrays.toString(trustManagers));
}
final X509TrustManager trustManager = (X509TrustManager) trustManagers[0];
// insert our own TLSSocketFactory
final SSLSocketFactory sslSocketFactory = TLSSocketFactoryCompat.getInstance();
builder.sslSocketFactory(sslSocketFactory, trustManager);
// This will try to enable all modern CipherSuites(+2 more)
// that are supported on the device.
// Necessary because some servers (e.g. Framatube.org)
// don't support the old cipher suites.
// https://github.com/square/okhttp/issues/4053#issuecomment-402579554
final List<CipherSuite> cipherSuites =
new ArrayList<>(ConnectionSpec.MODERN_TLS.cipherSuites());
cipherSuites.add(CipherSuite.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA);
cipherSuites.add(CipherSuite.TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA);
final ConnectionSpec legacyTLS = new ConnectionSpec.Builder(ConnectionSpec.MODERN_TLS)
.cipherSuites(cipherSuites.toArray(new CipherSuite[0]))
.build();
builder.connectionSpecs(Arrays.asList(legacyTLS, ConnectionSpec.CLEARTEXT));
} catch (final KeyManagementException | NoSuchAlgorithmException | KeyStoreException e) {
if (DEBUG) {
e.printStackTrace();
}
}
}
public String getCookies(final String url) { public String getCookies(final String url) {
final List<String> resultCookies = new ArrayList<>(); final String youtubeCookie = url.contains(YOUTUBE_DOMAIN)
if (url.contains(YOUTUBE_DOMAIN)) { ? getCookie(YOUTUBE_RESTRICTED_MODE_COOKIE_KEY) : null;
final String youtubeCookie = getCookie(YOUTUBE_RESTRICTED_MODE_COOKIE_KEY);
if (youtubeCookie != null) {
resultCookies.add(youtubeCookie);
}
}
// Recaptcha cookie is always added TODO: not sure if this is necessary // Recaptcha cookie is always added TODO: not sure if this is necessary
final String recaptchaCookie = getCookie(ReCaptchaActivity.RECAPTCHA_COOKIES_KEY); return Stream.of(youtubeCookie, getCookie(ReCaptchaActivity.RECAPTCHA_COOKIES_KEY))
if (recaptchaCookie != null) { .filter(Objects::nonNull)
resultCookies.add(recaptchaCookie); .flatMap(cookies -> Arrays.stream(cookies.split("; *")))
} .distinct()
return CookieUtils.concatCookies(resultCookies); .collect(Collectors.joining("; "));
} }
public String getCookie(final String key) { public String getCookie(final String key) {
@ -203,7 +133,7 @@ public final class DownloaderImpl extends Downloader {
RequestBody requestBody = null; RequestBody requestBody = null;
if (dataToSend != null) { if (dataToSend != null) {
requestBody = RequestBody.create(null, dataToSend); requestBody = RequestBody.create(dataToSend);
} }
final okhttp3.Request.Builder requestBuilder = new okhttp3.Request.Builder() final okhttp3.Request.Builder requestBuilder = new okhttp3.Request.Builder()

View File

@ -3,7 +3,6 @@ package org.schabi.newpipe;
import android.annotation.SuppressLint; import android.annotation.SuppressLint;
import android.app.Activity; import android.app.Activity;
import android.content.Intent; import android.content.Intent;
import android.os.Build;
import android.os.Bundle; import android.os.Bundle;
import org.schabi.newpipe.util.NavigationHelper; import org.schabi.newpipe.util.NavigationHelper;
@ -44,11 +43,7 @@ public class ExitActivity extends Activity {
protected void onCreate(final Bundle savedInstanceState) { protected void onCreate(final Bundle savedInstanceState) {
super.onCreate(savedInstanceState); super.onCreate(savedInstanceState);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { finishAndRemoveTask();
finishAndRemoveTask();
} else {
finish();
}
NavigationHelper.restartApp(this); NavigationHelper.restartApp(this);
} }

View File

@ -28,7 +28,6 @@ import android.content.Intent;
import android.content.IntentFilter; import android.content.IntentFilter;
import android.content.SharedPreferences; import android.content.SharedPreferences;
import android.content.pm.PackageManager; import android.content.pm.PackageManager;
import android.os.Build;
import android.os.Bundle; import android.os.Bundle;
import android.os.Handler; import android.os.Handler;
import android.os.Looper; import android.os.Looper;
@ -86,7 +85,6 @@ import org.schabi.newpipe.util.PermissionHelper;
import org.schabi.newpipe.util.SerializedCache; import org.schabi.newpipe.util.SerializedCache;
import org.schabi.newpipe.util.ServiceHelper; import org.schabi.newpipe.util.ServiceHelper;
import org.schabi.newpipe.util.StateSaver; import org.schabi.newpipe.util.StateSaver;
import org.schabi.newpipe.util.TLSSocketFactoryCompat;
import org.schabi.newpipe.util.ThemeHelper; import org.schabi.newpipe.util.ThemeHelper;
import org.schabi.newpipe.views.FocusOverlayView; import org.schabi.newpipe.views.FocusOverlayView;
@ -131,11 +129,6 @@ public class MainActivity extends AppCompatActivity {
+ "savedInstanceState = [" + savedInstanceState + "]"); + "savedInstanceState = [" + savedInstanceState + "]");
} }
// 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.setDayNightMode(this); ThemeHelper.setDayNightMode(this);
ThemeHelper.setTheme(this, ServiceHelper.getSelectedServiceId(this)); ThemeHelper.setTheme(this, ServiceHelper.getSelectedServiceId(this));
@ -381,8 +374,7 @@ public class MainActivity extends AppCompatActivity {
private void showServices() { private void showServices() {
for (final StreamingService s : NewPipe.getServices()) { for (final StreamingService s : NewPipe.getServices()) {
final String title = s.getServiceInfo().getName() final String title = s.getServiceInfo().getName();
+ (ServiceHelper.isBeta(s) ? " (beta)" : "");
final MenuItem menuItem = drawerLayoutBinding.navigation.getMenu() final MenuItem menuItem = drawerLayoutBinding.navigation.getMenu()
.add(R.id.menu_services_group, s.getServiceId(), ORDER, title) .add(R.id.menu_services_group, s.getServiceId(), ORDER, title)
@ -390,7 +382,7 @@ public class MainActivity extends AppCompatActivity {
// peertube specifics // peertube specifics
if (s.getServiceId() == 3) { if (s.getServiceId() == 3) {
enhancePeertubeMenu(s, menuItem); enhancePeertubeMenu(menuItem);
} }
} }
drawerLayoutBinding.navigation.getMenu() drawerLayoutBinding.navigation.getMenu()
@ -398,9 +390,9 @@ public class MainActivity extends AppCompatActivity {
.setChecked(true); .setChecked(true);
} }
private void enhancePeertubeMenu(final StreamingService s, final MenuItem menuItem) { private void enhancePeertubeMenu(final MenuItem menuItem) {
final PeertubeInstance currentInstance = PeertubeHelper.getCurrentInstance(); final PeertubeInstance currentInstance = PeertubeHelper.getCurrentInstance();
menuItem.setTitle(currentInstance.getName() + (ServiceHelper.isBeta(s) ? " (beta)" : "")); menuItem.setTitle(currentInstance.getName());
final Spinner spinner = InstanceSpinnerLayoutBinding.inflate(LayoutInflater.from(this)) final Spinner spinner = InstanceSpinnerLayoutBinding.inflate(LayoutInflater.from(this))
.getRoot(); .getRoot();
final List<PeertubeInstance> instances = PeertubeHelper.getInstanceList(this); final List<PeertubeInstance> instances = PeertubeHelper.getInstanceList(this);
@ -480,8 +472,8 @@ public class MainActivity extends AppCompatActivity {
ErrorUtil.showUiErrorSnackbar(this, "Setting up service toggle", e); ErrorUtil.showUiErrorSnackbar(this, "Setting up service toggle", e);
} }
final SharedPreferences sharedPreferences final SharedPreferences sharedPreferences =
= PreferenceManager.getDefaultSharedPreferences(this); PreferenceManager.getDefaultSharedPreferences(this);
if (sharedPreferences.getBoolean(Constants.KEY_THEME_CHANGE, false)) { if (sharedPreferences.getBoolean(Constants.KEY_THEME_CHANGE, false)) {
if (DEBUG) { if (DEBUG) {
Log.d(TAG, "Theme has changed, recreating activity..."); Log.d(TAG, "Theme has changed, recreating activity...");
@ -653,8 +645,8 @@ public class MainActivity extends AppCompatActivity {
} }
super.onCreateOptionsMenu(menu); super.onCreateOptionsMenu(menu);
final Fragment fragment final Fragment fragment =
= getSupportFragmentManager().findFragmentById(R.id.fragment_holder); getSupportFragmentManager().findFragmentById(R.id.fragment_holder);
if (!(fragment instanceof SearchFragment)) { if (!(fragment instanceof SearchFragment)) {
toolbarLayoutBinding.toolbarSearchContainer.getRoot().setVisibility(View.GONE); toolbarLayoutBinding.toolbarSearchContainer.getRoot().setVisibility(View.GONE);
} }

View File

@ -3,7 +3,6 @@ package org.schabi.newpipe;
import android.annotation.SuppressLint; import android.annotation.SuppressLint;
import android.app.Activity; import android.app.Activity;
import android.content.Intent; import android.content.Intent;
import android.os.Build;
import android.os.Bundle; import android.os.Bundle;
/* /*
@ -40,10 +39,6 @@ public class PanicResponderActivity extends Activity {
ExitActivity.exitAndRemoveFromRecentApps(this); ExitActivity.exitAndRemoveFromRecentApps(this);
} }
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { finishAndRemoveTask();
finishAndRemoveTask();
} else {
finish();
}
} }
} }

View File

@ -16,7 +16,7 @@ import org.schabi.newpipe.player.playqueue.PlayQueueItem;
import org.schabi.newpipe.util.NavigationHelper; import org.schabi.newpipe.util.NavigationHelper;
import org.schabi.newpipe.util.SparseItemUtil; import org.schabi.newpipe.util.SparseItemUtil;
import java.util.Collections; import java.util.List;
public final class QueueItemMenuUtil { public final class QueueItemMenuUtil {
private QueueItemMenuUtil() { private QueueItemMenuUtil() {
@ -53,7 +53,7 @@ public final class QueueItemMenuUtil {
case R.id.menu_item_append_playlist: case R.id.menu_item_append_playlist:
PlaylistDialog.createCorrespondingDialog( PlaylistDialog.createCorrespondingDialog(
context, context,
Collections.singletonList(new StreamEntity(item)), List.of(new StreamEntity(item)),
dialog -> dialog.show( dialog -> dialog.show(
fragmentManager, fragmentManager,
"QueueItemMenuUtil@append_playlist" "QueueItemMenuUtil@append_playlist"

View File

@ -30,6 +30,7 @@ import androidx.appcompat.app.AppCompatActivity;
import androidx.appcompat.content.res.AppCompatResources; import androidx.appcompat.content.res.AppCompatResources;
import androidx.core.app.NotificationCompat; import androidx.core.app.NotificationCompat;
import androidx.core.app.ServiceCompat; import androidx.core.app.ServiceCompat;
import androidx.core.math.MathUtils;
import androidx.fragment.app.FragmentManager; import androidx.fragment.app.FragmentManager;
import androidx.preference.PreferenceManager; import androidx.preference.PreferenceManager;
@ -60,7 +61,7 @@ import org.schabi.newpipe.extractor.playlist.PlaylistInfo;
import org.schabi.newpipe.extractor.stream.StreamInfo; import org.schabi.newpipe.extractor.stream.StreamInfo;
import org.schabi.newpipe.ktx.ExceptionUtils; import org.schabi.newpipe.ktx.ExceptionUtils;
import org.schabi.newpipe.local.dialog.PlaylistDialog; import org.schabi.newpipe.local.dialog.PlaylistDialog;
import org.schabi.newpipe.player.MainPlayer; import org.schabi.newpipe.player.PlayerType;
import org.schabi.newpipe.player.helper.PlayerHelper; import org.schabi.newpipe.player.helper.PlayerHelper;
import org.schabi.newpipe.player.helper.PlayerHolder; import org.schabi.newpipe.player.helper.PlayerHolder;
import org.schabi.newpipe.player.playqueue.ChannelPlayQueue; import org.schabi.newpipe.player.playqueue.ChannelPlayQueue;
@ -81,7 +82,6 @@ import org.schabi.newpipe.views.FocusOverlayView;
import java.io.Serializable; import java.io.Serializable;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.Collections;
import java.util.List; import java.util.List;
import icepick.Icepick; import icepick.Icepick;
@ -452,7 +452,7 @@ public class RouterActivity extends AppCompatActivity {
} }
} }
selectedRadioPosition = Math.min(Math.max(-1, selectedRadioPosition), choices.size() - 1); selectedRadioPosition = MathUtils.clamp(selectedRadioPosition, -1, choices.size() - 1);
if (selectedRadioPosition != -1) { if (selectedRadioPosition != -1) {
((RadioButton) radioGroup.getChildAt(selectedRadioPosition)).setChecked(true); ((RadioButton) radioGroup.getChildAt(selectedRadioPosition)).setChecked(true);
} }
@ -630,8 +630,8 @@ public class RouterActivity extends AppCompatActivity {
} }
// ...the player is not running or in normal Video-mode/type // ...the player is not running or in normal Video-mode/type
final MainPlayer.PlayerType playerType = PlayerHolder.getInstance().getType(); final PlayerType playerType = PlayerHolder.getInstance().getType();
return playerType == null || playerType == MainPlayer.PlayerType.VIDEO; return playerType == null || playerType == PlayerType.MAIN;
} }
private void openAddToPlaylistDialog() { private void openAddToPlaylistDialog() {
@ -649,7 +649,7 @@ public class RouterActivity extends AppCompatActivity {
.subscribe( .subscribe(
info -> PlaylistDialog.createCorrespondingDialog( info -> PlaylistDialog.createCorrespondingDialog(
getThemeWrapperContext(), getThemeWrapperContext(),
Collections.singletonList(new StreamEntity(info)), List.of(new StreamEntity(info)),
playlistDialog -> { playlistDialog -> {
playlistDialog.setOnDismissListener(dialog -> finish()); playlistDialog.setOnDismissListener(dialog -> finish());

View File

@ -8,7 +8,6 @@ import androidx.core.os.bundleOf
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import io.reactivex.rxjava3.disposables.CompositeDisposable import io.reactivex.rxjava3.disposables.CompositeDisposable
import org.schabi.newpipe.R import org.schabi.newpipe.R
import org.schabi.newpipe.about.LicenseFragmentHelper.showLicense
import org.schabi.newpipe.databinding.FragmentLicensesBinding import org.schabi.newpipe.databinding.FragmentLicensesBinding
import org.schabi.newpipe.databinding.ItemSoftwareComponentBinding import org.schabi.newpipe.databinding.ItemSoftwareComponentBinding

View File

@ -12,126 +12,92 @@ import org.schabi.newpipe.R
import org.schabi.newpipe.util.Localization import org.schabi.newpipe.util.Localization
import org.schabi.newpipe.util.ThemeHelper import org.schabi.newpipe.util.ThemeHelper
import org.schabi.newpipe.util.external_communication.ShareUtils import org.schabi.newpipe.util.external_communication.ShareUtils
import java.io.BufferedReader
import java.io.IOException import java.io.IOException
import java.io.InputStreamReader
import java.nio.charset.StandardCharsets
object LicenseFragmentHelper { /**
/** * @param context the context to use
* @param context the context to use * @param license the license
* @param license the license * @return String which contains a HTML formatted license page
* @return String which contains a HTML formatted license page * styled according to the context's theme
* styled according to the context's theme */
*/ private fun getFormattedLicense(context: Context, license: License): String {
private fun getFormattedLicense(context: Context, license: License): String { try {
val licenseContent = StringBuilder() return context.assets.open(license.filename).bufferedReader().use { it.readText() }
val webViewData: String // split the HTML file and insert the stylesheet into the HEAD of the file
try { .replace("</head>", "<style>${getLicenseStylesheet(context)}</style></head>")
BufferedReader( } catch (e: IOException) {
InputStreamReader( throw IllegalArgumentException("Could not get license file: ${license.filename}", e)
context.assets.open(license.filename), }
StandardCharsets.UTF_8 }
)
).use { `in` ->
var str: String?
while (`in`.readLine().also { str = it } != null) {
licenseContent.append(str)
}
// split the HTML file and insert the stylesheet into the HEAD of the file /**
webViewData = "$licenseContent".replace( * @param context the Android context
"</head>", * @return String which is a CSS stylesheet according to the context's theme
"<style>" + getLicenseStylesheet(context) + "</style></head>" */
) private fun getLicenseStylesheet(context: Context): String {
} val isLightTheme = ThemeHelper.isLightThemeSelected(context)
} catch (e: IOException) { val licenseBackgroundColor = getHexRGBColor(
throw IllegalArgumentException( context, if (isLightTheme) R.color.light_license_background_color else R.color.dark_license_background_color
"Could not get license file: " + license.filename, e )
) val licenseTextColor = getHexRGBColor(
context, if (isLightTheme) R.color.light_license_text_color else R.color.dark_license_text_color
)
val youtubePrimaryColor = getHexRGBColor(
context, if (isLightTheme) R.color.light_youtube_primary_color else R.color.dark_youtube_primary_color
)
return "body{padding:12px 15px;margin:0;background:#$licenseBackgroundColor;color:#$licenseTextColor}" +
"a[href]{color:#$youtubePrimaryColor}pre{white-space:pre-wrap}"
}
/**
* Cast R.color to a hexadecimal color value.
*
* @param context the context to use
* @param color the color number from R.color
* @return a six characters long String with hexadecimal RGB values
*/
private fun getHexRGBColor(context: Context, color: Int): String {
return context.getString(color).substring(3)
}
fun showLicense(context: Context?, component: SoftwareComponent): Disposable {
return showLicense(context, component.license) {
setPositiveButton(R.string.dismiss) { dialog, _ ->
dialog.dismiss()
} }
return webViewData setNeutralButton(R.string.open_website_license) { _, _ ->
} ShareUtils.openUrlInBrowser(context!!, component.link)
/**
* @param context the Android context
* @return String which is a CSS stylesheet according to the context's theme
*/
private fun getLicenseStylesheet(context: Context): String {
val isLightTheme = ThemeHelper.isLightThemeSelected(context)
return (
"body{padding:12px 15px;margin:0;" + "background:#" + getHexRGBColor(
context,
if (isLightTheme) R.color.light_license_background_color
else R.color.dark_license_background_color
) + ";" + "color:#" + getHexRGBColor(
context,
if (isLightTheme) R.color.light_license_text_color
else R.color.dark_license_text_color
) + "}" + "a[href]{color:#" + getHexRGBColor(
context,
if (isLightTheme) R.color.light_youtube_primary_color
else R.color.dark_youtube_primary_color
) + "}" + "pre{white-space:pre-wrap}"
)
}
/**
* Cast R.color to a hexadecimal color value.
*
* @param context the context to use
* @param color the color number from R.color
* @return a six characters long String with hexadecimal RGB values
*/
private fun getHexRGBColor(context: Context, color: Int): String {
return context.getString(color).substring(3)
}
fun showLicense(context: Context?, license: License): Disposable {
return showLicense(context, license) { alertDialog ->
alertDialog.setPositiveButton(R.string.ok) { dialog, _ ->
dialog.dismiss()
}
}
}
fun showLicense(context: Context?, component: SoftwareComponent): Disposable {
return showLicense(context, component.license) { alertDialog ->
alertDialog.setPositiveButton(R.string.dismiss) { dialog, _ ->
dialog.dismiss()
}
alertDialog.setNeutralButton(R.string.open_website_license) { _, _ ->
ShareUtils.openUrlInBrowser(context!!, component.link)
}
}
}
private fun showLicense(
context: Context?,
license: License,
block: (AlertDialog.Builder) -> Unit
): Disposable {
return if (context == null) {
Disposable.empty()
} else {
Observable.fromCallable { getFormattedLicense(context, license) }
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe { formattedLicense ->
val webViewData = Base64.encodeToString(
formattedLicense.toByteArray(StandardCharsets.UTF_8), Base64.NO_PADDING
)
val webView = WebView(context)
webView.loadData(webViewData, "text/html; charset=UTF-8", "base64")
AlertDialog.Builder(context).apply {
setTitle(license.name)
setView(webView)
Localization.assureCorrectAppLanguage(context)
block(this)
show()
}
}
} }
} }
} }
fun showLicense(context: Context?, license: License) = showLicense(context, license) {
setPositiveButton(R.string.ok) { dialog, _ -> dialog.dismiss() }
}
private fun showLicense(
context: Context?,
license: License,
block: AlertDialog.Builder.() -> AlertDialog.Builder
): Disposable {
return if (context == null) {
Disposable.empty()
} else {
Observable.fromCallable { getFormattedLicense(context, license) }
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe { formattedLicense ->
val webViewData =
Base64.encodeToString(formattedLicense.toByteArray(), Base64.NO_PADDING)
val webView = WebView(context)
webView.loadData(webViewData, "text/html; charset=UTF-8", "base64")
Localization.assureCorrectAppLanguage(context)
AlertDialog.Builder(context)
.setTitle(license.name)
.setView(webView)
.block()
.show()
}
}
}

View File

@ -3,7 +3,6 @@ package org.schabi.newpipe.database;
import androidx.room.Dao; import androidx.room.Dao;
import androidx.room.Delete; import androidx.room.Delete;
import androidx.room.Insert; import androidx.room.Insert;
import androidx.room.OnConflictStrategy;
import androidx.room.Update; import androidx.room.Update;
import java.util.Collection; import java.util.Collection;
@ -14,13 +13,10 @@ import io.reactivex.rxjava3.core.Flowable;
@Dao @Dao
public interface BasicDAO<Entity> { public interface BasicDAO<Entity> {
/* Inserts */ /* Inserts */
@Insert(onConflict = OnConflictStrategy.ABORT) @Insert
long insert(Entity entity); long insert(Entity entity);
@Insert(onConflict = OnConflictStrategy.ABORT) @Insert
List<Long> insertAll(Entity... entities);
@Insert(onConflict = OnConflictStrategy.ABORT)
List<Long> insertAll(Collection<Entity> entities); List<Long> insertAll(Collection<Entity> entities);
/* Searches */ /* Searches */
@ -32,9 +28,6 @@ public interface BasicDAO<Entity> {
@Delete @Delete
void delete(Entity entity); void delete(Entity entity);
@Delete
int delete(Collection<Entity> entities);
int deleteAll(); int deleteAll();
/* Updates */ /* Updates */

View File

@ -9,6 +9,7 @@ import androidx.room.Update
import io.reactivex.rxjava3.core.Flowable import io.reactivex.rxjava3.core.Flowable
import io.reactivex.rxjava3.core.Maybe import io.reactivex.rxjava3.core.Maybe
import org.schabi.newpipe.database.feed.model.FeedEntity import org.schabi.newpipe.database.feed.model.FeedEntity
import org.schabi.newpipe.database.feed.model.FeedGroupEntity
import org.schabi.newpipe.database.feed.model.FeedLastUpdatedEntity import org.schabi.newpipe.database.feed.model.FeedLastUpdatedEntity
import org.schabi.newpipe.database.stream.StreamWithState import org.schabi.newpipe.database.stream.StreamWithState
import org.schabi.newpipe.database.stream.model.StreamStateEntity import org.schabi.newpipe.database.stream.model.StreamStateEntity
@ -21,56 +22,16 @@ abstract class FeedDAO {
@Query("DELETE FROM feed") @Query("DELETE FROM feed")
abstract fun deleteAll(): Int abstract fun deleteAll(): Int
@Query(
"""
SELECT s.*, sst.progress_time
FROM streams s
LEFT JOIN stream_state sst
ON s.uid = sst.stream_id
LEFT JOIN stream_history sh
ON s.uid = sh.stream_id
INNER JOIN feed f
ON s.uid = f.stream_id
ORDER BY s.upload_date IS NULL DESC, s.upload_date DESC, s.uploader ASC
LIMIT 500
"""
)
abstract fun getAllStreams(): Maybe<List<StreamWithState>>
@Query(
"""
SELECT s.*, sst.progress_time
FROM streams s
LEFT JOIN stream_state sst
ON s.uid = sst.stream_id
LEFT JOIN stream_history sh
ON s.uid = sh.stream_id
INNER JOIN feed f
ON s.uid = f.stream_id
INNER JOIN feed_group_subscription_join fgs
ON fgs.subscription_id = f.subscription_id
WHERE fgs.group_id = :groupId
ORDER BY s.upload_date IS NULL DESC, s.upload_date DESC, s.uploader ASC
LIMIT 500
"""
)
abstract fun getAllStreamsForGroup(groupId: Long): Maybe<List<StreamWithState>>
/** /**
* @param groupId the group id to get feed streams of; use
* [FeedGroupEntity.GROUP_ALL_ID] to not filter by group
* @param includePlayed if false, only return all of the live, never-played or non-finished
* feed streams (see `@see` items); if true no filter is applied
* @param uploadDateBefore get only streams uploaded before this date (useful to filter out
* future streams); use null to not filter by upload date
* @return the feed streams filtered according to the conditions provided in the parameters
* @see StreamStateEntity.isFinished() * @see StreamStateEntity.isFinished()
* @see StreamStateEntity.PLAYBACK_FINISHED_END_MILLISECONDS * @see StreamStateEntity.PLAYBACK_FINISHED_END_MILLISECONDS
* @return all of the non-live, never-played and non-finished streams in the feed
* (all of the cited conditions must hold for a stream to be in the returned list)
*/ */
@Query( @Query(
""" """
@ -79,67 +40,44 @@ abstract class FeedDAO {
LEFT JOIN stream_state sst LEFT JOIN stream_state sst
ON s.uid = sst.stream_id ON s.uid = sst.stream_id
LEFT JOIN stream_history sh LEFT JOIN stream_history sh
ON s.uid = sh.stream_id ON s.uid = sh.stream_id
INNER JOIN feed f INNER JOIN feed f
ON s.uid = f.stream_id ON s.uid = f.stream_id
LEFT JOIN feed_group_subscription_join fgs
ON fgs.subscription_id = f.subscription_id
WHERE ( WHERE (
sh.stream_id IS NULL :groupId = ${FeedGroupEntity.GROUP_ALL_ID}
OR sst.stream_id IS NULL OR fgs.group_id = :groupId
OR sst.progress_time < s.duration * 1000 - ${StreamStateEntity.PLAYBACK_FINISHED_END_MILLISECONDS}
OR sst.progress_time < s.duration * 1000 * 3 / 4
OR s.stream_type = 'LIVE_STREAM'
OR s.stream_type = 'AUDIO_LIVE_STREAM'
) )
ORDER BY s.upload_date IS NULL DESC, s.upload_date DESC, s.uploader ASC
LIMIT 500
"""
)
abstract fun getLiveOrNotPlayedStreams(): Maybe<List<StreamWithState>>
/**
* @see StreamStateEntity.isFinished()
* @see StreamStateEntity.PLAYBACK_FINISHED_END_MILLISECONDS
* @param groupId the group id to get streams of
* @return all of the non-live, never-played and non-finished streams for the given feed group
* (all of the cited conditions must hold for a stream to be in the returned list)
*/
@Query(
"""
SELECT s.*, sst.progress_time
FROM streams s
LEFT JOIN stream_state sst
ON s.uid = sst.stream_id
LEFT JOIN stream_history sh
ON s.uid = sh.stream_id
INNER JOIN feed f
ON s.uid = f.stream_id
INNER JOIN feed_group_subscription_join fgs
ON fgs.subscription_id = f.subscription_id
WHERE fgs.group_id = :groupId
AND ( AND (
sh.stream_id IS NULL :includePlayed
OR sh.stream_id IS NULL
OR sst.stream_id IS NULL OR sst.stream_id IS NULL
OR sst.progress_time < s.duration * 1000 - ${StreamStateEntity.PLAYBACK_FINISHED_END_MILLISECONDS} OR sst.progress_time < s.duration * 1000 - ${StreamStateEntity.PLAYBACK_FINISHED_END_MILLISECONDS}
OR sst.progress_time < s.duration * 1000 * 3 / 4 OR sst.progress_time < s.duration * 1000 * 3 / 4
OR s.stream_type = 'LIVE_STREAM' OR s.stream_type = 'LIVE_STREAM'
OR s.stream_type = 'AUDIO_LIVE_STREAM' OR s.stream_type = 'AUDIO_LIVE_STREAM'
) )
AND (
:uploadDateBefore IS NULL
OR s.upload_date IS NULL
OR s.upload_date < :uploadDateBefore
)
ORDER BY s.upload_date IS NULL DESC, s.upload_date DESC, s.uploader ASC ORDER BY s.upload_date IS NULL DESC, s.upload_date DESC, s.uploader ASC
LIMIT 500 LIMIT 500
""" """
) )
abstract fun getLiveOrNotPlayedStreamsForGroup(groupId: Long): Maybe<List<StreamWithState>> abstract fun getStreams(
groupId: Long,
includePlayed: Boolean,
uploadDateBefore: OffsetDateTime?
): Maybe<List<StreamWithState>>
@Query( @Query(
""" """

View File

@ -3,10 +3,10 @@ package org.schabi.newpipe.database.playlist;
import org.schabi.newpipe.database.LocalItem; import org.schabi.newpipe.database.LocalItem;
import org.schabi.newpipe.database.playlist.model.PlaylistRemoteEntity; import org.schabi.newpipe.database.playlist.model.PlaylistRemoteEntity;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator; import java.util.Comparator;
import java.util.List; import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.Stream;
public interface PlaylistLocalItem extends LocalItem { public interface PlaylistLocalItem extends LocalItem {
String getOrderingName(); String getOrderingName();
@ -14,14 +14,9 @@ public interface PlaylistLocalItem extends LocalItem {
static List<PlaylistLocalItem> merge( static List<PlaylistLocalItem> merge(
final List<PlaylistMetadataEntry> localPlaylists, final List<PlaylistMetadataEntry> localPlaylists,
final List<PlaylistRemoteEntity> remotePlaylists) { final List<PlaylistRemoteEntity> remotePlaylists) {
final List<PlaylistLocalItem> items = new ArrayList<>( return Stream.concat(localPlaylists.stream(), remotePlaylists.stream())
localPlaylists.size() + remotePlaylists.size()); .sorted(Comparator.comparing(PlaylistLocalItem::getOrderingName,
items.addAll(localPlaylists); Comparator.nullsLast(String.CASE_INSENSITIVE_ORDER)))
items.addAll(remotePlaylists); .collect(Collectors.toList());
Collections.sort(items, Comparator.comparing(PlaylistLocalItem::getOrderingName,
Comparator.nullsLast(String.CASE_INSENSITIVE_ORDER)));
return items;
} }
} }

View File

@ -1,5 +1,9 @@
package org.schabi.newpipe.download; package org.schabi.newpipe.download;
import static org.schabi.newpipe.extractor.stream.DeliveryMethod.PROGRESSIVE_HTTP;
import static org.schabi.newpipe.util.ListHelper.getStreamsOfSpecifiedDelivery;
import static org.schabi.newpipe.util.Localization.assureCorrectAppLanguage;
import android.app.Activity; import android.app.Activity;
import android.content.ComponentName; import android.content.ComponentName;
import android.content.Context; import android.content.Context;
@ -82,10 +86,6 @@ import us.shandian.giga.service.DownloadManagerService;
import us.shandian.giga.service.DownloadManagerService.DownloadManagerBinder; import us.shandian.giga.service.DownloadManagerService.DownloadManagerBinder;
import us.shandian.giga.service.MissionState; import us.shandian.giga.service.MissionState;
import static org.schabi.newpipe.extractor.stream.DeliveryMethod.PROGRESSIVE_HTTP;
import static org.schabi.newpipe.util.ListHelper.getStreamsOfSpecifiedDelivery;
import static org.schabi.newpipe.util.Localization.assureCorrectAppLanguage;
public class DownloadDialog extends DialogFragment public class DownloadDialog extends DialogFragment
implements RadioGroup.OnCheckedChangeListener, AdapterView.OnItemSelectedListener { implements RadioGroup.OnCheckedChangeListener, AdapterView.OnItemSelectedListener {
private static final String TAG = "DialogFragment"; private static final String TAG = "DialogFragment";
@ -205,8 +205,8 @@ public class DownloadDialog extends DialogFragment
setStyle(STYLE_NO_TITLE, ThemeHelper.getDialogTheme(context)); setStyle(STYLE_NO_TITLE, ThemeHelper.getDialogTheme(context));
Icepick.restoreInstanceState(this, savedInstanceState); Icepick.restoreInstanceState(this, savedInstanceState);
final SparseArray<SecondaryStreamHelper<AudioStream>> secondaryStreams final SparseArray<SecondaryStreamHelper<AudioStream>> secondaryStreams =
= new SparseArray<>(4); new SparseArray<>(4);
final List<VideoStream> videoStreams = wrappedVideoStreams.getStreamsList(); final List<VideoStream> videoStreams = wrappedVideoStreams.getStreamsList();
for (int i = 0; i < videoStreams.size(); i++) { for (int i = 0; i < videoStreams.size(); i++) {

View File

@ -31,6 +31,7 @@ import org.schabi.newpipe.util.external_communication.ShareUtils;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter; import java.time.format.DateTimeFormatter;
import java.util.Arrays; import java.util.Arrays;
import java.util.stream.Collectors;
/* /*
* Created by Christian Schabesberger on 24.10.15. * Created by Christian Schabesberger on 24.10.15.
@ -65,11 +66,11 @@ public class ErrorActivity extends AppCompatActivity {
public static final String ERROR_EMAIL_ADDRESS = "crashreport@newpipe.schabi.org"; public static final String ERROR_EMAIL_ADDRESS = "crashreport@newpipe.schabi.org";
public static final String ERROR_EMAIL_SUBJECT = "Exception in "; public static final String ERROR_EMAIL_SUBJECT = "Exception in ";
public static final String ERROR_GITHUB_ISSUE_URL public static final String ERROR_GITHUB_ISSUE_URL =
= "https://github.com/TeamNewPipe/NewPipe/issues"; "https://github.com/TeamNewPipe/NewPipe/issues";
public static final DateTimeFormatter CURRENT_TIMESTAMP_FORMATTER public static final DateTimeFormatter CURRENT_TIMESTAMP_FORMATTER =
= DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm"); DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm");
private ErrorInfo errorInfo; private ErrorInfo errorInfo;
@ -182,14 +183,9 @@ public class ErrorActivity extends AppCompatActivity {
} }
private String formErrorText(final String[] el) { private String formErrorText(final String[] el) {
final StringBuilder text = new StringBuilder(); final String separator = "-------------------------------------";
if (el != null) { return Arrays.stream(el)
for (final String e : el) { .collect(Collectors.joining(separator + "\n", separator + "\n", separator));
text.append("-------------------------------------\n").append(e);
}
}
text.append("-------------------------------------");
return text.toString();
} }
/** /**

View File

@ -14,8 +14,6 @@ import org.schabi.newpipe.extractor.exceptions.ExtractionException
import org.schabi.newpipe.extractor.services.youtube.extractors.YoutubeStreamExtractor.DeobfuscateException import org.schabi.newpipe.extractor.services.youtube.extractors.YoutubeStreamExtractor.DeobfuscateException
import org.schabi.newpipe.ktx.isNetworkRelated import org.schabi.newpipe.ktx.isNetworkRelated
import org.schabi.newpipe.util.ServiceHelper import org.schabi.newpipe.util.ServiceHelper
import java.io.PrintWriter
import java.io.StringWriter
@Parcelize @Parcelize
class ErrorInfo( class ErrorInfo(
@ -80,19 +78,10 @@ class ErrorInfo(
companion object { companion object {
const val SERVICE_NONE = "none" const val SERVICE_NONE = "none"
private fun getStackTrace(throwable: Throwable): String { fun throwableToStringList(throwable: Throwable) = arrayOf(throwable.stackTraceToString())
StringWriter().use { stringWriter ->
PrintWriter(stringWriter, true).use { printWriter ->
throwable.printStackTrace(printWriter)
return stringWriter.buffer.toString()
}
}
}
fun throwableToStringList(throwable: Throwable) = arrayOf(getStackTrace(throwable)) fun throwableListToStringList(throwableList: List<Throwable>) =
throwableList.map { it.stackTraceToString() }.toTypedArray()
fun throwableListToStringList(throwable: List<Throwable>) =
Array(throwable.size) { i -> getStackTrace(throwable[i]) }
private fun getInfoServiceName(info: Info?) = private fun getInfoServiceName(info: Info?) =
if (info == null) SERVICE_NONE else ServiceHelper.getNameOfServiceById(info.serviceId) if (info == null) SERVICE_NONE else ServiceHelper.getNameOfServiceById(info.serviceId)

View File

@ -114,13 +114,7 @@ class ErrorUtil {
context, context,
context.getString(R.string.error_report_channel_id) context.getString(R.string.error_report_channel_id)
) )
.setSmallIcon( .setSmallIcon(R.drawable.ic_bug_report)
// the vector drawable icon causes crashes on KitKat devices
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP)
R.drawable.ic_bug_report
else
android.R.drawable.stat_notify_error
)
.setContentTitle(context.getString(R.string.error_report_notification_title)) .setContentTitle(context.getString(R.string.error_report_notification_title))
.setContentText(context.getString(errorInfo.messageStringId)) .setContentText(context.getString(errorInfo.messageStringId))
.setAutoCancel(true) .setAutoCancel(true)

View File

@ -3,14 +3,15 @@ package org.schabi.newpipe.error;
import android.annotation.SuppressLint; import android.annotation.SuppressLint;
import android.content.Intent; import android.content.Intent;
import android.content.SharedPreferences; import android.content.SharedPreferences;
import android.os.Build;
import android.os.Bundle; import android.os.Bundle;
import android.util.Log; import android.util.Log;
import android.view.Menu; import android.view.Menu;
import android.view.MenuItem; import android.view.MenuItem;
import android.webkit.CookieManager; import android.webkit.CookieManager;
import android.webkit.WebResourceRequest;
import android.webkit.WebSettings; import android.webkit.WebSettings;
import android.webkit.WebView; import android.webkit.WebView;
import android.webkit.WebViewClient;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
@ -18,7 +19,6 @@ import androidx.appcompat.app.ActionBar;
import androidx.appcompat.app.AppCompatActivity; import androidx.appcompat.app.AppCompatActivity;
import androidx.core.app.NavUtils; import androidx.core.app.NavUtils;
import androidx.preference.PreferenceManager; import androidx.preference.PreferenceManager;
import androidx.webkit.WebViewClientCompat;
import org.schabi.newpipe.databinding.ActivityRecaptchaBinding; import org.schabi.newpipe.databinding.ActivityRecaptchaBinding;
import org.schabi.newpipe.DownloaderImpl; import org.schabi.newpipe.DownloaderImpl;
@ -86,14 +86,15 @@ public class ReCaptchaActivity extends AppCompatActivity {
webSettings.setJavaScriptEnabled(true); webSettings.setJavaScriptEnabled(true);
webSettings.setUserAgentString(DownloaderImpl.USER_AGENT); webSettings.setUserAgentString(DownloaderImpl.USER_AGENT);
recaptchaBinding.reCaptchaWebView.setWebViewClient(new WebViewClientCompat() { recaptchaBinding.reCaptchaWebView.setWebViewClient(new WebViewClient() {
@Override @Override
public boolean shouldOverrideUrlLoading(final WebView view, final String url) { public boolean shouldOverrideUrlLoading(final WebView view,
final WebResourceRequest request) {
if (MainActivity.DEBUG) { if (MainActivity.DEBUG) {
Log.d(TAG, "shouldOverrideUrlLoading: url=" + url); Log.d(TAG, "shouldOverrideUrlLoading: url=" + request.getUrl().toString());
} }
handleCookiesFromUrl(url); handleCookiesFromUrl(request.getUrl().toString());
return false; return false;
} }
@ -107,12 +108,7 @@ public class ReCaptchaActivity extends AppCompatActivity {
// cleaning cache, history and cookies from webView // cleaning cache, history and cookies from webView
recaptchaBinding.reCaptchaWebView.clearCache(true); recaptchaBinding.reCaptchaWebView.clearCache(true);
recaptchaBinding.reCaptchaWebView.clearHistory(); recaptchaBinding.reCaptchaWebView.clearHistory();
final CookieManager cookieManager = CookieManager.getInstance(); CookieManager.getInstance().removeAllCookies(null);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
cookieManager.removeAllCookies(value -> { });
} else {
cookieManager.removeAllCookie();
}
recaptchaBinding.reCaptchaWebView.loadUrl(url); recaptchaBinding.reCaptchaWebView.loadUrl(url);
} }

View File

@ -1,5 +1,9 @@
package org.schabi.newpipe.fragments.detail; package org.schabi.newpipe.fragments.detail;
import static android.text.TextUtils.isEmpty;
import static org.schabi.newpipe.extractor.stream.StreamExtractor.NO_AGE_LIMIT;
import static org.schabi.newpipe.extractor.utils.Utils.isBlank;
import android.os.Bundle; import android.os.Bundle;
import android.view.LayoutInflater; import android.view.LayoutInflater;
import android.view.View; import android.view.View;
@ -26,17 +30,9 @@ import org.schabi.newpipe.util.NavigationHelper;
import org.schabi.newpipe.util.external_communication.ShareUtils; import org.schabi.newpipe.util.external_communication.ShareUtils;
import org.schabi.newpipe.util.external_communication.TextLinkifier; import org.schabi.newpipe.util.external_communication.TextLinkifier;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import icepick.State; import icepick.State;
import io.reactivex.rxjava3.disposables.CompositeDisposable; import io.reactivex.rxjava3.disposables.CompositeDisposable;
import static android.text.TextUtils.isEmpty;
import static org.schabi.newpipe.extractor.stream.StreamExtractor.NO_AGE_LIMIT;
import static org.schabi.newpipe.extractor.utils.Utils.isBlank;
public class DescriptionFragment extends BaseFragment { public class DescriptionFragment extends BaseFragment {
@State @State
@ -185,8 +181,8 @@ public class DescriptionFragment extends BaseFragment {
return; return;
} }
final ItemMetadataBinding itemBinding final ItemMetadataBinding itemBinding =
= ItemMetadataBinding.inflate(inflater, layout, false); ItemMetadataBinding.inflate(inflater, layout, false);
itemBinding.metadataTypeView.setText(type); itemBinding.metadataTypeView.setText(type);
itemBinding.metadataTypeView.setOnLongClickListener(v -> { itemBinding.metadataTypeView.setOnLongClickListener(v -> {
@ -206,19 +202,16 @@ public class DescriptionFragment extends BaseFragment {
private void addTagsMetadataItem(final LayoutInflater inflater, final LinearLayout layout) { private void addTagsMetadataItem(final LayoutInflater inflater, final LinearLayout layout) {
if (streamInfo.getTags() != null && !streamInfo.getTags().isEmpty()) { if (streamInfo.getTags() != null && !streamInfo.getTags().isEmpty()) {
final ItemMetadataTagsBinding itemBinding final var itemBinding = ItemMetadataTagsBinding.inflate(inflater, layout, false);
= ItemMetadataTagsBinding.inflate(inflater, layout, false);
final List<String> tags = new ArrayList<>(streamInfo.getTags()); streamInfo.getTags().stream().sorted(String.CASE_INSENSITIVE_ORDER).forEach(tag -> {
Collections.sort(tags);
for (final String tag : tags) {
final Chip chip = (Chip) inflater.inflate(R.layout.chip, final Chip chip = (Chip) inflater.inflate(R.layout.chip,
itemBinding.metadataTagsChips, false); itemBinding.metadataTagsChips, false);
chip.setText(tag); chip.setText(tag);
chip.setOnClickListener(this::onTagClick); chip.setOnClickListener(this::onTagClick);
chip.setOnLongClickListener(this::onTagLongClick); chip.setOnLongClickListener(this::onTagLongClick);
itemBinding.metadataTagsChips.addView(chip); itemBinding.metadataTagsChips.addView(chip);
} });
layout.addView(itemBinding.getRoot()); layout.addView(itemBinding.getRoot());
} }

View File

@ -1,5 +1,16 @@
package org.schabi.newpipe.fragments.detail; package org.schabi.newpipe.fragments.detail;
import static android.text.TextUtils.isEmpty;
import static org.schabi.newpipe.extractor.StreamingService.ServiceInfo.MediaCapability.COMMENTS;
import static org.schabi.newpipe.extractor.stream.StreamExtractor.NO_AGE_LIMIT;
import static org.schabi.newpipe.ktx.ViewUtils.animate;
import static org.schabi.newpipe.ktx.ViewUtils.animateRotation;
import static org.schabi.newpipe.player.helper.PlayerHelper.globalScreenOrientationLocked;
import static org.schabi.newpipe.player.helper.PlayerHelper.isClearingQueueConfirmationRequired;
import static org.schabi.newpipe.player.playqueue.PlayQueueItem.RECOVERY_UNSET;
import static org.schabi.newpipe.util.ExtractorHelper.showMetaInfoInTextView;
import static org.schabi.newpipe.util.ListHelper.getUrlAndNonTorrentStreams;
import android.animation.ValueAnimator; import android.animation.ValueAnimator;
import android.app.Activity; import android.app.Activity;
import android.content.BroadcastReceiver; import android.content.BroadcastReceiver;
@ -10,7 +21,6 @@ import android.content.SharedPreferences;
import android.content.pm.ActivityInfo; import android.content.pm.ActivityInfo;
import android.database.ContentObserver; import android.database.ContentObserver;
import android.graphics.Color; import android.graphics.Color;
import android.graphics.Point;
import android.graphics.Rect; import android.graphics.Rect;
import android.graphics.drawable.Drawable; import android.graphics.drawable.Drawable;
import android.net.Uri; import android.net.Uri;
@ -77,9 +87,9 @@ import org.schabi.newpipe.fragments.list.videos.RelatedItemsFragment;
import org.schabi.newpipe.ktx.AnimationType; import org.schabi.newpipe.ktx.AnimationType;
import org.schabi.newpipe.local.dialog.PlaylistDialog; import org.schabi.newpipe.local.dialog.PlaylistDialog;
import org.schabi.newpipe.local.history.HistoryRecordManager; import org.schabi.newpipe.local.history.HistoryRecordManager;
import org.schabi.newpipe.player.MainPlayer;
import org.schabi.newpipe.player.MainPlayer.PlayerType;
import org.schabi.newpipe.player.Player; import org.schabi.newpipe.player.Player;
import org.schabi.newpipe.player.PlayerService;
import org.schabi.newpipe.player.PlayerType;
import org.schabi.newpipe.player.event.OnKeyDownListener; import org.schabi.newpipe.player.event.OnKeyDownListener;
import org.schabi.newpipe.player.event.PlayerServiceExtendedEventListener; import org.schabi.newpipe.player.event.PlayerServiceExtendedEventListener;
import org.schabi.newpipe.player.helper.PlayerHelper; import org.schabi.newpipe.player.helper.PlayerHelper;
@ -87,6 +97,8 @@ import org.schabi.newpipe.player.helper.PlayerHolder;
import org.schabi.newpipe.player.playqueue.PlayQueue; import org.schabi.newpipe.player.playqueue.PlayQueue;
import org.schabi.newpipe.player.playqueue.PlayQueueItem; import org.schabi.newpipe.player.playqueue.PlayQueueItem;
import org.schabi.newpipe.player.playqueue.SinglePlayQueue; import org.schabi.newpipe.player.playqueue.SinglePlayQueue;
import org.schabi.newpipe.player.ui.MainPlayerUi;
import org.schabi.newpipe.player.ui.VideoPlayerUi;
import org.schabi.newpipe.util.Constants; import org.schabi.newpipe.util.Constants;
import org.schabi.newpipe.util.DeviceUtils; import org.schabi.newpipe.util.DeviceUtils;
import org.schabi.newpipe.util.ExtractorHelper; import org.schabi.newpipe.util.ExtractorHelper;
@ -101,11 +113,11 @@ import org.schabi.newpipe.util.external_communication.KoreUtils;
import org.schabi.newpipe.util.external_communication.ShareUtils; import org.schabi.newpipe.util.external_communication.ShareUtils;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collections;
import java.util.Iterator; import java.util.Iterator;
import java.util.LinkedList; import java.util.LinkedList;
import java.util.List; import java.util.List;
import java.util.Objects; import java.util.Objects;
import java.util.Optional;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import icepick.State; import icepick.State;
@ -114,17 +126,6 @@ import io.reactivex.rxjava3.disposables.CompositeDisposable;
import io.reactivex.rxjava3.disposables.Disposable; import io.reactivex.rxjava3.disposables.Disposable;
import io.reactivex.rxjava3.schedulers.Schedulers; import io.reactivex.rxjava3.schedulers.Schedulers;
import static android.text.TextUtils.isEmpty;
import static org.schabi.newpipe.extractor.StreamingService.ServiceInfo.MediaCapability.COMMENTS;
import static org.schabi.newpipe.extractor.stream.StreamExtractor.NO_AGE_LIMIT;
import static org.schabi.newpipe.ktx.ViewUtils.animate;
import static org.schabi.newpipe.ktx.ViewUtils.animateRotation;
import static org.schabi.newpipe.player.helper.PlayerHelper.globalScreenOrientationLocked;
import static org.schabi.newpipe.player.helper.PlayerHelper.isClearingQueueConfirmationRequired;
import static org.schabi.newpipe.player.playqueue.PlayQueueItem.RECOVERY_UNSET;
import static org.schabi.newpipe.util.ExtractorHelper.showMetaInfoInTextView;
import static org.schabi.newpipe.util.ListHelper.getUrlAndNonTorrentStreams;
public final class VideoDetailFragment public final class VideoDetailFragment
extends BaseStateFragment<StreamInfo> extends BaseStateFragment<StreamInfo>
implements BackPressable, implements BackPressable,
@ -179,6 +180,8 @@ public final class VideoDetailFragment
@State @State
int bottomSheetState = BottomSheetBehavior.STATE_EXPANDED; int bottomSheetState = BottomSheetBehavior.STATE_EXPANDED;
@State @State
int lastStableBottomSheetState = BottomSheetBehavior.STATE_EXPANDED;
@State
protected boolean autoPlayEnabled = true; protected boolean autoPlayEnabled = true;
@Nullable @Nullable
@ -190,6 +193,7 @@ public final class VideoDetailFragment
private Disposable positionSubscriber = null; private Disposable positionSubscriber = null;
private BottomSheetBehavior<FrameLayout> bottomSheetBehavior; private BottomSheetBehavior<FrameLayout> bottomSheetBehavior;
private BottomSheetBehavior.BottomSheetCallback bottomSheetCallback;
private BroadcastReceiver broadcastReceiver; private BroadcastReceiver broadcastReceiver;
/*////////////////////////////////////////////////////////////////////////// /*//////////////////////////////////////////////////////////////////////////
@ -202,7 +206,7 @@ public final class VideoDetailFragment
private ContentObserver settingsContentObserver; private ContentObserver settingsContentObserver;
@Nullable @Nullable
private MainPlayer playerService; private PlayerService playerService;
private Player player; private Player player;
private final PlayerHolder playerHolder = PlayerHolder.getInstance(); private final PlayerHolder playerHolder = PlayerHolder.getInstance();
@ -211,7 +215,7 @@ public final class VideoDetailFragment
//////////////////////////////////////////////////////////////////////////*/ //////////////////////////////////////////////////////////////////////////*/
@Override @Override
public void onServiceConnected(final Player connectedPlayer, public void onServiceConnected(final Player connectedPlayer,
final MainPlayer connectedPlayerService, final PlayerService connectedPlayerService,
final boolean playAfterConnect) { final boolean playAfterConnect) {
player = connectedPlayer; player = connectedPlayer;
playerService = connectedPlayerService; playerService = connectedPlayerService;
@ -219,6 +223,7 @@ public final class VideoDetailFragment
// It will do nothing if the player is not in fullscreen mode // It will do nothing if the player is not in fullscreen mode
hideSystemUiIfNeeded(); hideSystemUiIfNeeded();
final Optional<MainPlayerUi> playerUi = player.UIs().get(MainPlayerUi.class);
if (!player.videoPlayerSelected() && !playAfterConnect) { if (!player.videoPlayerSelected() && !playAfterConnect) {
return; return;
} }
@ -227,22 +232,19 @@ public final class VideoDetailFragment
// If the video is playing but orientation changed // If the video is playing but orientation changed
// let's make the video in fullscreen again // let's make the video in fullscreen again
checkLandscape(); checkLandscape();
} else if (player.isFullscreen() && !player.isVerticalVideo() } else if (playerUi.map(ui -> ui.isFullscreen() && !ui.isVerticalVideo()).orElse(false)
// Tablet UI has orientation-independent fullscreen // Tablet UI has orientation-independent fullscreen
&& !DeviceUtils.isTablet(activity)) { && !DeviceUtils.isTablet(activity)) {
// Device is in portrait orientation after rotation but UI is in fullscreen. // Device is in portrait orientation after rotation but UI is in fullscreen.
// Return back to non-fullscreen state // Return back to non-fullscreen state
player.toggleFullscreen(); playerUi.ifPresent(MainPlayerUi::toggleFullscreen);
}
if (playerIsNotStopped() && player.videoPlayerSelected()) {
addVideoPlayerView();
} }
//noinspection SimplifyOptionalCallChains
if (playAfterConnect if (playAfterConnect
|| (currentInfo != null || (currentInfo != null
&& isAutoplayEnabled() && isAutoplayEnabled()
&& player.getParentActivity() == null)) { && !playerUi.isPresent())) {
autoPlayEnabled = true; // forcefully start playing autoPlayEnabled = true; // forcefully start playing
openVideoPlayerAutoFullscreen(); openVideoPlayerAutoFullscreen();
} }
@ -269,7 +271,7 @@ public final class VideoDetailFragment
public static VideoDetailFragment getInstanceInCollapsedState() { public static VideoDetailFragment getInstanceInCollapsedState() {
final VideoDetailFragment instance = new VideoDetailFragment(); final VideoDetailFragment instance = new VideoDetailFragment();
instance.bottomSheetState = BottomSheetBehavior.STATE_COLLAPSED; instance.updateBottomSheetState(BottomSheetBehavior.STATE_COLLAPSED);
return instance; return instance;
} }
@ -329,6 +331,9 @@ public final class VideoDetailFragment
@Override @Override
public void onResume() { public void onResume() {
super.onResume(); super.onResume();
if (DEBUG) {
Log.d(TAG, "onResume() called");
}
activity.sendBroadcast(new Intent(ACTION_VIDEO_FRAGMENT_RESUMED)); activity.sendBroadcast(new Intent(ACTION_VIDEO_FRAGMENT_RESUMED));
@ -383,7 +388,7 @@ public final class VideoDetailFragment
disposables.clear(); disposables.clear();
positionSubscriber = null; positionSubscriber = null;
currentWorker = null; currentWorker = null;
bottomSheetBehavior.setBottomSheetCallback(null); bottomSheetBehavior.removeBottomSheetCallback(bottomSheetCallback);
if (activity.isFinishing()) { if (activity.isFinishing()) {
playQueue = null; playQueue = null;
@ -449,7 +454,7 @@ public final class VideoDetailFragment
disposables.add( disposables.add(
PlaylistDialog.createCorrespondingDialog( PlaylistDialog.createCorrespondingDialog(
getContext(), getContext(),
Collections.singletonList(new StreamEntity(currentInfo)), List.of(new StreamEntity(currentInfo)),
dialog -> dialog.show(getFM(), TAG) dialog -> dialog.show(getFM(), TAG)
) )
); );
@ -500,12 +505,18 @@ public final class VideoDetailFragment
} }
break; break;
case R.id.detail_thumbnail_root_layout: case R.id.detail_thumbnail_root_layout:
autoPlayEnabled = true; // forcefully start playing // make sure not to open any player if there is nothing currently loaded!
// FIXME Workaround #7427 // FIXME removing this `if` causes the player service to start correctly, then stop,
if (isPlayerAvailable()) { // then restart badly without calling `startForeground()`, causing a crash when
player.setRecovery(); // later closing the detail fragment
if (currentInfo != null) {
autoPlayEnabled = true; // forcefully start playing
// FIXME Workaround #7427
if (isPlayerAvailable()) {
player.setRecovery();
}
openVideoPlayerAutoFullscreen();
} }
openVideoPlayerAutoFullscreen();
break; break;
case R.id.detail_title_root_layout: case R.id.detail_title_root_layout:
toggleTitleAndSecondaryControls(); toggleTitleAndSecondaryControls();
@ -518,7 +529,7 @@ public final class VideoDetailFragment
case R.id.overlay_play_pause_button: case R.id.overlay_play_pause_button:
if (playerIsNotStopped()) { if (playerIsNotStopped()) {
player.playPause(); player.playPause();
player.hideControls(0, 0); player.UIs().get(VideoPlayerUi.class).ifPresent(ui -> ui.hideControls(0, 0));
showSystemUi(); showSystemUi();
} else { } else {
autoPlayEnabled = true; // forcefully start playing autoPlayEnabled = true; // forcefully start playing
@ -583,12 +594,12 @@ public final class VideoDetailFragment
if (binding.detailSecondaryControlPanel.getVisibility() == View.GONE) { if (binding.detailSecondaryControlPanel.getVisibility() == View.GONE) {
binding.detailVideoTitleView.setMaxLines(10); binding.detailVideoTitleView.setMaxLines(10);
animateRotation(binding.detailToggleSecondaryControlsView, animateRotation(binding.detailToggleSecondaryControlsView,
Player.DEFAULT_CONTROLS_DURATION, 180); VideoPlayerUi.DEFAULT_CONTROLS_DURATION, 180);
binding.detailSecondaryControlPanel.setVisibility(View.VISIBLE); binding.detailSecondaryControlPanel.setVisibility(View.VISIBLE);
} else { } else {
binding.detailVideoTitleView.setMaxLines(1); binding.detailVideoTitleView.setMaxLines(1);
animateRotation(binding.detailToggleSecondaryControlsView, animateRotation(binding.detailToggleSecondaryControlsView,
Player.DEFAULT_CONTROLS_DURATION, 0); VideoPlayerUi.DEFAULT_CONTROLS_DURATION, 0);
binding.detailSecondaryControlPanel.setVisibility(View.GONE); binding.detailSecondaryControlPanel.setVisibility(View.GONE);
} }
// view pager height has changed, update the tab layout // view pager height has changed, update the tab layout
@ -714,7 +725,7 @@ public final class VideoDetailFragment
} }
private void initThumbnailViews(@NonNull final StreamInfo info) { private void initThumbnailViews(@NonNull final StreamInfo info) {
PicassoHelper.loadThumbnail(info.getThumbnailUrl()).tag(PICASSO_VIDEO_DETAILS_TAG) PicassoHelper.loadDetailsThumbnail(info.getThumbnailUrl()).tag(PICASSO_VIDEO_DETAILS_TAG)
.into(binding.detailThumbnailImageView, new Callback() { .into(binding.detailThumbnailImageView, new Callback() {
@Override @Override
public void onSuccess() { public void onSuccess() {
@ -746,7 +757,9 @@ public final class VideoDetailFragment
@Override @Override
public boolean onKeyDown(final int keyCode) { public boolean onKeyDown(final int keyCode) {
return isPlayerAvailable() && player.onKeyDown(keyCode); return isPlayerAvailable()
&& player.UIs().get(VideoPlayerUi.class)
.map(playerUi -> playerUi.onKeyDown(keyCode)).orElse(false);
} }
@Override @Override
@ -756,7 +769,7 @@ public final class VideoDetailFragment
} }
// If we are in fullscreen mode just exit from it via first back press // If we are in fullscreen mode just exit from it via first back press
if (isPlayerAvailable() && player.isFullscreen()) { if (isFullscreen()) {
if (!DeviceUtils.isTablet(activity)) { if (!DeviceUtils.isTablet(activity)) {
player.pause(); player.pause();
} }
@ -1006,8 +1019,7 @@ public final class VideoDetailFragment
getChildFragmentManager().beginTransaction() getChildFragmentManager().beginTransaction()
.replace(R.id.relatedItemsLayout, RelatedItemsFragment.getInstance(info)) .replace(R.id.relatedItemsLayout, RelatedItemsFragment.getInstance(info))
.commitAllowingStateLoss(); .commitAllowingStateLoss();
binding.relatedItemsLayout.setVisibility( binding.relatedItemsLayout.setVisibility(isFullscreen() ? View.GONE : View.VISIBLE);
isPlayerAvailable() && player.isFullscreen() ? View.GONE : View.VISIBLE);
} }
} }
@ -1047,15 +1059,13 @@ public final class VideoDetailFragment
// call `post()` to be sure `viewPager.getHitRect()` // call `post()` to be sure `viewPager.getHitRect()`
// is up to date and not being currently recomputed // is up to date and not being currently recomputed
binding.tabLayout.post(() -> { binding.tabLayout.post(() -> {
if (getContext() != null) { final var activity = getActivity();
if (activity != null) {
final Rect pagerHitRect = new Rect(); final Rect pagerHitRect = new Rect();
binding.viewPager.getHitRect(pagerHitRect); binding.viewPager.getHitRect(pagerHitRect);
final Point displaySize = new Point(); final int height = DeviceUtils.getWindowHeight(activity.getWindowManager());
Objects.requireNonNull(ContextCompat.getSystemService(getContext(), final int viewPagerVisibleHeight = height - pagerHitRect.top;
WindowManager.class)).getDefaultDisplay().getSize(displaySize);
final int viewPagerVisibleHeight = displaySize.y - pagerHitRect.top;
// see TabLayout.DEFAULT_HEIGHT, which is equal to 48dp // see TabLayout.DEFAULT_HEIGHT, which is equal to 48dp
final float tabLayoutHeight = TypedValue.applyDimension( final float tabLayoutHeight = TypedValue.applyDimension(
TypedValue.COMPLEX_UNIT_DIP, 48, getResources().getDisplayMetrics()); TypedValue.COMPLEX_UNIT_DIP, 48, getResources().getDisplayMetrics());
@ -1087,8 +1097,12 @@ public final class VideoDetailFragment
private void toggleFullscreenIfInFullscreenMode() { private void toggleFullscreenIfInFullscreenMode() {
// If a user watched video inside fullscreen mode and than chose another player // If a user watched video inside fullscreen mode and than chose another player
// return to non-fullscreen mode // return to non-fullscreen mode
if (isPlayerAvailable() && player.isFullscreen()) { if (isPlayerAvailable()) {
player.toggleFullscreen(); player.UIs().get(MainPlayerUi.class).ifPresent(playerUi -> {
if (playerUi.isFullscreen()) {
playerUi.toggleFullscreen();
}
});
} }
} }
@ -1164,7 +1178,7 @@ public final class VideoDetailFragment
// doesn't tell which state it was settling to, and thus the bottom sheet settles to // doesn't tell which state it was settling to, and thus the bottom sheet settles to
// STATE_COLLAPSED. This can be solved by manually setting the state that will be // STATE_COLLAPSED. This can be solved by manually setting the state that will be
// restored (i.e. bottomSheetState) to STATE_EXPANDED. // restored (i.e. bottomSheetState) to STATE_EXPANDED.
bottomSheetState = BottomSheetBehavior.STATE_EXPANDED; updateBottomSheetState(BottomSheetBehavior.STATE_EXPANDED);
// toggle landscape in order to open directly in fullscreen // toggle landscape in order to open directly in fullscreen
onScreenRotationButtonClicked(); onScreenRotationButtonClicked();
} }
@ -1214,16 +1228,10 @@ public final class VideoDetailFragment
} }
final PlayQueue queue = setupPlayQueueForIntent(false); final PlayQueue queue = setupPlayQueueForIntent(false);
tryAddVideoPlayerView();
// Video view can have elements visible from popup,
// We hide it here but once it ready the view will be shown in handleIntent()
if (playerService.getView() != null) {
playerService.getView().setVisibility(View.GONE);
}
addVideoPlayerView();
final Intent playerIntent = NavigationHelper.getPlayerIntent(requireContext(), final Intent playerIntent = NavigationHelper.getPlayerIntent(requireContext(),
MainPlayer.class, queue, true, autoPlayEnabled); PlayerService.class, queue, true, autoPlayEnabled);
ContextCompat.startForegroundService(activity, playerIntent); ContextCompat.startForegroundService(activity, playerIntent);
} }
@ -1235,8 +1243,8 @@ public final class VideoDetailFragment
* be reused in a few milliseconds and the flickering would be annoying. * be reused in a few milliseconds and the flickering would be annoying.
*/ */
private void hideMainPlayerOnLoadingNewStream() { private void hideMainPlayerOnLoadingNewStream() {
if (!isPlayerServiceAvailable() //noinspection SimplifyOptionalCallChains
|| playerService.getView() == null if (!isPlayerServiceAvailable() || !getRoot().isPresent()
|| !player.videoPlayerSelected()) { || !player.videoPlayerSelected()) {
return; return;
} }
@ -1244,7 +1252,7 @@ public final class VideoDetailFragment
removeVideoPlayerView(); removeVideoPlayerView();
if (isAutoplayEnabled()) { if (isAutoplayEnabled()) {
playerService.stopForImmediateReusing(); playerService.stopForImmediateReusing();
playerService.getView().setVisibility(View.GONE); getRoot().ifPresent(view -> view.setVisibility(View.GONE));
} else { } else {
playerHolder.stopService(); playerHolder.stopService();
} }
@ -1301,27 +1309,41 @@ public final class VideoDetailFragment
&& PlayerHelper.isAutoplayAllowedByUser(requireContext()); && PlayerHelper.isAutoplayAllowedByUser(requireContext());
} }
private void addVideoPlayerView() { private void tryAddVideoPlayerView() {
if (!isPlayerAvailable() || getView() == null) { if (isPlayerAvailable() && getView() != null) {
return; // Setup the surface view height, so that it fits the video correctly; this is done also
// here, and not only in the Handler, to avoid a choppy fullscreen rotation animation.
setHeightThumbnail();
} }
// Check if viewHolder already contains a child // do all the null checks in the posted lambda, too, since the player, the binding and the
if (player.getRootView().getParent() != binding.playerPlaceholder) { // view could be set or unset before the lambda gets executed on the next main thread cycle
playerService.removeViewFromParent(); new Handler(Looper.getMainLooper()).post(() -> {
} if (!isPlayerAvailable() || getView() == null) {
setHeightThumbnail(); return;
}
// Prevent from re-adding a view multiple times // setup the surface view height, so that it fits the video correctly
if (player.getRootView().getParent() == null) { setHeightThumbnail();
binding.playerPlaceholder.addView(player.getRootView());
} player.UIs().get(MainPlayerUi.class).ifPresent(playerUi -> {
// sometimes binding would be null here, even though getView() != null above u.u
if (binding != null) {
// prevent from re-adding a view multiple times
playerUi.removeViewFromParent();
binding.playerPlaceholder.addView(playerUi.getBinding().getRoot());
playerUi.setupVideoSurfaceIfNeeded();
}
});
});
} }
private void removeVideoPlayerView() { private void removeVideoPlayerView() {
makeDefaultHeightForVideoPlaceholder(); makeDefaultHeightForVideoPlaceholder();
playerService.removeViewFromParent(); if (player != null) {
player.UIs().get(VideoPlayerUi.class).ifPresent(VideoPlayerUi::removeViewFromParent);
}
} }
private void makeDefaultHeightForVideoPlaceholder() { private void makeDefaultHeightForVideoPlaceholder() {
@ -1362,7 +1384,7 @@ public final class VideoDetailFragment
final boolean isPortrait = metrics.heightPixels > metrics.widthPixels; final boolean isPortrait = metrics.heightPixels > metrics.widthPixels;
requireView().getViewTreeObserver().removeOnPreDrawListener(preDrawListener); requireView().getViewTreeObserver().removeOnPreDrawListener(preDrawListener);
if (isPlayerAvailable() && player.isFullscreen()) { if (isFullscreen()) {
final int height = (DeviceUtils.isInMultiWindow(activity) final int height = (DeviceUtils.isInMultiWindow(activity)
? requireView() ? requireView()
: activity.getWindow().getDecorView()).getHeight(); : activity.getWindow().getDecorView()).getHeight();
@ -1387,8 +1409,9 @@ public final class VideoDetailFragment
binding.detailThumbnailImageView.setMinimumHeight(newHeight); binding.detailThumbnailImageView.setMinimumHeight(newHeight);
if (isPlayerAvailable()) { if (isPlayerAvailable()) {
final int maxHeight = (int) (metrics.heightPixels * MAX_PLAYER_HEIGHT); final int maxHeight = (int) (metrics.heightPixels * MAX_PLAYER_HEIGHT);
player.getSurfaceView() player.UIs().get(VideoPlayerUi.class).ifPresent(ui ->
.setHeights(newHeight, player.isFullscreen() ? newHeight : maxHeight); ui.getBinding().surfaceView.setHeights(newHeight,
ui.isFullscreen() ? newHeight : maxHeight));
} }
} }
@ -1517,7 +1540,7 @@ public final class VideoDetailFragment
if (binding.relatedItemsLayout != null) { if (binding.relatedItemsLayout != null) {
if (showRelatedItems) { if (showRelatedItems) {
binding.relatedItemsLayout.setVisibility( binding.relatedItemsLayout.setVisibility(
isPlayerAvailable() && player.isFullscreen() ? View.GONE : View.INVISIBLE); isFullscreen() ? View.GONE : View.INVISIBLE);
} else { } else {
binding.relatedItemsLayout.setVisibility(View.GONE); binding.relatedItemsLayout.setVisibility(View.GONE);
} }
@ -1551,7 +1574,8 @@ public final class VideoDetailFragment
binding.detailUploaderThumbnailView.setVisibility(View.GONE); binding.detailUploaderThumbnailView.setVisibility(View.GONE);
} }
final Drawable buddyDrawable = AppCompatResources.getDrawable(activity, R.drawable.buddy); final Drawable buddyDrawable =
AppCompatResources.getDrawable(activity, R.drawable.placeholder_person);
binding.detailSubChannelThumbnailView.setImageDrawable(buddyDrawable); binding.detailSubChannelThumbnailView.setImageDrawable(buddyDrawable);
binding.detailUploaderThumbnailView.setImageDrawable(buddyDrawable); binding.detailUploaderThumbnailView.setImageDrawable(buddyDrawable);
@ -1778,6 +1802,11 @@ public final class VideoDetailFragment
// Player event listener // Player event listener
//////////////////////////////////////////////////////////////////////////*/ //////////////////////////////////////////////////////////////////////////*/
@Override
public void onViewCreated() {
tryAddVideoPlayerView();
}
@Override @Override
public void onQueueUpdate(final PlayQueue queue) { public void onQueueUpdate(final PlayQueue queue) {
playQueue = queue; playQueue = queue;
@ -1898,15 +1927,10 @@ public final class VideoDetailFragment
@Override @Override
public void onFullscreenStateChanged(final boolean fullscreen) { public void onFullscreenStateChanged(final boolean fullscreen) {
setupBrightness(); setupBrightness();
//noinspection SimplifyOptionalCallChains
if (!isPlayerAndPlayerServiceAvailable() if (!isPlayerAndPlayerServiceAvailable()
|| playerService.getView() == null || !player.UIs().get(MainPlayerUi.class).isPresent()
|| player.getParentActivity() == null) { || getRoot().map(View::getParent).orElse(null) == null) {
return;
}
final View view = playerService.getView();
final ViewGroup parent = (ViewGroup) view.getParent();
if (parent == null) {
return; return;
} }
@ -1922,13 +1946,7 @@ public final class VideoDetailFragment
} }
scrollToTop(); scrollToTop();
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { tryAddVideoPlayerView();
addVideoPlayerView();
} else {
// KitKat needs a delay before addVideoPlayerView call or it reports wrong height in
// activity.getWindow().getDecorView().getHeight()
new Handler().post(this::addVideoPlayerView);
}
} }
@Override @Override
@ -1940,7 +1958,7 @@ public final class VideoDetailFragment
final boolean isLandscape = DeviceUtils.isLandscape(requireContext()); final boolean isLandscape = DeviceUtils.isLandscape(requireContext());
if (DeviceUtils.isTablet(activity) if (DeviceUtils.isTablet(activity)
&& (!globalScreenOrientationLocked(activity) || isLandscape)) { && (!globalScreenOrientationLocked(activity) || isLandscape)) {
player.toggleFullscreen(); player.UIs().get(MainPlayerUi.class).ifPresent(MainPlayerUi::toggleFullscreen);
return; return;
} }
@ -1991,10 +2009,8 @@ public final class VideoDetailFragment
} }
activity.getWindow().getDecorView().setSystemUiVisibility(0); activity.getWindow().getDecorView().setSystemUiVisibility(0);
activity.getWindow().clearFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN); activity.getWindow().clearFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { activity.getWindow().setStatusBarColor(ThemeHelper.resolveColorFromAttr(
activity.getWindow().setStatusBarColor(ThemeHelper.resolveColorFromAttr( requireContext(), android.R.attr.colorPrimary));
requireContext(), android.R.attr.colorPrimary));
}
} }
private void hideSystemUi() { private void hideSystemUi() {
@ -2025,8 +2041,7 @@ public final class VideoDetailFragment
} }
activity.getWindow().getDecorView().setSystemUiVisibility(visibility); activity.getWindow().getDecorView().setSystemUiVisibility(visibility);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP if (isInMultiWindow || isFullscreen()) {
&& (isInMultiWindow || (isPlayerAvailable() && player.isFullscreen()))) {
activity.getWindow().setStatusBarColor(Color.TRANSPARENT); activity.getWindow().setStatusBarColor(Color.TRANSPARENT);
activity.getWindow().setNavigationBarColor(Color.TRANSPARENT); activity.getWindow().setNavigationBarColor(Color.TRANSPARENT);
} }
@ -2034,14 +2049,19 @@ public final class VideoDetailFragment
} }
// Listener implementation // Listener implementation
@Override
public void hideSystemUiIfNeeded() { public void hideSystemUiIfNeeded() {
if (isPlayerAvailable() if (isFullscreen()
&& player.isFullscreen()
&& bottomSheetBehavior.getState() == BottomSheetBehavior.STATE_EXPANDED) { && bottomSheetBehavior.getState() == BottomSheetBehavior.STATE_EXPANDED) {
hideSystemUi(); hideSystemUi();
} }
} }
private boolean isFullscreen() {
return isPlayerAvailable() && player.UIs().get(VideoPlayerUi.class)
.map(VideoPlayerUi::isFullscreen).orElse(false);
}
private boolean playerIsNotStopped() { private boolean playerIsNotStopped() {
return isPlayerAvailable() && !player.isStopped(); return isPlayerAvailable() && !player.isStopped();
} }
@ -2064,10 +2084,7 @@ public final class VideoDetailFragment
} }
final WindowManager.LayoutParams lp = activity.getWindow().getAttributes(); final WindowManager.LayoutParams lp = activity.getWindow().getAttributes();
if (!isPlayerAvailable() if (!isFullscreen() || bottomSheetState != BottomSheetBehavior.STATE_EXPANDED) {
|| !player.videoPlayerSelected()
|| !player.isFullscreen()
|| bottomSheetState != BottomSheetBehavior.STATE_EXPANDED) {
// Apply system brightness when the player is not in fullscreen // Apply system brightness when the player is not in fullscreen
restoreDefaultBrightness(); restoreDefaultBrightness();
} else { } else {
@ -2091,7 +2108,7 @@ public final class VideoDetailFragment
setAutoPlay(true); setAutoPlay(true);
} }
player.checkLandscape(); player.UIs().get(MainPlayerUi.class).ifPresent(MainPlayerUi::checkLandscape);
// Let's give a user time to look at video information page if video is not playing // Let's give a user time to look at video information page if video is not playing
if (globalScreenOrientationLocked(activity) && !player.isPlaying()) { if (globalScreenOrientationLocked(activity) && !player.isPlaying()) {
player.play(); player.play();
@ -2170,12 +2187,8 @@ public final class VideoDetailFragment
} else { } else {
final int selectedVideoStreamIndexForExternalPlayers = final int selectedVideoStreamIndexForExternalPlayers =
ListHelper.getDefaultResolutionIndex(activity, videoStreamsForExternalPlayers); ListHelper.getDefaultResolutionIndex(activity, videoStreamsForExternalPlayers);
final CharSequence[] resolutions = final CharSequence[] resolutions = videoStreamsForExternalPlayers.stream()
new CharSequence[videoStreamsForExternalPlayers.size()]; .map(VideoStream::getResolution).toArray(CharSequence[]::new);
for (int i = 0; i < videoStreamsForExternalPlayers.size(); i++) {
resolutions[i] = videoStreamsForExternalPlayers.get(i).getResolution();
}
builder.setSingleChoiceItems(resolutions, selectedVideoStreamIndexForExternalPlayers, builder.setSingleChoiceItems(resolutions, selectedVideoStreamIndexForExternalPlayers,
null); null);
@ -2279,7 +2292,9 @@ public final class VideoDetailFragment
final FrameLayout bottomSheetLayout = activity.findViewById(R.id.fragment_player_holder); final FrameLayout bottomSheetLayout = activity.findViewById(R.id.fragment_player_holder);
bottomSheetBehavior = BottomSheetBehavior.from(bottomSheetLayout); bottomSheetBehavior = BottomSheetBehavior.from(bottomSheetLayout);
bottomSheetBehavior.setState(bottomSheetState); bottomSheetBehavior.setState(lastStableBottomSheetState);
updateBottomSheetState(lastStableBottomSheetState);
final int peekHeight = getResources().getDimensionPixelSize(R.dimen.mini_player_height); final int peekHeight = getResources().getDimensionPixelSize(R.dimen.mini_player_height);
if (bottomSheetState != BottomSheetBehavior.STATE_HIDDEN) { if (bottomSheetState != BottomSheetBehavior.STATE_HIDDEN) {
manageSpaceAtTheBottom(false); manageSpaceAtTheBottom(false);
@ -2292,10 +2307,10 @@ public final class VideoDetailFragment
} }
} }
bottomSheetBehavior.setBottomSheetCallback(new BottomSheetBehavior.BottomSheetCallback() { bottomSheetCallback = new BottomSheetBehavior.BottomSheetCallback() {
@Override @Override
public void onStateChanged(@NonNull final View bottomSheet, final int newState) { public void onStateChanged(@NonNull final View bottomSheet, final int newState) {
bottomSheetState = newState; updateBottomSheetState(newState);
switch (newState) { switch (newState) {
case BottomSheetBehavior.STATE_HIDDEN: case BottomSheetBehavior.STATE_HIDDEN:
@ -2318,10 +2333,10 @@ public final class VideoDetailFragment
if (DeviceUtils.isLandscape(requireContext()) if (DeviceUtils.isLandscape(requireContext())
&& isPlayerAvailable() && isPlayerAvailable()
&& player.isPlaying() && player.isPlaying()
&& !player.isFullscreen() && !isFullscreen()
&& !DeviceUtils.isTablet(activity) && !DeviceUtils.isTablet(activity)) {
&& player.videoPlayerSelected()) { player.UIs().get(MainPlayerUi.class)
player.toggleFullscreen(); .ifPresent(MainPlayerUi::toggleFullscreen);
} }
setOverlayLook(binding.appBarLayout, behavior, 1); setOverlayLook(binding.appBarLayout, behavior, 1);
break; break;
@ -2334,19 +2349,26 @@ public final class VideoDetailFragment
// Re-enable clicks // Re-enable clicks
setOverlayElementsClickable(true); setOverlayElementsClickable(true);
if (isPlayerAvailable()) { if (isPlayerAvailable()) {
player.closeItemsList(); player.UIs().get(MainPlayerUi.class)
.ifPresent(MainPlayerUi::closeItemsList);
} }
setOverlayLook(binding.appBarLayout, behavior, 0); setOverlayLook(binding.appBarLayout, behavior, 0);
break; break;
case BottomSheetBehavior.STATE_DRAGGING: case BottomSheetBehavior.STATE_DRAGGING:
case BottomSheetBehavior.STATE_SETTLING: case BottomSheetBehavior.STATE_SETTLING:
if (isPlayerAvailable() && player.isFullscreen()) { if (isFullscreen()) {
showSystemUi(); showSystemUi();
} }
if (isPlayerAvailable() && player.isControlsVisible()) { if (isPlayerAvailable()) {
player.hideControls(0, 0); player.UIs().get(MainPlayerUi.class).ifPresent(ui -> {
if (ui.isControlsVisible()) {
ui.hideControls(0, 0);
}
});
} }
break; break;
case BottomSheetBehavior.STATE_HALF_EXPANDED:
break;
} }
} }
@ -2354,7 +2376,9 @@ public final class VideoDetailFragment
public void onSlide(@NonNull final View bottomSheet, final float slideOffset) { public void onSlide(@NonNull final View bottomSheet, final float slideOffset) {
setOverlayLook(binding.appBarLayout, behavior, slideOffset); setOverlayLook(binding.appBarLayout, behavior, slideOffset);
} }
}); };
bottomSheetBehavior.addBottomSheetCallback(bottomSheetCallback);
// User opened a new page and the player will hide itself // User opened a new page and the player will hide itself
activity.getSupportFragmentManager().addOnBackStackChangedListener(() -> { activity.getSupportFragmentManager().addOnBackStackChangedListener(() -> {
@ -2369,8 +2393,8 @@ public final class VideoDetailFragment
@Nullable final String thumbnailUrl) { @Nullable final String thumbnailUrl) {
binding.overlayTitleTextView.setText(isEmpty(overlayTitle) ? "" : overlayTitle); binding.overlayTitleTextView.setText(isEmpty(overlayTitle) ? "" : overlayTitle);
binding.overlayChannelTextView.setText(isEmpty(uploader) ? "" : uploader); binding.overlayChannelTextView.setText(isEmpty(uploader) ? "" : uploader);
binding.overlayThumbnail.setImageResource(R.drawable.dummy_thumbnail_dark); binding.overlayThumbnail.setImageDrawable(null);
PicassoHelper.loadThumbnail(thumbnailUrl).tag(PICASSO_VIDEO_DETAILS_TAG) PicassoHelper.loadDetailsThumbnail(thumbnailUrl).tag(PICASSO_VIDEO_DETAILS_TAG)
.into(binding.overlayThumbnail); .into(binding.overlayThumbnail);
} }
@ -2418,4 +2442,21 @@ public final class VideoDetailFragment
boolean isPlayerAndPlayerServiceAvailable() { boolean isPlayerAndPlayerServiceAvailable() {
return (player != null && playerService != null); return (player != null && playerService != null);
} }
public Optional<View> getRoot() {
if (player == null) {
return Optional.empty();
}
return player.UIs().get(VideoPlayerUi.class)
.map(playerUi -> playerUi.getBinding().getRoot());
}
private void updateBottomSheetState(final int newState) {
bottomSheetState = newState;
if (newState != BottomSheetBehavior.STATE_DRAGGING
&& newState != BottomSheetBehavior.STATE_SETTLING) {
lastStableBottomSheetState = newState;
}
}
} }

View File

@ -6,6 +6,7 @@ import static com.google.android.exoplayer2.PlaybackException.ERROR_CODE_UNSPECI
import android.content.Context; import android.content.Context;
import android.util.Log; import android.util.Log;
import android.util.Pair;
import android.view.ContextThemeWrapper; import android.view.ContextThemeWrapper;
import android.view.LayoutInflater; import android.view.LayoutInflater;
import android.view.ViewGroup; import android.view.ViewGroup;
@ -28,9 +29,7 @@ import org.schabi.newpipe.player.Player;
import org.schabi.newpipe.util.ThemeHelper; import org.schabi.newpipe.util.ThemeHelper;
import java.io.IOException; import java.io.IOException;
import java.util.Collections; import java.util.List;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.function.Supplier; import java.util.function.Supplier;
/** /**
@ -43,50 +42,34 @@ public final class VideoDetailPlayerCrasher {
// https://stackoverflow.com/a/54744028 // https://stackoverflow.com/a/54744028
private static final String TAG = "VideoDetPlayerCrasher"; private static final String TAG = "VideoDetPlayerCrasher";
private static final Map<String, Supplier<ExoPlaybackException>> AVAILABLE_EXCEPTION_TYPES = private static final String DEFAULT_MSG = "Dummy";
getExceptionTypes();
private static final List<Pair<String, Supplier<ExoPlaybackException>>>
AVAILABLE_EXCEPTION_TYPES = List.of(
new Pair<>("Source", () -> ExoPlaybackException.createForSource(
new IOException(DEFAULT_MSG),
ERROR_CODE_BEHIND_LIVE_WINDOW
)),
new Pair<>("Renderer", () -> ExoPlaybackException.createForRenderer(
new Exception(DEFAULT_MSG),
"Dummy renderer",
0,
null,
C.FORMAT_HANDLED,
/*isRecoverable=*/false,
ERROR_CODE_DECODING_FAILED
)),
new Pair<>("Unexpected", () -> ExoPlaybackException.createForUnexpected(
new RuntimeException(DEFAULT_MSG),
ERROR_CODE_UNSPECIFIED
)),
new Pair<>("Remote", () -> ExoPlaybackException.createForRemote(DEFAULT_MSG))
);
private VideoDetailPlayerCrasher() { private VideoDetailPlayerCrasher() {
// No impls // No impls
} }
private static Map<String, Supplier<ExoPlaybackException>> getExceptionTypes() {
final String defaultMsg = "Dummy";
final Map<String, Supplier<ExoPlaybackException>> exceptionTypes = new LinkedHashMap<>();
exceptionTypes.put(
"Source",
() -> ExoPlaybackException.createForSource(
new IOException(defaultMsg),
ERROR_CODE_BEHIND_LIVE_WINDOW
)
);
exceptionTypes.put(
"Renderer",
() -> ExoPlaybackException.createForRenderer(
new Exception(defaultMsg),
"Dummy renderer",
0,
null,
C.FORMAT_HANDLED,
/*isRecoverable=*/false,
ERROR_CODE_DECODING_FAILED
)
);
exceptionTypes.put(
"Unexpected",
() -> ExoPlaybackException.createForUnexpected(
new RuntimeException(defaultMsg),
ERROR_CODE_UNSPECIFIED
)
);
exceptionTypes.put(
"Remote",
() -> ExoPlaybackException.createForRemote(defaultMsg)
);
return Collections.unmodifiableMap(exceptionTypes);
}
private static Context getThemeWrapperContext(final Context context) { private static Context getThemeWrapperContext(final Context context) {
return new ContextThemeWrapper( return new ContextThemeWrapper(
context, context,
@ -121,10 +104,9 @@ public final class VideoDetailPlayerCrasher {
.setNegativeButton(R.string.cancel, null) .setNegativeButton(R.string.cancel, null)
.create(); .create();
for (final Map.Entry<String, Supplier<ExoPlaybackException>> entry for (final Pair<String, Supplier<ExoPlaybackException>> entry : AVAILABLE_EXCEPTION_TYPES) {
: AVAILABLE_EXCEPTION_TYPES.entrySet()) {
final RadioButton radioButton = ListRadioIconItemBinding.inflate(inflater).getRoot(); final RadioButton radioButton = ListRadioIconItemBinding.inflate(inflater).getRoot();
radioButton.setText(entry.getKey()); radioButton.setText(entry.first);
radioButton.setChecked(false); radioButton.setChecked(false);
radioButton.setLayoutParams( radioButton.setLayoutParams(
new RadioGroup.LayoutParams( new RadioGroup.LayoutParams(
@ -133,7 +115,7 @@ public final class VideoDetailPlayerCrasher {
) )
); );
radioButton.setOnClickListener(v -> { radioButton.setOnClickListener(v -> {
tryCrashPlayerWith(player, entry.getValue().get()); tryCrashPlayerWith(player, entry.second.get());
alertDialog.cancel(); alertDialog.cancel();
}); });
binding.list.addView(radioButton); binding.list.addView(radioButton);

View File

@ -23,14 +23,11 @@ import androidx.recyclerview.widget.RecyclerView;
import org.schabi.newpipe.R; import org.schabi.newpipe.R;
import org.schabi.newpipe.error.ErrorUtil; import org.schabi.newpipe.error.ErrorUtil;
import org.schabi.newpipe.extractor.InfoItem; import org.schabi.newpipe.extractor.InfoItem;
import org.schabi.newpipe.extractor.channel.ChannelInfoItem;
import org.schabi.newpipe.extractor.comments.CommentsInfoItem;
import org.schabi.newpipe.extractor.playlist.PlaylistInfoItem;
import org.schabi.newpipe.extractor.stream.StreamInfoItem; import org.schabi.newpipe.extractor.stream.StreamInfoItem;
import org.schabi.newpipe.fragments.BaseStateFragment; import org.schabi.newpipe.fragments.BaseStateFragment;
import org.schabi.newpipe.fragments.OnScrollBelowItemsListener; import org.schabi.newpipe.fragments.OnScrollBelowItemsListener;
import org.schabi.newpipe.info_list.dialog.InfoItemDialog;
import org.schabi.newpipe.info_list.InfoListAdapter; import org.schabi.newpipe.info_list.InfoListAdapter;
import org.schabi.newpipe.info_list.dialog.InfoItemDialog;
import org.schabi.newpipe.util.NavigationHelper; import org.schabi.newpipe.util.NavigationHelper;
import org.schabi.newpipe.util.OnClickGesture; import org.schabi.newpipe.util.OnClickGesture;
import org.schabi.newpipe.util.StateSaver; import org.schabi.newpipe.util.StateSaver;
@ -264,45 +261,28 @@ public abstract class BaseListFragment<I, N> extends BaseStateFragment<I>
} }
}); });
infoListAdapter.setOnChannelSelectedListener(new OnClickGesture<>() { infoListAdapter.setOnChannelSelectedListener(selectedItem -> {
@Override try {
public void selected(final ChannelInfoItem selectedItem) {
try {
onItemSelected(selectedItem);
NavigationHelper.openChannelFragment(getFM(),
selectedItem.getServiceId(),
selectedItem.getUrl(),
selectedItem.getName());
} catch (final Exception e) {
ErrorUtil.showUiErrorSnackbar(
BaseListFragment.this, "Opening channel fragment", e);
}
}
});
infoListAdapter.setOnPlaylistSelectedListener(new OnClickGesture<>() {
@Override
public void selected(final PlaylistInfoItem selectedItem) {
try {
onItemSelected(selectedItem);
NavigationHelper.openPlaylistFragment(getFM(),
selectedItem.getServiceId(),
selectedItem.getUrl(),
selectedItem.getName());
} catch (final Exception e) {
ErrorUtil.showUiErrorSnackbar(BaseListFragment.this,
"Opening playlist fragment", e);
}
}
});
infoListAdapter.setOnCommentsSelectedListener(new OnClickGesture<>() {
@Override
public void selected(final CommentsInfoItem selectedItem) {
onItemSelected(selectedItem); onItemSelected(selectedItem);
NavigationHelper.openChannelFragment(getFM(), selectedItem.getServiceId(),
selectedItem.getUrl(), selectedItem.getName());
} catch (final Exception e) {
ErrorUtil.showUiErrorSnackbar(this, "Opening channel fragment", e);
} }
}); });
infoListAdapter.setOnPlaylistSelectedListener(selectedItem -> {
try {
onItemSelected(selectedItem);
NavigationHelper.openPlaylistFragment(getFM(), selectedItem.getServiceId(),
selectedItem.getUrl(), selectedItem.getName());
} catch (final Exception e) {
ErrorUtil.showUiErrorSnackbar(this, "Opening playlist fragment", e);
}
});
infoListAdapter.setOnCommentsSelectedListener(this::onItemSelected);
// Ensure that there is always a scroll listener (e.g. when rotating the device) // Ensure that there is always a scroll listener (e.g. when rotating the device)
useNormalItemListScrollListener(); useNormalItemListScrollListener();
} }

View File

@ -43,7 +43,7 @@ import org.schabi.newpipe.fragments.list.BaseListInfoFragment;
import org.schabi.newpipe.ktx.AnimationType; import org.schabi.newpipe.ktx.AnimationType;
import org.schabi.newpipe.local.subscription.SubscriptionManager; import org.schabi.newpipe.local.subscription.SubscriptionManager;
import org.schabi.newpipe.local.feed.notifications.NotificationHelper; import org.schabi.newpipe.local.feed.notifications.NotificationHelper;
import org.schabi.newpipe.player.MainPlayer.PlayerType; import org.schabi.newpipe.player.PlayerType;
import org.schabi.newpipe.player.playqueue.ChannelPlayQueue; import org.schabi.newpipe.player.playqueue.ChannelPlayQueue;
import org.schabi.newpipe.player.playqueue.PlayQueue; import org.schabi.newpipe.player.playqueue.PlayQueue;
import org.schabi.newpipe.util.ExtractorHelper; import org.schabi.newpipe.util.ExtractorHelper;
@ -578,17 +578,13 @@ public class ChannelFragment extends BaseListInfoFragment<StreamInfoItem, Channe
} }
private PlayQueue getPlayQueue() { private PlayQueue getPlayQueue() {
return getPlayQueue(0);
}
private PlayQueue getPlayQueue(final int index) {
final List<StreamInfoItem> streamItems = infoListAdapter.getItemsList().stream() final List<StreamInfoItem> streamItems = infoListAdapter.getItemsList().stream()
.filter(StreamInfoItem.class::isInstance) .filter(StreamInfoItem.class::isInstance)
.map(StreamInfoItem.class::cast) .map(StreamInfoItem.class::cast)
.collect(Collectors.toList()); .collect(Collectors.toList());
return new ChannelPlayQueue(currentInfo.getServiceId(), currentInfo.getUrl(), return new ChannelPlayQueue(currentInfo.getServiceId(), currentInfo.getUrl(),
currentInfo.getNextPage(), streamItems, index); currentInfo.getNextPage(), streamItems, 0);
} }
/*////////////////////////////////////////////////////////////////////////// /*//////////////////////////////////////////////////////////////////////////

View File

@ -43,7 +43,7 @@ import org.schabi.newpipe.info_list.dialog.InfoItemDialog;
import org.schabi.newpipe.info_list.dialog.StreamDialogDefaultEntry; import org.schabi.newpipe.info_list.dialog.StreamDialogDefaultEntry;
import org.schabi.newpipe.local.dialog.PlaylistDialog; import org.schabi.newpipe.local.dialog.PlaylistDialog;
import org.schabi.newpipe.local.playlist.RemotePlaylistManager; import org.schabi.newpipe.local.playlist.RemotePlaylistManager;
import org.schabi.newpipe.player.MainPlayer.PlayerType; import org.schabi.newpipe.player.PlayerType;
import org.schabi.newpipe.player.playqueue.PlayQueue; import org.schabi.newpipe.player.playqueue.PlayQueue;
import org.schabi.newpipe.player.playqueue.PlaylistPlayQueue; import org.schabi.newpipe.player.playqueue.PlaylistPlayQueue;
import org.schabi.newpipe.util.ExtractorHelper; import org.schabi.newpipe.util.ExtractorHelper;

View File

@ -200,7 +200,7 @@ public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.I
showLocalSuggestions = NewPipeSettings.showLocalSearchSuggestions(activity, prefs); showLocalSuggestions = NewPipeSettings.showLocalSearchSuggestions(activity, prefs);
showRemoteSuggestions = NewPipeSettings.showRemoteSearchSuggestions(activity, prefs); showRemoteSuggestions = NewPipeSettings.showRemoteSearchSuggestions(activity, prefs);
suggestionListAdapter = new SuggestionListAdapter(activity); suggestionListAdapter = new SuggestionListAdapter();
historyRecordManager = new HistoryRecordManager(context); historyRecordManager = new HistoryRecordManager(context);
} }
@ -340,6 +340,8 @@ public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.I
super.initViews(rootView, savedInstanceState); super.initViews(rootView, savedInstanceState);
searchBinding.suggestionsList.setAdapter(suggestionListAdapter); searchBinding.suggestionsList.setAdapter(suggestionListAdapter);
// animations are just strange and useless, since the suggestions keep changing too much
searchBinding.suggestionsList.setItemAnimator(null);
new ItemTouchHelper(new ItemTouchHelper.Callback() { new ItemTouchHelper(new ItemTouchHelper.Callback() {
@Override @Override
public int getMovementFlags(@NonNull final RecyclerView recyclerView, public int getMovementFlags(@NonNull final RecyclerView recyclerView,
@ -497,9 +499,6 @@ public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.I
+ lastSearchedString); + lastSearchedString);
} }
searchEditText.setText(searchString); searchEditText.setText(searchString);
if (android.os.Build.VERSION.SDK_INT < android.os.Build.VERSION_CODES.LOLLIPOP) {
searchEditText.setHintTextColor(searchEditText.getTextColors().withAlpha(128));
}
if (TextUtils.isEmpty(searchString) || TextUtils.isEmpty(searchEditText.getText())) { if (TextUtils.isEmpty(searchString) || TextUtils.isEmpty(searchEditText.getText())) {
searchToolbarContainer.setTranslationX(100); searchToolbarContainer.setTranslationX(100);
@ -533,7 +532,7 @@ public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.I
searchBinding.correctSuggestion.setVisibility(View.GONE); searchBinding.correctSuggestion.setVisibility(View.GONE);
searchEditText.setText(""); searchEditText.setText("");
suggestionListAdapter.setItems(new ArrayList<>()); suggestionListAdapter.submitList(null);
showKeyboardSearch(); showKeyboardSearch();
}); });
@ -922,7 +921,7 @@ public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.I
filterItemCheckedId = item.getItemId(); filterItemCheckedId = item.getItemId();
item.setChecked(true); item.setChecked(true);
contentFilter = new String[]{theContentFilter.get(0)}; contentFilter = theContentFilter.toArray(new String[0]);
if (!TextUtils.isEmpty(searchString)) { if (!TextUtils.isEmpty(searchString)) {
search(searchString, contentFilter, sortFilter); search(searchString, contentFilter, sortFilter);
@ -947,8 +946,8 @@ public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.I
if (DEBUG) { if (DEBUG) {
Log.d(TAG, "handleSuggestions() called with: suggestions = [" + suggestions + "]"); Log.d(TAG, "handleSuggestions() called with: suggestions = [" + suggestions + "]");
} }
searchBinding.suggestionsList.smoothScrollToPosition(0); suggestionListAdapter.submitList(suggestions,
searchBinding.suggestionsList.post(() -> suggestionListAdapter.setItems(suggestions)); () -> searchBinding.suggestionsList.scrollToPosition(0));
if (suggestionsPanelVisible && isErrorPanelVisible()) { if (suggestionsPanelVisible && isErrorPanelVisible()) {
hideLoading(); hideLoading();
@ -983,8 +982,7 @@ public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.I
isCorrectedSearch = result.isCorrectedSearch(); isCorrectedSearch = result.isCorrectedSearch();
// List<MetaInfo> cannot be bundled without creating some containers // List<MetaInfo> cannot be bundled without creating some containers
metaInfo = new MetaInfo[result.getMetaInfo().size()]; metaInfo = result.getMetaInfo().toArray(new MetaInfo[0]);
metaInfo = result.getMetaInfo().toArray(metaInfo);
showMetaInfoInTextView(result.getMetaInfo(), searchBinding.searchMetaInfoTextView, showMetaInfoInTextView(result.getMetaInfo(), searchBinding.searchMetaInfoTextView,
searchBinding.searchMetaInfoSeparator, disposables); searchBinding.searchMetaInfoSeparator, disposables);
@ -1070,14 +1068,14 @@ public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.I
return 0; return 0;
} }
final SuggestionItem item = suggestionListAdapter.getItem(position); final SuggestionItem item = suggestionListAdapter.getCurrentList().get(position);
return item.fromHistory ? makeMovementFlags(0, return item.fromHistory ? makeMovementFlags(0,
ItemTouchHelper.LEFT | ItemTouchHelper.RIGHT) : 0; ItemTouchHelper.LEFT | ItemTouchHelper.RIGHT) : 0;
} }
public void onSuggestionItemSwiped(@NonNull final RecyclerView.ViewHolder viewHolder) { public void onSuggestionItemSwiped(@NonNull final RecyclerView.ViewHolder viewHolder) {
final int position = viewHolder.getBindingAdapterPosition(); final int position = viewHolder.getBindingAdapterPosition();
final String query = suggestionListAdapter.getItem(position).query; final String query = suggestionListAdapter.getCurrentList().get(position).query;
final Disposable onDelete = historyRecordManager.deleteSearchHistory(query) final Disposable onDelete = historyRecordManager.deleteSearchHistory(query)
.observeOn(AndroidSchedulers.mainThread()) .observeOn(AndroidSchedulers.mainThread())
.subscribe( .subscribe(

View File

@ -1,34 +1,22 @@
package org.schabi.newpipe.fragments.list.search; package org.schabi.newpipe.fragments.list.search;
import android.content.Context;
import android.view.LayoutInflater; import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup; import android.view.ViewGroup;
import android.widget.ImageView;
import android.widget.TextView;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.recyclerview.widget.DiffUtil;
import androidx.recyclerview.widget.ListAdapter;
import androidx.recyclerview.widget.RecyclerView; import androidx.recyclerview.widget.RecyclerView;
import org.schabi.newpipe.R; import org.schabi.newpipe.R;
import org.schabi.newpipe.databinding.ItemSearchSuggestionBinding;
import java.util.ArrayList;
import java.util.List;
public class SuggestionListAdapter public class SuggestionListAdapter
extends RecyclerView.Adapter<SuggestionListAdapter.SuggestionItemHolder> { extends ListAdapter<SuggestionItem, SuggestionListAdapter.SuggestionItemHolder> {
private final ArrayList<SuggestionItem> items = new ArrayList<>();
private final Context context;
private OnSuggestionItemSelected listener; private OnSuggestionItemSelected listener;
public SuggestionListAdapter(final Context context) { public SuggestionListAdapter() {
this.context = context; super(new SuggestionItemCallback());
}
public void setItems(final List<SuggestionItem> items) {
this.items.clear();
this.items.addAll(items);
notifyDataSetChanged();
} }
public void setListener(final OnSuggestionItemSelected listener) { public void setListener(final OnSuggestionItemSelected listener) {
@ -39,45 +27,32 @@ public class SuggestionListAdapter
@Override @Override
public SuggestionItemHolder onCreateViewHolder(@NonNull final ViewGroup parent, public SuggestionItemHolder onCreateViewHolder(@NonNull final ViewGroup parent,
final int viewType) { final int viewType) {
return new SuggestionItemHolder(LayoutInflater.from(context) return new SuggestionItemHolder(ItemSearchSuggestionBinding
.inflate(R.layout.item_search_suggestion, parent, false)); .inflate(LayoutInflater.from(parent.getContext()), parent, false));
} }
@Override @Override
public void onBindViewHolder(final SuggestionItemHolder holder, final int position) { public void onBindViewHolder(final SuggestionItemHolder holder, final int position) {
final SuggestionItem currentItem = getItem(position); final SuggestionItem currentItem = getItem(position);
holder.updateFrom(currentItem); holder.updateFrom(currentItem);
holder.queryView.setOnClickListener(v -> { holder.itemBinding.suggestionSearch.setOnClickListener(v -> {
if (listener != null) { if (listener != null) {
listener.onSuggestionItemSelected(currentItem); listener.onSuggestionItemSelected(currentItem);
} }
}); });
holder.queryView.setOnLongClickListener(v -> { holder.itemBinding.suggestionSearch.setOnLongClickListener(v -> {
if (listener != null) { if (listener != null) {
listener.onSuggestionItemLongClick(currentItem); listener.onSuggestionItemLongClick(currentItem);
} }
return true; return true;
}); });
holder.insertView.setOnClickListener(v -> { holder.itemBinding.suggestionInsert.setOnClickListener(v -> {
if (listener != null) { if (listener != null) {
listener.onSuggestionItemInserted(currentItem); listener.onSuggestionItemInserted(currentItem);
} }
}); });
} }
SuggestionItem getItem(final int position) {
return items.get(position);
}
@Override
public int getItemCount() {
return items.size();
}
public boolean isEmpty() {
return getItemCount() == 0;
}
public interface OnSuggestionItemSelected { public interface OnSuggestionItemSelected {
void onSuggestionItemSelected(SuggestionItem item); void onSuggestionItemSelected(SuggestionItem item);
@ -87,30 +62,32 @@ public class SuggestionListAdapter
} }
public static final class SuggestionItemHolder extends RecyclerView.ViewHolder { public static final class SuggestionItemHolder extends RecyclerView.ViewHolder {
private final TextView itemSuggestionQuery; private final ItemSearchSuggestionBinding itemBinding;
private final ImageView suggestionIcon;
private final View queryView;
private final View insertView;
// Cache some ids, as they can potentially be constantly updated/recycled private SuggestionItemHolder(final ItemSearchSuggestionBinding binding) {
private final int historyResId; super(binding.getRoot());
private final int searchResId; this.itemBinding = binding;
private SuggestionItemHolder(final View rootView) {
super(rootView);
suggestionIcon = rootView.findViewById(R.id.item_suggestion_icon);
itemSuggestionQuery = rootView.findViewById(R.id.item_suggestion_query);
queryView = rootView.findViewById(R.id.suggestion_search);
insertView = rootView.findViewById(R.id.suggestion_insert);
historyResId = R.drawable.ic_history;
searchResId = R.drawable.ic_search;
} }
private void updateFrom(final SuggestionItem item) { private void updateFrom(final SuggestionItem item) {
suggestionIcon.setImageResource(item.fromHistory ? historyResId : searchResId); itemBinding.itemSuggestionIcon.setImageResource(item.fromHistory ? R.drawable.ic_history
itemSuggestionQuery.setText(item.query); : R.drawable.ic_search);
itemBinding.itemSuggestionQuery.setText(item.query);
}
}
private static class SuggestionItemCallback extends DiffUtil.ItemCallback<SuggestionItem> {
@Override
public boolean areItemsTheSame(@NonNull final SuggestionItem oldItem,
@NonNull final SuggestionItem newItem) {
return oldItem.fromHistory == newItem.fromHistory
&& oldItem.query.equals(newItem.query);
}
@Override
public boolean areContentsTheSame(@NonNull final SuggestionItem oldItem,
@NonNull final SuggestionItem newItem) {
return true; // items' contents never change; the list of items themselves does
} }
} }
} }

View File

@ -67,8 +67,8 @@ public class InfoItemBuilder {
public View buildView(@NonNull final ViewGroup parent, @NonNull final InfoItem infoItem, public View buildView(@NonNull final ViewGroup parent, @NonNull final InfoItem infoItem,
final HistoryRecordManager historyRecordManager, final HistoryRecordManager historyRecordManager,
final boolean useMiniVariant) { final boolean useMiniVariant) {
final InfoItemHolder holder final InfoItemHolder holder =
= holderFromInfoType(parent, infoItem.getInfoType(), useMiniVariant); holderFromInfoType(parent, infoItem.getInfoType(), useMiniVariant);
holder.updateFromItem(infoItem, historyRecordManager); holder.updateFromItem(infoItem, historyRecordManager);
return holder.itemView; return holder.itemView;
} }

View File

@ -321,6 +321,7 @@ public final class InfoItemDialog {
*/ */
public Builder addDefaultEndEntries() { public Builder addDefaultEndEntries() {
addAllEntries( addAllEntries(
StreamDialogDefaultEntry.DOWNLOAD,
StreamDialogDefaultEntry.APPEND_PLAYLIST, StreamDialogDefaultEntry.APPEND_PLAYLIST,
StreamDialogDefaultEntry.SHARE, StreamDialogDefaultEntry.SHARE,
StreamDialogDefaultEntry.OPEN_IN_BROWSER StreamDialogDefaultEntry.OPEN_IN_BROWSER

View File

@ -2,6 +2,7 @@ package org.schabi.newpipe.info_list.dialog;
import static org.schabi.newpipe.util.NavigationHelper.openChannelFragment; import static org.schabi.newpipe.util.NavigationHelper.openChannelFragment;
import static org.schabi.newpipe.util.SparseItemUtil.fetchItemInfoIfSparse; import static org.schabi.newpipe.util.SparseItemUtil.fetchItemInfoIfSparse;
import static org.schabi.newpipe.util.SparseItemUtil.fetchStreamInfoAndSaveToDatabase;
import static org.schabi.newpipe.util.SparseItemUtil.fetchUploaderUrlIfSparse; import static org.schabi.newpipe.util.SparseItemUtil.fetchUploaderUrlIfSparse;
import android.net.Uri; import android.net.Uri;
@ -11,6 +12,7 @@ import androidx.annotation.StringRes;
import org.schabi.newpipe.R; import org.schabi.newpipe.R;
import org.schabi.newpipe.database.stream.model.StreamEntity; import org.schabi.newpipe.database.stream.model.StreamEntity;
import org.schabi.newpipe.download.DownloadDialog;
import org.schabi.newpipe.local.dialog.PlaylistAppendDialog; import org.schabi.newpipe.local.dialog.PlaylistAppendDialog;
import org.schabi.newpipe.local.dialog.PlaylistDialog; import org.schabi.newpipe.local.dialog.PlaylistDialog;
import org.schabi.newpipe.local.history.HistoryRecordManager; import org.schabi.newpipe.local.history.HistoryRecordManager;
@ -18,7 +20,7 @@ import org.schabi.newpipe.util.NavigationHelper;
import org.schabi.newpipe.util.external_communication.KoreUtils; import org.schabi.newpipe.util.external_communication.KoreUtils;
import org.schabi.newpipe.util.external_communication.ShareUtils; import org.schabi.newpipe.util.external_communication.ShareUtils;
import java.util.Collections; import java.util.List;
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers; import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
@ -87,7 +89,7 @@ public enum StreamDialogDefaultEntry {
APPEND_PLAYLIST(R.string.add_to_playlist, (fragment, item) -> APPEND_PLAYLIST(R.string.add_to_playlist, (fragment, item) ->
PlaylistDialog.createCorrespondingDialog( PlaylistDialog.createCorrespondingDialog(
fragment.getContext(), fragment.getContext(),
Collections.singletonList(new StreamEntity(item)), List.of(new StreamEntity(item)),
dialog -> dialog.show( dialog -> dialog.show(
fragment.getParentFragmentManager(), fragment.getParentFragmentManager(),
"StreamDialogEntry@" "StreamDialogEntry@"
@ -110,6 +112,15 @@ public enum StreamDialogDefaultEntry {
ShareUtils.shareText(fragment.requireContext(), item.getName(), item.getUrl(), ShareUtils.shareText(fragment.requireContext(), item.getName(), item.getUrl(),
item.getThumbnailUrl())), item.getThumbnailUrl())),
DOWNLOAD(R.string.download, (fragment, item) ->
fetchStreamInfoAndSaveToDatabase(fragment.requireContext(), item.getServiceId(),
item.getUrl(), info -> {
final DownloadDialog downloadDialog =
new DownloadDialog(fragment.requireContext(), info);
downloadDialog.show(fragment.getChildFragmentManager(), "downloadDialog");
})
),
OPEN_IN_BROWSER(R.string.open_in_browser, (fragment, item) -> OPEN_IN_BROWSER(R.string.open_in_browser, (fragment, item) ->
ShareUtils.openUrlInBrowser(fragment.requireContext(), item.getUrl())), ShareUtils.openUrlInBrowser(fragment.requireContext(), item.getUrl())),

View File

@ -42,7 +42,7 @@ public class ChannelMiniInfoItemHolder extends InfoItemHolder {
itemTitleView.setText(item.getName()); itemTitleView.setText(item.getName());
itemAdditionalDetailView.setText(getDetailLine(item)); itemAdditionalDetailView.setText(getDetailLine(item));
PicassoHelper.loadThumbnail(item.getThumbnailUrl()).into(itemThumbnailView); PicassoHelper.loadAvatar(item.getThumbnailUrl()).into(itemThumbnailView);
itemView.setOnClickListener(view -> { itemView.setOnClickListener(view -> {
if (itemBuilder.getOnChannelSelectedListener() != null) { if (itemBuilder.getOnChannelSelectedListener() != null) {

View File

@ -23,9 +23,9 @@ import org.schabi.newpipe.util.CommentTextOnTouchListener;
import org.schabi.newpipe.util.DeviceUtils; import org.schabi.newpipe.util.DeviceUtils;
import org.schabi.newpipe.util.Localization; import org.schabi.newpipe.util.Localization;
import org.schabi.newpipe.util.NavigationHelper; import org.schabi.newpipe.util.NavigationHelper;
import org.schabi.newpipe.util.external_communication.TimestampExtractor;
import org.schabi.newpipe.util.external_communication.ShareUtils;
import org.schabi.newpipe.util.PicassoHelper; import org.schabi.newpipe.util.PicassoHelper;
import org.schabi.newpipe.util.external_communication.ShareUtils;
import org.schabi.newpipe.util.external_communication.TimestampExtractor;
import java.util.regex.Matcher; import java.util.regex.Matcher;
@ -204,8 +204,9 @@ public class CommentsMiniInfoItemHolder extends InfoItemHolder {
boolean hasEllipsis = false; boolean hasEllipsis = false;
if (itemContentView.getLineCount() > COMMENT_DEFAULT_LINES) { if (itemContentView.getLineCount() > COMMENT_DEFAULT_LINES) {
final int endOfLastLine final int endOfLastLine = itemContentView
= itemContentView.getLayout().getLineEnd(COMMENT_DEFAULT_LINES - 1); .getLayout()
.getLineEnd(COMMENT_DEFAULT_LINES - 1);
int end = itemContentView.getText().toString().lastIndexOf(' ', endOfLastLine - 2); int end = itemContentView.getText().toString().lastIndexOf(' ', endOfLastLine - 2);
if (end == -1) { if (end == -1) {
end = Math.max(endOfLastLine - 2, 0); end = Math.max(endOfLastLine - 2, 0);

View File

@ -14,8 +14,8 @@ import org.schabi.newpipe.extractor.stream.StreamInfoItem;
import org.schabi.newpipe.info_list.InfoItemBuilder; import org.schabi.newpipe.info_list.InfoItemBuilder;
import org.schabi.newpipe.ktx.ViewUtils; import org.schabi.newpipe.ktx.ViewUtils;
import org.schabi.newpipe.local.history.HistoryRecordManager; import org.schabi.newpipe.local.history.HistoryRecordManager;
import org.schabi.newpipe.util.PicassoHelper;
import org.schabi.newpipe.util.Localization; import org.schabi.newpipe.util.Localization;
import org.schabi.newpipe.util.PicassoHelper;
import org.schabi.newpipe.util.StreamTypeUtil; import org.schabi.newpipe.util.StreamTypeUtil;
import org.schabi.newpipe.views.AnimatedProgressBar; import org.schabi.newpipe.views.AnimatedProgressBar;
@ -111,8 +111,9 @@ public class StreamMiniInfoItemHolder extends InfoItemHolder {
final HistoryRecordManager historyRecordManager) { final HistoryRecordManager historyRecordManager) {
final StreamInfoItem item = (StreamInfoItem) infoItem; final StreamInfoItem item = (StreamInfoItem) infoItem;
final StreamStateEntity state final StreamStateEntity state = historyRecordManager
= historyRecordManager.loadStreamState(infoItem).blockingGet()[0]; .loadStreamState(infoItem)
.blockingGet()[0];
if (state != null && item.getDuration() > 0 if (state != null && item.getDuration() > 0
&& !StreamTypeUtil.isLiveStream(item.getStreamType())) { && !StreamTypeUtil.isLiveStream(item.getStreamType())) {
itemProgressView.setMax((int) item.getDuration()); itemProgressView.setMax((int) item.getDuration());

View File

@ -21,10 +21,6 @@ import org.schabi.newpipe.MainActivity
private const val TAG = "ViewUtils" private const val TAG = "ViewUtils"
inline var View.backgroundTintListCompat: ColorStateList?
get() = ViewCompat.getBackgroundTintList(this)
set(value) = ViewCompat.setBackgroundTintList(this, value)
/** /**
* Animate the view. * Animate the view.
* *
@ -96,62 +92,43 @@ fun View.animateBackgroundColor(duration: Long, @ColorInt colorStart: Int, @Colo
if (MainActivity.DEBUG) { if (MainActivity.DEBUG) {
Log.d( Log.d(
TAG, TAG,
"animateBackgroundColor() called with: " + "animateBackgroundColor() called with: view = [$this], duration = [$duration], " +
"view = [" + this + "], duration = [" + duration + "], " + "colorStart = [$colorStart], colorEnd = [$colorEnd]"
"colorStart = [" + colorStart + "], colorEnd = [" + colorEnd + "]"
) )
} }
val empty = arrayOf(IntArray(0))
val viewPropertyAnimator = ValueAnimator.ofObject(ArgbEvaluator(), colorStart, colorEnd) val viewPropertyAnimator = ValueAnimator.ofObject(ArgbEvaluator(), colorStart, colorEnd)
viewPropertyAnimator.interpolator = FastOutSlowInInterpolator() viewPropertyAnimator.interpolator = FastOutSlowInInterpolator()
viewPropertyAnimator.duration = duration viewPropertyAnimator.duration = duration
viewPropertyAnimator.addUpdateListener { animation: ValueAnimator ->
backgroundTintListCompat = ColorStateList(empty, intArrayOf(animation.animatedValue as Int)) fun listenerAction(color: Int) {
ViewCompat.setBackgroundTintList(this, ColorStateList.valueOf(color))
} }
viewPropertyAnimator.addListener( viewPropertyAnimator.addUpdateListener { listenerAction(it.animatedValue as Int) }
onCancel = { backgroundTintListCompat = ColorStateList(empty, intArrayOf(colorEnd)) }, viewPropertyAnimator.addListener(onCancel = { listenerAction(colorEnd) }, onEnd = { listenerAction(colorEnd) })
onEnd = { backgroundTintListCompat = ColorStateList(empty, intArrayOf(colorEnd)) }
)
viewPropertyAnimator.start() viewPropertyAnimator.start()
} }
fun View.animateHeight(duration: Long, targetHeight: Int): ValueAnimator { fun View.animateHeight(duration: Long, targetHeight: Int): ValueAnimator {
if (MainActivity.DEBUG) { if (MainActivity.DEBUG) {
Log.d( Log.d(TAG, "animateHeight: duration = [$duration], from $height to → $targetHeight in: $this")
TAG,
"animateHeight: duration = [" + duration + "], " +
"from " + height + " to → " + targetHeight + " in: " + this
)
} }
val animator = ValueAnimator.ofFloat(height.toFloat(), targetHeight.toFloat()) val animator = ValueAnimator.ofFloat(height.toFloat(), targetHeight.toFloat())
animator.interpolator = FastOutSlowInInterpolator() animator.interpolator = FastOutSlowInInterpolator()
animator.duration = duration animator.duration = duration
animator.addUpdateListener { animation: ValueAnimator ->
val value = animation.animatedValue as Float fun listenerAction(value: Int) {
layoutParams.height = value.toInt() layoutParams.height = value
requestLayout() requestLayout()
} }
animator.addListener( animator.addUpdateListener { listenerAction((it.animatedValue as Float).toInt()) }
onCancel = { animator.addListener(onCancel = { listenerAction(targetHeight) }, onEnd = { listenerAction(targetHeight) })
layoutParams.height = targetHeight
requestLayout()
},
onEnd = {
layoutParams.height = targetHeight
requestLayout()
}
)
animator.start() animator.start()
return animator return animator
} }
fun View.animateRotation(duration: Long, targetRotation: Int) { fun View.animateRotation(duration: Long, targetRotation: Int) {
if (MainActivity.DEBUG) { if (MainActivity.DEBUG) {
Log.d( Log.d(TAG, "animateRotation: duration = [$duration], from $rotation to → $targetRotation in: $this")
TAG,
"animateRotation: duration = [" + duration + "], " +
"from " + rotation + " to → " + targetRotation + " in: " + this
)
} }
animate().setListener(null).cancel() animate().setListener(null).cancel()
animate() animate()
@ -172,20 +149,13 @@ private fun View.animateAlpha(enterOrExit: Boolean, duration: Long, delay: Long,
if (enterOrExit) { if (enterOrExit) {
animate().setInterpolator(FastOutSlowInInterpolator()).alpha(1f) animate().setInterpolator(FastOutSlowInInterpolator()).alpha(1f)
.setDuration(duration).setStartDelay(delay) .setDuration(duration).setStartDelay(delay)
.setListener(object : AnimatorListenerAdapter() { .setListener(ExecOnEndListener(execOnEnd))
override fun onAnimationEnd(animation: Animator) { .start()
execOnEnd?.run()
}
}).start()
} else { } else {
animate().setInterpolator(FastOutSlowInInterpolator()).alpha(0f) animate().setInterpolator(FastOutSlowInInterpolator()).alpha(0f)
.setDuration(duration).setStartDelay(delay) .setDuration(duration).setStartDelay(delay)
.setListener(object : AnimatorListenerAdapter() { .setListener(HideAndExecOnEndListener(this, execOnEnd))
override fun onAnimationEnd(animation: Animator) { .start()
isGone = true
execOnEnd?.run()
}
}).start()
} }
} }
@ -197,11 +167,8 @@ private fun View.animateScaleAndAlpha(enterOrExit: Boolean, duration: Long, dela
.setInterpolator(FastOutSlowInInterpolator()) .setInterpolator(FastOutSlowInInterpolator())
.alpha(1f).scaleX(1f).scaleY(1f) .alpha(1f).scaleX(1f).scaleY(1f)
.setDuration(duration).setStartDelay(delay) .setDuration(duration).setStartDelay(delay)
.setListener(object : AnimatorListenerAdapter() { .setListener(ExecOnEndListener(execOnEnd))
override fun onAnimationEnd(animation: Animator) { .start()
execOnEnd?.run()
}
}).start()
} else { } else {
scaleX = 1f scaleX = 1f
scaleY = 1f scaleY = 1f
@ -209,12 +176,8 @@ private fun View.animateScaleAndAlpha(enterOrExit: Boolean, duration: Long, dela
.setInterpolator(FastOutSlowInInterpolator()) .setInterpolator(FastOutSlowInInterpolator())
.alpha(0f).scaleX(.8f).scaleY(.8f) .alpha(0f).scaleX(.8f).scaleY(.8f)
.setDuration(duration).setStartDelay(delay) .setDuration(duration).setStartDelay(delay)
.setListener(object : AnimatorListenerAdapter() { .setListener(HideAndExecOnEndListener(this, execOnEnd))
override fun onAnimationEnd(animation: Animator) { .start()
isGone = true
execOnEnd?.run()
}
}).start()
} }
} }
@ -227,11 +190,8 @@ private fun View.animateLightScaleAndAlpha(enterOrExit: Boolean, duration: Long,
.setInterpolator(FastOutSlowInInterpolator()) .setInterpolator(FastOutSlowInInterpolator())
.alpha(1f).scaleX(1f).scaleY(1f) .alpha(1f).scaleX(1f).scaleY(1f)
.setDuration(duration).setStartDelay(delay) .setDuration(duration).setStartDelay(delay)
.setListener(object : AnimatorListenerAdapter() { .setListener(ExecOnEndListener(execOnEnd))
override fun onAnimationEnd(animation: Animator) { .start()
execOnEnd?.run()
}
}).start()
} else { } else {
alpha = 1f alpha = 1f
scaleX = 1f scaleX = 1f
@ -240,12 +200,8 @@ private fun View.animateLightScaleAndAlpha(enterOrExit: Boolean, duration: Long,
.setInterpolator(FastOutSlowInInterpolator()) .setInterpolator(FastOutSlowInInterpolator())
.alpha(0f).scaleX(.95f).scaleY(.95f) .alpha(0f).scaleX(.95f).scaleY(.95f)
.setDuration(duration).setStartDelay(delay) .setDuration(duration).setStartDelay(delay)
.setListener(object : AnimatorListenerAdapter() { .setListener(HideAndExecOnEndListener(this, execOnEnd))
override fun onAnimationEnd(animation: Animator) { .start()
isGone = true
execOnEnd?.run()
}
}).start()
} }
} }
@ -256,22 +212,15 @@ private fun View.animateSlideAndAlpha(enterOrExit: Boolean, duration: Long, dela
animate() animate()
.setInterpolator(FastOutSlowInInterpolator()).alpha(1f).translationY(0f) .setInterpolator(FastOutSlowInInterpolator()).alpha(1f).translationY(0f)
.setDuration(duration).setStartDelay(delay) .setDuration(duration).setStartDelay(delay)
.setListener(object : AnimatorListenerAdapter() { .setListener(ExecOnEndListener(execOnEnd))
override fun onAnimationEnd(animation: Animator) { .start()
execOnEnd?.run()
}
}).start()
} else { } else {
animate() animate()
.setInterpolator(FastOutSlowInInterpolator()) .setInterpolator(FastOutSlowInInterpolator())
.alpha(0f).translationY(-height.toFloat()) .alpha(0f).translationY(-height.toFloat())
.setDuration(duration).setStartDelay(delay) .setDuration(duration).setStartDelay(delay)
.setListener(object : AnimatorListenerAdapter() { .setListener(HideAndExecOnEndListener(this, execOnEnd))
override fun onAnimationEnd(animation: Animator) { .start()
isGone = true
execOnEnd?.run()
}
}).start()
} }
} }
@ -282,21 +231,14 @@ private fun View.animateLightSlideAndAlpha(enterOrExit: Boolean, duration: Long,
animate() animate()
.setInterpolator(FastOutSlowInInterpolator()).alpha(1f).translationY(0f) .setInterpolator(FastOutSlowInInterpolator()).alpha(1f).translationY(0f)
.setDuration(duration).setStartDelay(delay) .setDuration(duration).setStartDelay(delay)
.setListener(object : AnimatorListenerAdapter() { .setListener(ExecOnEndListener(execOnEnd))
override fun onAnimationEnd(animation: Animator) { .start()
execOnEnd?.run()
}
}).start()
} else { } else {
animate().setInterpolator(FastOutSlowInInterpolator()) animate().setInterpolator(FastOutSlowInInterpolator())
.alpha(0f).translationY(-height / 2.0f) .alpha(0f).translationY(-height / 2.0f)
.setDuration(duration).setStartDelay(delay) .setDuration(duration).setStartDelay(delay)
.setListener(object : AnimatorListenerAdapter() { .setListener(HideAndExecOnEndListener(this, execOnEnd))
override fun onAnimationEnd(animation: Animator) { .start()
isGone = true
execOnEnd?.run()
}
}).start()
} }
} }
@ -318,11 +260,7 @@ fun View.slideUp(
.setStartDelay(delay) .setStartDelay(delay)
.setDuration(duration) .setDuration(duration)
.setInterpolator(FastOutSlowInInterpolator()) .setInterpolator(FastOutSlowInInterpolator())
.setListener(object : AnimatorListenerAdapter() { .setListener(ExecOnEndListener(execOnEnd))
override fun onAnimationEnd(animation: Animator) {
execOnEnd?.run()
}
})
.start() .start()
} }
@ -336,6 +274,20 @@ fun View.animateHideRecyclerViewAllowingScrolling() {
animate().alpha(0.0f).setDuration(200).start() animate().alpha(0.0f).setDuration(200).start()
} }
private open class ExecOnEndListener(private val execOnEnd: Runnable?) : AnimatorListenerAdapter() {
override fun onAnimationEnd(animation: Animator) {
execOnEnd?.run()
}
}
private class HideAndExecOnEndListener(private val view: View, execOnEnd: Runnable?) :
ExecOnEndListener(execOnEnd) {
override fun onAnimationEnd(animation: Animator) {
view.isGone = true
super.onAnimationEnd(animation)
}
}
enum class AnimationType { enum class AnimationType {
ALPHA, SCALE_AND_ALPHA, LIGHT_SCALE_AND_ALPHA, SLIDE_AND_ALPHA, LIGHT_SLIDE_AND_ALPHA ALPHA, SCALE_AND_ALPHA, LIGHT_SCALE_AND_ALPHA, SLIDE_AND_ALPHA, LIGHT_SLIDE_AND_ALPHA
} }

View File

@ -98,7 +98,7 @@ public final class BookmarkFragment extends BaseLocalListFragment<List<PlaylistL
protected void initListeners() { protected void initListeners() {
super.initListeners(); super.initListeners();
itemListAdapter.setSelectedListener(new OnClickGesture<LocalItem>() { itemListAdapter.setSelectedListener(new OnClickGesture<>() {
@Override @Override
public void selected(final LocalItem selectedItem) { public void selected(final LocalItem selectedItem) {
final FragmentManager fragmentManager = getFM(); final FragmentManager fragmentManager = getFM();
@ -256,8 +256,8 @@ public final class BookmarkFragment extends BaseLocalListFragment<List<PlaylistL
} }
private void showLocalDialog(final PlaylistMetadataEntry selectedItem) { private void showLocalDialog(final PlaylistMetadataEntry selectedItem) {
final DialogEditTextBinding dialogBinding final DialogEditTextBinding dialogBinding =
= DialogEditTextBinding.inflate(getLayoutInflater()); DialogEditTextBinding.inflate(getLayoutInflater());
dialogBinding.dialogEditText.setHint(R.string.name); dialogBinding.dialogEditText.setHint(R.string.name);
dialogBinding.dialogEditText.setInputType(InputType.TYPE_CLASS_TEXT); dialogBinding.dialogEditText.setInputType(InputType.TYPE_CLASS_TEXT);
dialogBinding.dialogEditText.setText(selectedItem.name); dialogBinding.dialogEditText.setText(selectedItem.name);

View File

@ -13,12 +13,10 @@ import androidx.recyclerview.widget.RecyclerView;
import org.schabi.newpipe.NewPipeDatabase; import org.schabi.newpipe.NewPipeDatabase;
import org.schabi.newpipe.R; import org.schabi.newpipe.R;
import org.schabi.newpipe.database.LocalItem;
import org.schabi.newpipe.database.playlist.PlaylistMetadataEntry; import org.schabi.newpipe.database.playlist.PlaylistMetadataEntry;
import org.schabi.newpipe.database.stream.model.StreamEntity; import org.schabi.newpipe.database.stream.model.StreamEntity;
import org.schabi.newpipe.local.LocalItemListAdapter; import org.schabi.newpipe.local.LocalItemListAdapter;
import org.schabi.newpipe.local.playlist.LocalPlaylistManager; import org.schabi.newpipe.local.playlist.LocalPlaylistManager;
import org.schabi.newpipe.util.OnClickGesture;
import java.util.List; import java.util.List;
@ -63,18 +61,10 @@ public final class PlaylistAppendDialog extends PlaylistDialog {
new LocalPlaylistManager(NewPipeDatabase.getInstance(requireContext())); new LocalPlaylistManager(NewPipeDatabase.getInstance(requireContext()));
playlistAdapter = new LocalItemListAdapter(getActivity()); playlistAdapter = new LocalItemListAdapter(getActivity());
playlistAdapter.setSelectedListener(new OnClickGesture<LocalItem>() { playlistAdapter.setSelectedListener(selectedItem -> {
@Override final List<StreamEntity> entities = getStreamEntities();
public void selected(final LocalItem selectedItem) { if (selectedItem instanceof PlaylistMetadataEntry && entities != null) {
if (!(selectedItem instanceof PlaylistMetadataEntry) onPlaylistSelected(playlistManager, (PlaylistMetadataEntry) selectedItem, entities);
|| getStreamEntities() == null) {
return;
}
onPlaylistSelected(
playlistManager,
(PlaylistMetadataEntry) selectedItem,
getStreamEntities()
);
} }
}); });
@ -138,14 +128,11 @@ public final class PlaylistAppendDialog extends PlaylistDialog {
private void onPlaylistSelected(@NonNull final LocalPlaylistManager manager, private void onPlaylistSelected(@NonNull final LocalPlaylistManager manager,
@NonNull final PlaylistMetadataEntry playlist, @NonNull final PlaylistMetadataEntry playlist,
@NonNull final List<StreamEntity> streams) { @NonNull final List<StreamEntity> streams) {
if (getStreamEntities() == null) {
return;
}
final Toast successToast = Toast.makeText(getContext(), final Toast successToast = Toast.makeText(getContext(),
R.string.playlist_add_stream_success, Toast.LENGTH_SHORT); R.string.playlist_add_stream_success, Toast.LENGTH_SHORT);
if (playlist.thumbnailUrl.equals("drawable://" + R.drawable.dummy_thumbnail_playlist)) { if (playlist.thumbnailUrl
.equals("drawable://" + R.drawable.placeholder_thumbnail_playlist)) {
playlistDisposables.add(manager playlistDisposables.add(manager
.changePlaylistThumbnail(playlist.uid, streams.get(0).getThumbnailUrl()) .changePlaylistThumbnail(playlist.uid, streams.get(0).getThumbnailUrl())
.observeOn(AndroidSchedulers.mainThread()) .observeOn(AndroidSchedulers.mainThread())

View File

@ -45,8 +45,8 @@ public final class PlaylistCreationDialog extends PlaylistDialog {
return super.onCreateDialog(savedInstanceState); return super.onCreateDialog(savedInstanceState);
} }
final DialogEditTextBinding dialogBinding final DialogEditTextBinding dialogBinding =
= DialogEditTextBinding.inflate(getLayoutInflater()); DialogEditTextBinding.inflate(getLayoutInflater());
dialogBinding.getRoot().getContext().setTheme(ThemeHelper.getDialogTheme(requireContext())); dialogBinding.getRoot().getContext().setTheme(ThemeHelper.getDialogTheme(requireContext()));
dialogBinding.dialogEditText.setHint(R.string.name); dialogBinding.dialogEditText.setHint(R.string.name);
dialogBinding.dialogEditText.setInputType(InputType.TYPE_CLASS_TEXT); dialogBinding.dialogEditText.setInputType(InputType.TYPE_CLASS_TEXT);

View File

@ -9,15 +9,20 @@ import android.view.Window;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.fragment.app.DialogFragment; import androidx.fragment.app.DialogFragment;
import androidx.fragment.app.FragmentManager;
import org.schabi.newpipe.NewPipeDatabase; import org.schabi.newpipe.NewPipeDatabase;
import org.schabi.newpipe.database.stream.model.StreamEntity; import org.schabi.newpipe.database.stream.model.StreamEntity;
import org.schabi.newpipe.local.playlist.LocalPlaylistManager; import org.schabi.newpipe.local.playlist.LocalPlaylistManager;
import org.schabi.newpipe.player.Player;
import org.schabi.newpipe.util.StateSaver; import org.schabi.newpipe.util.StateSaver;
import java.util.List; import java.util.List;
import java.util.Objects;
import java.util.Queue; import java.util.Queue;
import java.util.function.Consumer; import java.util.function.Consumer;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers; import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
import io.reactivex.rxjava3.disposables.Disposable; import io.reactivex.rxjava3.disposables.Disposable;
@ -131,13 +136,13 @@ public abstract class PlaylistDialog extends DialogFragment implements StateSave
* @param context context used for accessing the database * @param context context used for accessing the database
* @param streamEntities used for crating the dialog * @param streamEntities used for crating the dialog
* @param onExec execution that should occur after a dialog got created, e.g. showing it * @param onExec execution that should occur after a dialog got created, e.g. showing it
* @return Disposable * @return the disposable that was created
*/ */
public static Disposable createCorrespondingDialog( public static Disposable createCorrespondingDialog(
final Context context, final Context context,
final List<StreamEntity> streamEntities, final List<StreamEntity> streamEntities,
final Consumer<PlaylistDialog> onExec final Consumer<PlaylistDialog> onExec) {
) {
return new LocalPlaylistManager(NewPipeDatabase.getInstance(context)) return new LocalPlaylistManager(NewPipeDatabase.getInstance(context))
.hasPlaylists() .hasPlaylists()
.observeOn(AndroidSchedulers.mainThread()) .observeOn(AndroidSchedulers.mainThread())
@ -147,4 +152,30 @@ public abstract class PlaylistDialog extends DialogFragment implements StateSave
: PlaylistCreationDialog.newInstance(streamEntities)) : PlaylistCreationDialog.newInstance(streamEntities))
); );
} }
/**
* Creates a {@link PlaylistAppendDialog} when playlists exists,
* otherwise a {@link PlaylistCreationDialog}. If the player's play queue is null or empty, no
* dialog will be created.
*
* @param player the player from which to extract the context and the play queue
* @param fragmentManager the fragment manager to use to show the dialog
* @return the disposable that was created
*/
public static Disposable showForPlayQueue(
final Player player,
@NonNull final FragmentManager fragmentManager) {
final List<StreamEntity> streamEntities = Stream.of(player.getPlayQueue())
.filter(Objects::nonNull)
.flatMap(playQueue -> playQueue.getStreams().stream())
.map(StreamEntity::new)
.collect(Collectors.toList());
if (streamEntities.isEmpty()) {
return Disposable.empty();
}
return PlaylistDialog.createCorrespondingDialog(player.getContext(), streamEntities,
dialog -> dialog.show(fragmentManager, "PlaylistDialog"));
}
} }

View File

@ -41,19 +41,15 @@ class FeedDatabaseManager(context: Context) {
fun database() = database fun database() = database
fun getStreams( fun getStreams(
groupId: Long = FeedGroupEntity.GROUP_ALL_ID, groupId: Long,
getPlayedStreams: Boolean = true includePlayedStreams: Boolean,
includeFutureStreams: Boolean
): Maybe<List<StreamWithState>> { ): Maybe<List<StreamWithState>> {
return when (groupId) { return feedTable.getStreams(
FeedGroupEntity.GROUP_ALL_ID -> { groupId,
if (getPlayedStreams) feedTable.getAllStreams() includePlayedStreams,
else feedTable.getLiveOrNotPlayedStreams() if (includeFutureStreams) null else OffsetDateTime.now()
} )
else -> {
if (getPlayedStreams) feedTable.getAllStreamsForGroup(groupId)
else feedTable.getLiveOrNotPlayedStreamsForGroup(groupId)
}
}
} }
fun outdatedSubscriptions(outdatedThreshold: OffsetDateTime) = feedTable.getAllOutdated(outdatedThreshold) fun outdatedSubscriptions(outdatedThreshold: OffsetDateTime) = feedTable.getAllOutdated(outdatedThreshold)

View File

@ -41,6 +41,7 @@ import androidx.appcompat.app.AlertDialog
import androidx.appcompat.content.res.AppCompatResources import androidx.appcompat.content.res.AppCompatResources
import androidx.core.content.edit import androidx.core.content.edit
import androidx.core.os.bundleOf import androidx.core.os.bundleOf
import androidx.core.view.MenuItemCompat
import androidx.core.view.isVisible import androidx.core.view.isVisible
import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.ViewModelProvider
import androidx.preference.PreferenceManager import androidx.preference.PreferenceManager
@ -98,6 +99,7 @@ class FeedFragment : BaseStateFragment<FeedState>() {
private lateinit var groupAdapter: GroupieAdapter private lateinit var groupAdapter: GroupieAdapter
@State @JvmField var showPlayedItems: Boolean = true @State @JvmField var showPlayedItems: Boolean = true
@State @JvmField var showFutureItems: Boolean = true
private var onSettingsChangeListener: SharedPreferences.OnSharedPreferenceChangeListener? = null private var onSettingsChangeListener: SharedPreferences.OnSharedPreferenceChangeListener? = null
private var updateListViewModeOnResume = false private var updateListViewModeOnResume = false
@ -134,9 +136,10 @@ class FeedFragment : BaseStateFragment<FeedState>() {
_feedBinding = FragmentFeedBinding.bind(rootView) _feedBinding = FragmentFeedBinding.bind(rootView)
super.onViewCreated(rootView, savedInstanceState) super.onViewCreated(rootView, savedInstanceState)
val factory = FeedViewModel.Factory(requireContext(), groupId) val factory = FeedViewModel.getFactory(requireContext(), groupId)
viewModel = ViewModelProvider(this, factory).get(FeedViewModel::class.java) viewModel = ViewModelProvider(this, factory)[FeedViewModel::class.java]
showPlayedItems = viewModel.getShowPlayedItemsFromPreferences() showPlayedItems = viewModel.getShowPlayedItemsFromPreferences()
showFutureItems = viewModel.getShowFutureItemsFromPreferences()
viewModel.stateLiveData.observe(viewLifecycleOwner) { it?.let(::handleResult) } viewModel.stateLiveData.observe(viewLifecycleOwner) { it?.let(::handleResult) }
groupAdapter = GroupieAdapter().apply { groupAdapter = GroupieAdapter().apply {
@ -212,6 +215,7 @@ class FeedFragment : BaseStateFragment<FeedState>() {
inflater.inflate(R.menu.menu_feed_fragment, menu) inflater.inflate(R.menu.menu_feed_fragment, menu)
updateTogglePlayedItemsButton(menu.findItem(R.id.menu_item_feed_toggle_played_items)) updateTogglePlayedItemsButton(menu.findItem(R.id.menu_item_feed_toggle_played_items))
updateToggleFutureItemsButton(menu.findItem(R.id.menu_item_feed_toggle_future_items))
} }
override fun onOptionsItemSelected(item: MenuItem): Boolean { override fun onOptionsItemSelected(item: MenuItem): Boolean {
@ -241,6 +245,11 @@ class FeedFragment : BaseStateFragment<FeedState>() {
updateTogglePlayedItemsButton(item) updateTogglePlayedItemsButton(item)
viewModel.togglePlayedItems(showPlayedItems) viewModel.togglePlayedItems(showPlayedItems)
viewModel.saveShowPlayedItemsToPreferences(showPlayedItems) viewModel.saveShowPlayedItemsToPreferences(showPlayedItems)
} else if (item.itemId == R.id.menu_item_feed_toggle_future_items) {
showFutureItems = !item.isChecked
updateToggleFutureItemsButton(item)
viewModel.toggleFutureItems(showFutureItems)
viewModel.saveShowFutureItemsToPreferences(showFutureItems)
} }
return super.onOptionsItemSelected(item) return super.onOptionsItemSelected(item)
@ -278,6 +287,32 @@ class FeedFragment : BaseStateFragment<FeedState>() {
requireContext(), requireContext(),
if (showPlayedItems) R.drawable.ic_visibility_on else R.drawable.ic_visibility_off if (showPlayedItems) R.drawable.ic_visibility_on else R.drawable.ic_visibility_off
) )
MenuItemCompat.setTooltipText(
menuItem,
getString(
if (showPlayedItems)
R.string.feed_toggle_hide_played_items
else
R.string.feed_toggle_show_played_items
)
)
}
private fun updateToggleFutureItemsButton(menuItem: MenuItem) {
menuItem.isChecked = showFutureItems
menuItem.icon = AppCompatResources.getDrawable(
requireContext(),
if (showFutureItems) R.drawable.ic_history_future else R.drawable.ic_history
)
MenuItemCompat.setTooltipText(
menuItem,
getString(
if (showFutureItems)
R.string.feed_toggle_hide_future_items
else
R.string.feed_toggle_show_future_items
)
)
} }
// ////////////////////////////////////////////////////////////////////////// // //////////////////////////////////////////////////////////////////////////

View File

@ -1,17 +1,20 @@
package org.schabi.newpipe.local.feed package org.schabi.newpipe.local.feed
import android.app.Application
import android.content.Context import android.content.Context
import androidx.core.content.edit import androidx.core.content.edit
import androidx.lifecycle.LiveData import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.viewmodel.initializer
import androidx.lifecycle.viewmodel.viewModelFactory
import androidx.preference.PreferenceManager import androidx.preference.PreferenceManager
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
import io.reactivex.rxjava3.core.Flowable import io.reactivex.rxjava3.core.Flowable
import io.reactivex.rxjava3.functions.Function4 import io.reactivex.rxjava3.functions.Function5
import io.reactivex.rxjava3.processors.BehaviorProcessor import io.reactivex.rxjava3.processors.BehaviorProcessor
import io.reactivex.rxjava3.schedulers.Schedulers import io.reactivex.rxjava3.schedulers.Schedulers
import org.schabi.newpipe.App
import org.schabi.newpipe.R import org.schabi.newpipe.R
import org.schabi.newpipe.database.feed.model.FeedGroupEntity import org.schabi.newpipe.database.feed.model.FeedGroupEntity
import org.schabi.newpipe.database.stream.StreamWithState import org.schabi.newpipe.database.stream.StreamWithState
@ -26,17 +29,23 @@ import java.time.OffsetDateTime
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
class FeedViewModel( class FeedViewModel(
private val applicationContext: Context, private val application: Application,
groupId: Long = FeedGroupEntity.GROUP_ALL_ID, groupId: Long = FeedGroupEntity.GROUP_ALL_ID,
initialShowPlayedItems: Boolean = true initialShowPlayedItems: Boolean = true,
initialShowFutureItems: Boolean = true
) : ViewModel() { ) : ViewModel() {
private var feedDatabaseManager: FeedDatabaseManager = FeedDatabaseManager(applicationContext) private val feedDatabaseManager = FeedDatabaseManager(application)
private val toggleShowPlayedItems = BehaviorProcessor.create<Boolean>() private val toggleShowPlayedItems = BehaviorProcessor.create<Boolean>()
private val toggleShowPlayedItemsFlowable = toggleShowPlayedItems private val toggleShowPlayedItemsFlowable = toggleShowPlayedItems
.startWithItem(initialShowPlayedItems) .startWithItem(initialShowPlayedItems)
.distinctUntilChanged() .distinctUntilChanged()
private val toggleShowFutureItems = BehaviorProcessor.create<Boolean>()
private val toggleShowFutureItemsFlowable = toggleShowFutureItems
.startWithItem(initialShowFutureItems)
.distinctUntilChanged()
private val mutableStateLiveData = MutableLiveData<FeedState>() private val mutableStateLiveData = MutableLiveData<FeedState>()
val stateLiveData: LiveData<FeedState> = mutableStateLiveData val stateLiveData: LiveData<FeedState> = mutableStateLiveData
@ -44,21 +53,22 @@ class FeedViewModel(
.combineLatest( .combineLatest(
FeedEventManager.events(), FeedEventManager.events(),
toggleShowPlayedItemsFlowable, toggleShowPlayedItemsFlowable,
toggleShowFutureItemsFlowable,
feedDatabaseManager.notLoadedCount(groupId), feedDatabaseManager.notLoadedCount(groupId),
feedDatabaseManager.oldestSubscriptionUpdate(groupId), feedDatabaseManager.oldestSubscriptionUpdate(groupId),
Function4 { t1: FeedEventManager.Event, t2: Boolean, Function5 { t1: FeedEventManager.Event, t2: Boolean, t3: Boolean,
t3: Long, t4: List<OffsetDateTime> -> t4: Long, t5: List<OffsetDateTime> ->
return@Function4 CombineResultEventHolder(t1, t2, t3, t4.firstOrNull()) return@Function5 CombineResultEventHolder(t1, t2, t3, t4, t5.firstOrNull())
} }
) )
.throttleLatest(DEFAULT_THROTTLE_TIMEOUT, TimeUnit.MILLISECONDS) .throttleLatest(DEFAULT_THROTTLE_TIMEOUT, TimeUnit.MILLISECONDS)
.subscribeOn(Schedulers.io()) .subscribeOn(Schedulers.io())
.observeOn(Schedulers.io()) .observeOn(Schedulers.io())
.map { (event, showPlayedItems, notLoadedCount, oldestUpdate) -> .map { (event, showPlayedItems, showFutureItems, notLoadedCount, oldestUpdate) ->
val streamItems = if (event is SuccessResultEvent || event is IdleEvent) val streamItems = if (event is SuccessResultEvent || event is IdleEvent)
feedDatabaseManager feedDatabaseManager
.getStreams(groupId, showPlayedItems) .getStreams(groupId, showPlayedItems, showFutureItems)
.blockingGet(arrayListOf()) .blockingGet(arrayListOf())
else else
arrayListOf() arrayListOf()
@ -89,8 +99,9 @@ class FeedViewModel(
private data class CombineResultEventHolder( private data class CombineResultEventHolder(
val t1: FeedEventManager.Event, val t1: FeedEventManager.Event,
val t2: Boolean, val t2: Boolean,
val t3: Long, val t3: Boolean,
val t4: OffsetDateTime? val t4: Long,
val t5: OffsetDateTime?
) )
private data class CombineResultDataHolder( private data class CombineResultDataHolder(
@ -105,31 +116,42 @@ class FeedViewModel(
} }
fun saveShowPlayedItemsToPreferences(showPlayedItems: Boolean) = fun saveShowPlayedItemsToPreferences(showPlayedItems: Boolean) =
PreferenceManager.getDefaultSharedPreferences(applicationContext).edit { PreferenceManager.getDefaultSharedPreferences(application).edit {
this.putBoolean(applicationContext.getString(R.string.feed_show_played_items_key), showPlayedItems) this.putBoolean(application.getString(R.string.feed_show_played_items_key), showPlayedItems)
this.apply() this.apply()
} }
fun getShowPlayedItemsFromPreferences() = getShowPlayedItemsFromPreferences(applicationContext) fun getShowPlayedItemsFromPreferences() = getShowPlayedItemsFromPreferences(application)
fun toggleFutureItems(showFutureItems: Boolean) {
toggleShowFutureItems.onNext(showFutureItems)
}
fun saveShowFutureItemsToPreferences(showFutureItems: Boolean) =
PreferenceManager.getDefaultSharedPreferences(application).edit {
this.putBoolean(application.getString(R.string.feed_show_future_items_key), showFutureItems)
this.apply()
}
fun getShowFutureItemsFromPreferences() = getShowFutureItemsFromPreferences(application)
companion object { companion object {
private fun getShowPlayedItemsFromPreferences(context: Context) = private fun getShowPlayedItemsFromPreferences(context: Context) =
PreferenceManager.getDefaultSharedPreferences(context) PreferenceManager.getDefaultSharedPreferences(context)
.getBoolean(context.getString(R.string.feed_show_played_items_key), true) .getBoolean(context.getString(R.string.feed_show_played_items_key), true)
} private fun getShowFutureItemsFromPreferences(context: Context) =
PreferenceManager.getDefaultSharedPreferences(context)
class Factory( .getBoolean(context.getString(R.string.feed_show_future_items_key), true)
private val context: Context, fun getFactory(context: Context, groupId: Long) = viewModelFactory {
private val groupId: Long = FeedGroupEntity.GROUP_ALL_ID initializer {
) : ViewModelProvider.Factory { FeedViewModel(
@Suppress("UNCHECKED_CAST") App.getApp(),
override fun <T : ViewModel?> create(modelClass: Class<T>): T { groupId,
return FeedViewModel( // Read initial value from preferences
context.applicationContext, getShowPlayedItemsFromPreferences(context.applicationContext),
groupId, getShowFutureItemsFromPreferences(context.applicationContext)
// Read initial value from preferences )
getShowPlayedItemsFromPreferences(context.applicationContext) }
) as T
} }
} }
} }

View File

@ -4,6 +4,8 @@ import android.app.NotificationManager
import android.app.PendingIntent import android.app.PendingIntent
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.graphics.Bitmap
import android.graphics.drawable.Drawable
import android.net.Uri import android.net.Uri
import android.os.Build import android.os.Build
import android.provider.Settings import android.provider.Settings
@ -11,6 +13,8 @@ import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat import androidx.core.app.NotificationManagerCompat
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.preference.PreferenceManager import androidx.preference.PreferenceManager
import com.squareup.picasso.Picasso
import com.squareup.picasso.Target
import org.schabi.newpipe.R import org.schabi.newpipe.R
import org.schabi.newpipe.extractor.stream.StreamInfoItem import org.schabi.newpipe.extractor.stream.StreamInfoItem
import org.schabi.newpipe.local.feed.service.FeedUpdateInfo import org.schabi.newpipe.local.feed.service.FeedUpdateInfo
@ -27,6 +31,8 @@ class NotificationHelper(val context: Context) {
Context.NOTIFICATION_SERVICE Context.NOTIFICATION_SERVICE
) as NotificationManager ) as NotificationManager
private val iconLoadingTargets = ArrayList<Target>()
/** /**
* Show a notification about new streams from a single channel. * Show a notification about new streams from a single channel.
* Opening the notification will open the corresponding channel page. * Opening the notification will open the corresponding channel page.
@ -77,10 +83,29 @@ class NotificationHelper(val context: Context) {
) )
) )
PicassoHelper.loadNotificationIcon(data.avatarUrl) { bitmap -> // a Target is like a listener for image loading events
bitmap?.let { builder.setLargeIcon(it) } // set only if != null val target = object : Target {
manager.notify(data.pseudoId, builder.build()) override fun onBitmapLoaded(bitmap: Bitmap, from: Picasso.LoadedFrom) {
builder.setLargeIcon(bitmap) // set only if there is actually one
manager.notify(data.pseudoId, builder.build())
iconLoadingTargets.remove(this) // allow it to be garbage-collected
}
override fun onBitmapFailed(e: Exception, errorDrawable: Drawable) {
manager.notify(data.pseudoId, builder.build())
iconLoadingTargets.remove(this) // allow it to be garbage-collected
}
override fun onPrepareLoad(placeHolderDrawable: Drawable) {
// Nothing to do
}
} }
// add the target to the list to hold a strong reference and prevent it from being garbage
// collected, since Picasso only holds weak references to targets
iconLoadingTargets.add(target)
PicassoHelper.loadNotificationIcon(data.avatarUrl).into(target)
} }
companion object { companion object {

View File

@ -28,7 +28,6 @@ import org.schabi.newpipe.NewPipeDatabase;
import org.schabi.newpipe.R; import org.schabi.newpipe.R;
import org.schabi.newpipe.database.AppDatabase; import org.schabi.newpipe.database.AppDatabase;
import org.schabi.newpipe.database.LocalItem; import org.schabi.newpipe.database.LocalItem;
import org.schabi.newpipe.database.feed.dao.FeedDAO;
import org.schabi.newpipe.database.history.dao.SearchHistoryDAO; import org.schabi.newpipe.database.history.dao.SearchHistoryDAO;
import org.schabi.newpipe.database.history.dao.StreamHistoryDAO; import org.schabi.newpipe.database.history.dao.StreamHistoryDAO;
import org.schabi.newpipe.database.history.model.SearchHistoryEntry; import org.schabi.newpipe.database.history.model.SearchHistoryEntry;
@ -51,7 +50,6 @@ import org.schabi.newpipe.util.ExtractorHelper;
import java.time.OffsetDateTime; import java.time.OffsetDateTime;
import java.time.ZoneOffset; import java.time.ZoneOffset;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collection;
import java.util.List; import java.util.List;
import io.reactivex.rxjava3.core.Completable; import io.reactivex.rxjava3.core.Completable;
@ -89,7 +87,6 @@ public class HistoryRecordManager {
* Marks a stream item as watched such that it is hidden from the feed if watched videos are * Marks a stream item as watched such that it is hidden from the feed if watched videos are
* hidden. Adds a history entry and updates the stream progress to 100%. * hidden. Adds a history entry and updates the stream progress to 100%.
* *
* @see FeedDAO#getLiveOrNotPlayedStreams
* @see FeedViewModel#togglePlayedItems * @see FeedViewModel#togglePlayedItems
* @param info the item to mark as watched * @param info the item to mark as watched
* @return a Maybe containing the ID of the item if successful * @return a Maybe containing the ID of the item if successful
@ -176,10 +173,6 @@ public class HistoryRecordManager {
.subscribeOn(Schedulers.io()); .subscribeOn(Schedulers.io());
} }
public Flowable<List<StreamHistoryEntry>> getStreamHistory() {
return streamHistoryTable.getHistory().subscribeOn(Schedulers.io());
}
public Flowable<List<StreamHistoryEntry>> getStreamHistorySortedById() { public Flowable<List<StreamHistoryEntry>> getStreamHistorySortedById() {
return streamHistoryTable.getHistorySortedById().subscribeOn(Schedulers.io()); return streamHistoryTable.getHistorySortedById().subscribeOn(Schedulers.io());
} }
@ -188,24 +181,6 @@ public class HistoryRecordManager {
return streamHistoryTable.getStatistics().subscribeOn(Schedulers.io()); return streamHistoryTable.getStatistics().subscribeOn(Schedulers.io());
} }
public Single<List<Long>> insertStreamHistory(final Collection<StreamHistoryEntry> entries) {
final List<StreamHistoryEntity> entities = new ArrayList<>(entries.size());
for (final StreamHistoryEntry entry : entries) {
entities.add(entry.toStreamHistoryEntity());
}
return Single.fromCallable(() -> streamHistoryTable.insertAll(entities))
.subscribeOn(Schedulers.io());
}
public Single<Integer> deleteStreamHistory(final Collection<StreamHistoryEntry> entries) {
final List<StreamHistoryEntity> entities = new ArrayList<>(entries.size());
for (final StreamHistoryEntry entry : entries) {
entities.add(entry.toStreamHistoryEntity());
}
return Single.fromCallable(() -> streamHistoryTable.delete(entities))
.subscribeOn(Schedulers.io());
}
private boolean isStreamHistoryEnabled() { private boolean isStreamHistoryEnabled() {
return sharedPreferences.getBoolean(streamHistoryKey, false); return sharedPreferences.getBoolean(streamHistoryKey, false);
} }
@ -259,13 +234,6 @@ public class HistoryRecordManager {
// Stream State History // Stream State History
/////////////////////////////////////////////////////// ///////////////////////////////////////////////////////
public Maybe<StreamHistoryEntity> getStreamHistory(final StreamInfo info) {
return Maybe.fromCallable(() -> {
final long streamId = streamTable.upsert(new StreamEntity(info));
return streamHistoryTable.getLatestEntry(streamId);
}).subscribeOn(Schedulers.io());
}
public Maybe<StreamStateEntity> loadStreamState(final PlayQueueItem queueItem) { public Maybe<StreamStateEntity> loadStreamState(final PlayQueueItem queueItem) {
return queueItem.getStream() return queueItem.getStream()
.map(info -> streamTable.upsert(new StreamEntity(info))) .map(info -> streamTable.upsert(new StreamEntity(info)))
@ -311,28 +279,6 @@ public class HistoryRecordManager {
}).subscribeOn(Schedulers.io()); }).subscribeOn(Schedulers.io());
} }
public Single<List<StreamStateEntity>> loadStreamStateBatch(final List<InfoItem> infos) {
return Single.fromCallable(() -> {
final List<StreamStateEntity> result = new ArrayList<>(infos.size());
for (final InfoItem info : infos) {
final List<StreamEntity> entities = streamTable
.getStream(info.getServiceId(), info.getUrl()).blockingFirst();
if (entities.isEmpty()) {
result.add(null);
continue;
}
final List<StreamStateEntity> states = streamStateTable
.getState(entities.get(0).getUid()).blockingFirst();
if (states.isEmpty()) {
result.add(null);
} else {
result.add(states.get(0));
}
}
return result;
}).subscribeOn(Schedulers.io());
}
public Single<List<StreamStateEntity>> loadLocalStreamStateBatch( public Single<List<StreamStateEntity>> loadLocalStreamStateBatch(
final List<? extends LocalItem> items) { final List<? extends LocalItem> items) {
return Single.fromCallable(() -> { return Single.fromCallable(() -> {

View File

@ -135,7 +135,7 @@ public class StatisticsPlaylistFragment
protected void initListeners() { protected void initListeners() {
super.initListeners(); super.initListeners();
itemListAdapter.setSelectedListener(new OnClickGesture<LocalItem>() { itemListAdapter.setSelectedListener(new OnClickGesture<>() {
@Override @Override
public void selected(final LocalItem selectedItem) { public void selected(final LocalItem selectedItem) {
if (selectedItem instanceof StreamStatisticsEntry) { if (selectedItem instanceof StreamStatisticsEntry) {

View File

@ -1,5 +1,6 @@
package org.schabi.newpipe.local.playlist; package org.schabi.newpipe.local.playlist;
import static org.schabi.newpipe.error.ErrorUtil.showUiErrorSnackbar;
import static org.schabi.newpipe.ktx.ViewUtils.animate; import static org.schabi.newpipe.ktx.ViewUtils.animate;
import static org.schabi.newpipe.util.ThemeHelper.shouldUseGridLayout; import static org.schabi.newpipe.util.ThemeHelper.shouldUseGridLayout;
@ -41,15 +42,16 @@ import org.schabi.newpipe.error.ErrorInfo;
import org.schabi.newpipe.error.UserAction; import org.schabi.newpipe.error.UserAction;
import org.schabi.newpipe.extractor.stream.StreamInfoItem; import org.schabi.newpipe.extractor.stream.StreamInfoItem;
import org.schabi.newpipe.info_list.dialog.InfoItemDialog; import org.schabi.newpipe.info_list.dialog.InfoItemDialog;
import org.schabi.newpipe.info_list.dialog.StreamDialogDefaultEntry;
import org.schabi.newpipe.local.BaseLocalListFragment; import org.schabi.newpipe.local.BaseLocalListFragment;
import org.schabi.newpipe.local.history.HistoryRecordManager; import org.schabi.newpipe.local.history.HistoryRecordManager;
import org.schabi.newpipe.player.MainPlayer.PlayerType; import org.schabi.newpipe.player.PlayerType;
import org.schabi.newpipe.player.playqueue.PlayQueue; import org.schabi.newpipe.player.playqueue.PlayQueue;
import org.schabi.newpipe.player.playqueue.SinglePlayQueue; import org.schabi.newpipe.player.playqueue.SinglePlayQueue;
import org.schabi.newpipe.util.Localization; import org.schabi.newpipe.util.Localization;
import org.schabi.newpipe.util.NavigationHelper; import org.schabi.newpipe.util.NavigationHelper;
import org.schabi.newpipe.util.OnClickGesture; import org.schabi.newpipe.util.OnClickGesture;
import org.schabi.newpipe.info_list.dialog.StreamDialogDefaultEntry; import org.schabi.newpipe.util.external_communication.ShareUtils;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collections; import java.util.Collections;
@ -57,10 +59,12 @@ import java.util.Iterator;
import java.util.List; import java.util.List;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicBoolean;
import java.util.stream.Collectors;
import icepick.State; import icepick.State;
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers; import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
import io.reactivex.rxjava3.core.Flowable; import io.reactivex.rxjava3.core.Flowable;
import io.reactivex.rxjava3.core.Single;
import io.reactivex.rxjava3.disposables.CompositeDisposable; import io.reactivex.rxjava3.disposables.CompositeDisposable;
import io.reactivex.rxjava3.disposables.Disposable; import io.reactivex.rxjava3.disposables.Disposable;
import io.reactivex.rxjava3.schedulers.Schedulers; import io.reactivex.rxjava3.schedulers.Schedulers;
@ -163,7 +167,7 @@ public class LocalPlaylistFragment extends BaseLocalListFragment<List<PlaylistSt
itemTouchHelper = new ItemTouchHelper(getItemTouchCallback()); itemTouchHelper = new ItemTouchHelper(getItemTouchCallback());
itemTouchHelper.attachToRecyclerView(itemsList); itemTouchHelper.attachToRecyclerView(itemsList);
itemListAdapter.setSelectedListener(new OnClickGesture<LocalItem>() { itemListAdapter.setSelectedListener(new OnClickGesture<>() {
@Override @Override
public void selected(final LocalItem selectedItem) { public void selected(final LocalItem selectedItem) {
if (selectedItem instanceof PlaylistStreamEntry) { if (selectedItem instanceof PlaylistStreamEntry) {
@ -345,7 +349,11 @@ public class LocalPlaylistFragment extends BaseLocalListFragment<List<PlaylistSt
@Override @Override
public boolean onOptionsItemSelected(final MenuItem item) { public boolean onOptionsItemSelected(final MenuItem item) {
if (item.getItemId() == R.id.menu_item_remove_watched) { if (item.getItemId() == R.id.menu_item_share_playlist) {
sharePlaylist();
} else if (item.getItemId() == R.id.menu_item_rename_playlist) {
createRenameDialog();
} else if (item.getItemId() == R.id.menu_item_remove_watched) {
if (!isRemovingWatched) { if (!isRemovingWatched) {
new AlertDialog.Builder(requireContext()) new AlertDialog.Builder(requireContext())
.setMessage(R.string.remove_watched_popup_warning) .setMessage(R.string.remove_watched_popup_warning)
@ -360,14 +368,26 @@ public class LocalPlaylistFragment extends BaseLocalListFragment<List<PlaylistSt
.create() .create()
.show(); .show();
} }
} else if (item.getItemId() == R.id.menu_item_rename_playlist) {
createRenameDialog();
} else { } else {
return super.onOptionsItemSelected(item); return super.onOptionsItemSelected(item);
} }
return true; return true;
} }
/**
* Share the playlist as a newline-separated list of stream URLs.
*/
public void sharePlaylist() {
disposables.add(playlistManager.getPlaylistStreams(playlistId)
.flatMapSingle(playlist -> Single.just(playlist.stream()
.map(PlaylistStreamEntry::getStreamEntity)
.map(StreamEntity::getUrl)
.collect(Collectors.joining("\n"))))
.observeOn(AndroidSchedulers.mainThread())
.subscribe(urlsText -> ShareUtils.shareText(requireContext(), name, urlsText),
throwable -> showUiErrorSnackbar(this, "Sharing playlist", throwable)));
}
public void removeWatchedStreams(final boolean removePartiallyWatched) { public void removeWatchedStreams(final boolean removePartiallyWatched) {
if (isRemovingWatched) { if (isRemovingWatched) {
return; return;
@ -382,8 +402,8 @@ public class LocalPlaylistFragment extends BaseLocalListFragment<List<PlaylistSt
final Iterator<PlaylistStreamEntry> playlistIter = playlist.iterator(); final Iterator<PlaylistStreamEntry> playlistIter = playlist.iterator();
// History data // History data
final HistoryRecordManager recordManager final HistoryRecordManager recordManager =
= new HistoryRecordManager(getContext()); new HistoryRecordManager(getContext());
final Iterator<StreamHistoryEntry> historyIter = recordManager final Iterator<StreamHistoryEntry> historyIter = recordManager
.getStreamHistorySortedById().blockingFirst().iterator(); .getStreamHistorySortedById().blockingFirst().iterator();
@ -524,8 +544,8 @@ public class LocalPlaylistFragment extends BaseLocalListFragment<List<PlaylistSt
return; return;
} }
final DialogEditTextBinding dialogBinding final DialogEditTextBinding dialogBinding =
= DialogEditTextBinding.inflate(getLayoutInflater()); DialogEditTextBinding.inflate(getLayoutInflater());
dialogBinding.dialogEditText.setHint(R.string.name); dialogBinding.dialogEditText.setHint(R.string.name);
dialogBinding.dialogEditText.setInputType(InputType.TYPE_CLASS_TEXT); dialogBinding.dialogEditText.setInputType(InputType.TYPE_CLASS_TEXT);
dialogBinding.dialogEditText.setSelection(dialogBinding.dialogEditText.getText().length()); dialogBinding.dialogEditText.setSelection(dialogBinding.dialogEditText.getText().length());
@ -593,7 +613,7 @@ public class LocalPlaylistFragment extends BaseLocalListFragment<List<PlaylistSt
newThumbnailUrl = ((PlaylistStreamEntry) itemListAdapter.getItemsList().get(0)) newThumbnailUrl = ((PlaylistStreamEntry) itemListAdapter.getItemsList().get(0))
.getStreamEntity().getThumbnailUrl(); .getStreamEntity().getThumbnailUrl();
} else { } else {
newThumbnailUrl = "drawable://" + R.drawable.dummy_thumbnail_playlist; newThumbnailUrl = "drawable://" + R.drawable.placeholder_thumbnail_playlist;
} }
changeThumbnailUrl(newThumbnailUrl); changeThumbnailUrl(newThumbnailUrl);

View File

@ -346,7 +346,7 @@ class SubscriptionFragment : BaseStateFragment<SubscriptionState>() {
override fun doInitialLoadLogic() = Unit override fun doInitialLoadLogic() = Unit
override fun startLoading(forceLoad: Boolean) = Unit override fun startLoading(forceLoad: Boolean) = Unit
private val listenerFeedGroups = object : OnClickGesture<Item<*>>() { private val listenerFeedGroups = object : OnClickGesture<Item<*>> {
override fun selected(selectedItem: Item<*>?) { override fun selected(selectedItem: Item<*>?) {
when (selectedItem) { when (selectedItem) {
is FeedGroupCardItem -> NavigationHelper.openFeedFragment(fm, selectedItem.groupId, selectedItem.name) is FeedGroupCardItem -> NavigationHelper.openFeedFragment(fm, selectedItem.groupId, selectedItem.name)
@ -361,7 +361,7 @@ class SubscriptionFragment : BaseStateFragment<SubscriptionState>() {
} }
} }
private val listenerChannelItem = object : OnClickGesture<ChannelInfoItem>() { private val listenerChannelItem = object : OnClickGesture<ChannelInfoItem> {
override fun selected(selectedItem: ChannelInfoItem) = NavigationHelper.openChannelFragment( override fun selected(selectedItem: ChannelInfoItem) = NavigationHelper.openChannelFragment(
fm, fm,
selectedItem.serviceId, selectedItem.url, selectedItem.name selectedItem.serviceId, selectedItem.url, selectedItem.name

View File

@ -8,12 +8,10 @@ import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.view.inputmethod.InputMethodManager import android.view.inputmethod.InputMethodManager
import android.widget.Toast import android.widget.Toast
import androidx.appcompat.content.res.AppCompatResources
import androidx.core.content.getSystemService import androidx.core.content.getSystemService
import androidx.core.os.bundleOf import androidx.core.os.bundleOf
import androidx.core.view.isGone import androidx.core.view.isGone
import androidx.core.view.isVisible import androidx.core.view.isVisible
import androidx.core.widget.ImageViewCompat
import androidx.core.widget.doOnTextChanged import androidx.core.widget.doOnTextChanged
import androidx.fragment.app.DialogFragment import androidx.fragment.app.DialogFragment
import androidx.lifecycle.Observer import androidx.lifecycle.Observer
@ -124,14 +122,6 @@ class FeedGroupDialog : DialogFragment(), BackPressable {
_feedGroupCreateBinding = DialogFeedGroupCreateBinding.bind(view) _feedGroupCreateBinding = DialogFeedGroupCreateBinding.bind(view)
_searchLayoutBinding = feedGroupCreateBinding.subscriptionsHeaderSearchContainer _searchLayoutBinding = feedGroupCreateBinding.subscriptionsHeaderSearchContainer
if (android.os.Build.VERSION.SDK_INT < android.os.Build.VERSION_CODES.LOLLIPOP) {
// KitKat doesn't apply container's theme to <include> content
val contrastColor = AppCompatResources.getColorStateList(requireContext(), R.color.contrastColor)
searchLayoutBinding.toolbarSearchEditText.setTextColor(contrastColor)
searchLayoutBinding.toolbarSearchEditText.setHintTextColor(contrastColor.withAlpha(128))
ImageViewCompat.setImageTintList(searchLayoutBinding.toolbarSearchClearIcon, contrastColor)
}
viewModel = ViewModelProvider( viewModel = ViewModelProvider(
this, this,
FeedGroupDialogViewModel.Factory( FeedGroupDialogViewModel.Factory(

View File

@ -122,7 +122,7 @@ class FeedGroupDialogViewModel(
private val initialShowOnlyUngrouped: Boolean = false private val initialShowOnlyUngrouped: Boolean = false
) : ViewModelProvider.Factory { ) : ViewModelProvider.Factory {
@Suppress("UNCHECKED_CAST") @Suppress("UNCHECKED_CAST")
override fun <T : ViewModel?> create(modelClass: Class<T>): T { override fun <T : ViewModel> create(modelClass: Class<T>): T {
return FeedGroupDialogViewModel( return FeedGroupDialogViewModel(
context.applicationContext, context.applicationContext,
groupId, initialQuery, initialShowOnlyUngrouped groupId, initialQuery, initialShowOnlyUngrouped

View File

@ -39,7 +39,7 @@ class ChannelItem(
itemChannelDescriptionView.text = infoItem.description itemChannelDescriptionView.text = infoItem.description
} }
PicassoHelper.loadThumbnail(infoItem.thumbnailUrl).into(itemThumbnailView) PicassoHelper.loadAvatar(infoItem.thumbnailUrl).into(itemThumbnailView)
gesturesListener?.run { gesturesListener?.run {
viewHolder.root.setOnClickListener { selected(infoItem) } viewHolder.root.setOnClickListener { selected(infoItem) }

View File

@ -19,6 +19,8 @@
package org.schabi.newpipe.local.subscription.services; package org.schabi.newpipe.local.subscription.services;
import static org.schabi.newpipe.MainActivity.DEBUG;
import android.content.Intent; import android.content.Intent;
import android.net.Uri; import android.net.Uri;
import android.util.Log; import android.util.Log;
@ -43,8 +45,6 @@ import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
import io.reactivex.rxjava3.functions.Function; import io.reactivex.rxjava3.functions.Function;
import io.reactivex.rxjava3.schedulers.Schedulers; import io.reactivex.rxjava3.schedulers.Schedulers;
import static org.schabi.newpipe.MainActivity.DEBUG;
public class SubscriptionsExportService extends BaseImportExportService { public class SubscriptionsExportService extends BaseImportExportService {
public static final String KEY_FILE_PATH = "key_file_path"; public static final String KEY_FILE_PATH = "key_file_path";
@ -109,8 +109,8 @@ public class SubscriptionsExportService extends BaseImportExportService {
subscriptionManager.subscriptionTable().getAll().take(1) subscriptionManager.subscriptionTable().getAll().take(1)
.map(subscriptionEntities -> { .map(subscriptionEntities -> {
final List<SubscriptionItem> result final List<SubscriptionItem> result =
= new ArrayList<>(subscriptionEntities.size()); new ArrayList<>(subscriptionEntities.size());
for (final SubscriptionEntity entity : subscriptionEntities) { for (final SubscriptionEntity entity : subscriptionEntities) {
result.add(new SubscriptionItem(entity.getServiceId(), entity.getUrl(), result.add(new SubscriptionItem(entity.getServiceId(), entity.getUrl(),
entity.getName())); entity.getName()));

View File

@ -1,259 +0,0 @@
/*
* Copyright 2017 Mauricio Colli <mauriciocolli@outlook.com>
* Part of NewPipe
*
* License: GPL-3.0+
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* 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 <http://www.gnu.org/licenses/>.
*/
package org.schabi.newpipe.player;
import android.app.Service;
import android.content.Context;
import android.content.Intent;
import android.os.Binder;
import android.os.IBinder;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.view.WindowManager;
import androidx.annotation.Nullable;
import androidx.core.content.ContextCompat;
import org.schabi.newpipe.App;
import org.schabi.newpipe.databinding.PlayerBinding;
import org.schabi.newpipe.util.DeviceUtils;
import org.schabi.newpipe.util.ThemeHelper;
import static org.schabi.newpipe.util.Localization.assureCorrectAppLanguage;
/**
* One service for all players.
*
* @author mauriciocolli
*/
public final class MainPlayer extends Service {
private static final String TAG = "MainPlayer";
private static final boolean DEBUG = Player.DEBUG;
private Player player;
private WindowManager windowManager;
private final IBinder mBinder = new MainPlayer.LocalBinder();
public enum PlayerType {
VIDEO,
AUDIO,
POPUP
}
/*//////////////////////////////////////////////////////////////////////////
// Notification
//////////////////////////////////////////////////////////////////////////*/
static final String ACTION_CLOSE
= App.PACKAGE_NAME + ".player.MainPlayer.CLOSE";
static final String ACTION_PLAY_PAUSE
= App.PACKAGE_NAME + ".player.MainPlayer.PLAY_PAUSE";
static final String ACTION_REPEAT
= App.PACKAGE_NAME + ".player.MainPlayer.REPEAT";
static final String ACTION_PLAY_NEXT
= App.PACKAGE_NAME + ".player.MainPlayer.ACTION_PLAY_NEXT";
static final String ACTION_PLAY_PREVIOUS
= App.PACKAGE_NAME + ".player.MainPlayer.ACTION_PLAY_PREVIOUS";
static final String ACTION_FAST_REWIND
= App.PACKAGE_NAME + ".player.MainPlayer.ACTION_FAST_REWIND";
static final String ACTION_FAST_FORWARD
= App.PACKAGE_NAME + ".player.MainPlayer.ACTION_FAST_FORWARD";
static final String ACTION_SHUFFLE
= App.PACKAGE_NAME + ".player.MainPlayer.ACTION_SHUFFLE";
public static final String ACTION_RECREATE_NOTIFICATION
= App.PACKAGE_NAME + ".player.MainPlayer.ACTION_RECREATE_NOTIFICATION";
/*//////////////////////////////////////////////////////////////////////////
// Service's LifeCycle
//////////////////////////////////////////////////////////////////////////*/
@Override
public void onCreate() {
if (DEBUG) {
Log.d(TAG, "onCreate() called");
}
assureCorrectAppLanguage(this);
windowManager = ContextCompat.getSystemService(this, WindowManager.class);
ThemeHelper.setTheme(this);
createView();
}
private void createView() {
final PlayerBinding binding = PlayerBinding.inflate(LayoutInflater.from(this));
player = new Player(this);
player.setupFromView(binding);
NotificationUtil.getInstance().createNotificationAndStartForeground(player, this);
}
@Override
public int onStartCommand(final Intent intent, final int flags, final int startId) {
if (DEBUG) {
Log.d(TAG, "onStartCommand() called with: intent = [" + intent
+ "], flags = [" + flags + "], startId = [" + startId + "]");
}
if (Intent.ACTION_MEDIA_BUTTON.equals(intent.getAction())
&& player.getPlayQueue() == null) {
// Player is not working, no need to process media button's action
return START_NOT_STICKY;
}
if (Intent.ACTION_MEDIA_BUTTON.equals(intent.getAction())
|| intent.getStringExtra(Player.PLAY_QUEUE_KEY) != null) {
NotificationUtil.getInstance().createNotificationAndStartForeground(player, this);
}
player.handleIntent(intent);
if (player.getMediaSessionManager() != null) {
player.getMediaSessionManager().handleMediaButtonIntent(intent);
}
return START_NOT_STICKY;
}
public void stopForImmediateReusing() {
if (DEBUG) {
Log.d(TAG, "stopForImmediateReusing() called");
}
if (!player.exoPlayerIsNull()) {
player.saveWasPlaying();
// Releases wifi & cpu, disables keepScreenOn, etc.
// We can't just pause the player here because it will make transition
// from one stream to a new stream not smooth
player.smoothStopPlayer();
player.setRecovery();
// Android TV will handle back button in case controls will be visible
// (one more additional unneeded click while the player is hidden)
player.hideControls(0, 0);
player.closeItemsList();
// Notification shows information about old stream but if a user selects
// a stream from backStack it's not actual anymore
// So we should hide the notification at all.
// When autoplay enabled such notification flashing is annoying so skip this case
}
}
@Override
public void onTaskRemoved(final Intent rootIntent) {
super.onTaskRemoved(rootIntent);
if (!player.videoPlayerSelected()) {
return;
}
onDestroy();
// Unload from memory completely
Runtime.getRuntime().halt(0);
}
@Override
public void onDestroy() {
if (DEBUG) {
Log.d(TAG, "destroy() called");
}
cleanup();
}
private void cleanup() {
if (player != null) {
// Exit from fullscreen when user closes the player via notification
if (player.isFullscreen()) {
player.toggleFullscreen();
}
removeViewFromParent();
player.saveStreamProgressState();
player.setRecovery();
player.stopActivityBinding();
player.removePopupFromView();
player.destroy();
player = null;
}
}
public void stopService() {
NotificationUtil.getInstance().cancelNotificationAndStopForeground(this);
cleanup();
stopSelf();
}
@Override
protected void attachBaseContext(final Context base) {
super.attachBaseContext(AudioServiceLeakFix.preventLeakOf(base));
}
@Override
public IBinder onBind(final Intent intent) {
return mBinder;
}
/*//////////////////////////////////////////////////////////////////////////
// Utils
//////////////////////////////////////////////////////////////////////////*/
boolean isLandscape() {
// DisplayMetrics from activity context knows about MultiWindow feature
// while DisplayMetrics from app context doesn't
return DeviceUtils.isLandscape(player != null && player.getParentActivity() != null
? player.getParentActivity() : this);
}
@Nullable
public View getView() {
if (player == null) {
return null;
}
return player.getRootView();
}
public void removeViewFromParent() {
if (getView() != null && getView().getParent() != null) {
if (player.getParentActivity() != null) {
// This means view was added to fragment
final ViewGroup parent = (ViewGroup) getView().getParent();
parent.removeView(getView());
} else {
// This means view was added by windowManager for popup player
windowManager.removeViewImmediate(getView());
}
}
}
public class LocalBinder extends Binder {
public MainPlayer getService() {
return MainPlayer.this;
}
public Player getPlayer() {
return MainPlayer.this.player;
}
}
}

View File

@ -29,6 +29,7 @@ import org.schabi.newpipe.R;
import org.schabi.newpipe.databinding.ActivityPlayerQueueControlBinding; import org.schabi.newpipe.databinding.ActivityPlayerQueueControlBinding;
import org.schabi.newpipe.extractor.stream.StreamInfo; import org.schabi.newpipe.extractor.stream.StreamInfo;
import org.schabi.newpipe.fragments.OnScrollBelowItemsListener; import org.schabi.newpipe.fragments.OnScrollBelowItemsListener;
import org.schabi.newpipe.local.dialog.PlaylistDialog;
import org.schabi.newpipe.player.event.PlayerEventListener; import org.schabi.newpipe.player.event.PlayerEventListener;
import org.schabi.newpipe.player.helper.PlaybackParameterDialog; import org.schabi.newpipe.player.helper.PlaybackParameterDialog;
import org.schabi.newpipe.player.playqueue.PlayQueue; import org.schabi.newpipe.player.playqueue.PlayQueue;
@ -51,7 +52,7 @@ public final class PlayQueueActivity extends AppCompatActivity
private static final int SMOOTH_SCROLL_MAXIMUM_DISTANCE = 80; private static final int SMOOTH_SCROLL_MAXIMUM_DISTANCE = 80;
protected Player player; private Player player;
private boolean serviceBound; private boolean serviceBound;
private ServiceConnection serviceConnection; private ServiceConnection serviceConnection;
@ -126,13 +127,13 @@ public final class PlayQueueActivity extends AppCompatActivity
NavigationHelper.openSettings(this); NavigationHelper.openSettings(this);
return true; return true;
case R.id.action_append_playlist: case R.id.action_append_playlist:
player.onAddToPlaylistClicked(getSupportFragmentManager()); PlaylistDialog.showForPlayQueue(player, getSupportFragmentManager());
return true; return true;
case R.id.action_playback_speed: case R.id.action_playback_speed:
openPlaybackParameterDialog(); openPlaybackParameterDialog();
return true; return true;
case R.id.action_mute: case R.id.action_mute:
player.onMuteUnmuteButtonClicked(); player.toggleMute();
return true; return true;
case R.id.action_system_audio: case R.id.action_system_audio:
startActivity(new Intent(Settings.ACTION_SOUND_SETTINGS)); startActivity(new Intent(Settings.ACTION_SOUND_SETTINGS));
@ -168,7 +169,7 @@ public final class PlayQueueActivity extends AppCompatActivity
//////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////
private void bind() { private void bind() {
final Intent bindIntent = new Intent(this, MainPlayer.class); final Intent bindIntent = new Intent(this, PlayerService.class);
final boolean success = bindService(bindIntent, serviceConnection, BIND_AUTO_CREATE); final boolean success = bindService(bindIntent, serviceConnection, BIND_AUTO_CREATE);
if (!success) { if (!success) {
unbindService(serviceConnection); unbindService(serviceConnection);
@ -184,10 +185,7 @@ public final class PlayQueueActivity extends AppCompatActivity
player.removeActivityListener(this); player.removeActivityListener(this);
} }
if (player != null && player.getPlayQueueAdapter() != null) { onQueueUpdate(null);
player.getPlayQueueAdapter().unsetSelectedListener();
}
queueControlBinding.playQueue.setAdapter(null);
if (itemTouchHelper != null) { if (itemTouchHelper != null) {
itemTouchHelper.attachToRecyclerView(null); itemTouchHelper.attachToRecyclerView(null);
} }
@ -208,17 +206,15 @@ public final class PlayQueueActivity extends AppCompatActivity
public void onServiceConnected(final ComponentName name, final IBinder service) { public void onServiceConnected(final ComponentName name, final IBinder service) {
Log.d(TAG, "Player service is connected"); Log.d(TAG, "Player service is connected");
if (service instanceof PlayerServiceBinder) { if (service instanceof PlayerService.LocalBinder) {
player = ((PlayerServiceBinder) service).getPlayerInstance(); player = ((PlayerService.LocalBinder) service).getPlayer();
} else if (service instanceof MainPlayer.LocalBinder) {
player = ((MainPlayer.LocalBinder) service).getPlayer();
} }
if (player == null || player.getPlayQueue() == null if (player == null || player.getPlayQueue() == null || player.exoPlayerIsNull()) {
|| player.getPlayQueueAdapter() == null || player.exoPlayerIsNull()) {
unbind(); unbind();
finish(); finish();
} else { } else {
onQueueUpdate(player.getPlayQueue());
buildComponents(); buildComponents();
if (player != null) { if (player != null) {
player.setActivityListener(PlayQueueActivity.this); player.setActivityListener(PlayQueueActivity.this);
@ -241,7 +237,6 @@ public final class PlayQueueActivity extends AppCompatActivity
private void buildQueue() { private void buildQueue() {
queueControlBinding.playQueue.setLayoutManager(new LinearLayoutManager(this)); queueControlBinding.playQueue.setLayoutManager(new LinearLayoutManager(this));
queueControlBinding.playQueue.setAdapter(player.getPlayQueueAdapter());
queueControlBinding.playQueue.setClickable(true); queueControlBinding.playQueue.setClickable(true);
queueControlBinding.playQueue.setLongClickable(true); queueControlBinding.playQueue.setLongClickable(true);
queueControlBinding.playQueue.clearOnScrollListeners(); queueControlBinding.playQueue.clearOnScrollListeners();
@ -249,8 +244,6 @@ public final class PlayQueueActivity extends AppCompatActivity
itemTouchHelper = new ItemTouchHelper(getItemTouchCallback()); itemTouchHelper = new ItemTouchHelper(getItemTouchCallback());
itemTouchHelper.attachToRecyclerView(queueControlBinding.playQueue); itemTouchHelper.attachToRecyclerView(queueControlBinding.playQueue);
player.getPlayQueueAdapter().setSelectedListener(getOnSelectedListener());
} }
private void buildMetadata() { private void buildMetadata() {
@ -370,7 +363,7 @@ public final class PlayQueueActivity extends AppCompatActivity
} }
if (view.getId() == queueControlBinding.controlRepeat.getId()) { if (view.getId() == queueControlBinding.controlRepeat.getId()) {
player.onRepeatClicked(); player.cycleNextRepeatMode();
} else if (view.getId() == queueControlBinding.controlBackward.getId()) { } else if (view.getId() == queueControlBinding.controlBackward.getId()) {
player.playPrevious(); player.playPrevious();
} else if (view.getId() == queueControlBinding.controlFastRewind.getId()) { } else if (view.getId() == queueControlBinding.controlFastRewind.getId()) {
@ -382,7 +375,7 @@ public final class PlayQueueActivity extends AppCompatActivity
} else if (view.getId() == queueControlBinding.controlForward.getId()) { } else if (view.getId() == queueControlBinding.controlForward.getId()) {
player.playNext(); player.playNext();
} else if (view.getId() == queueControlBinding.controlShuffle.getId()) { } else if (view.getId() == queueControlBinding.controlShuffle.getId()) {
player.onShuffleClicked(); player.toggleShuffleModeEnabled();
} else if (view.getId() == queueControlBinding.metadata.getId()) { } else if (view.getId() == queueControlBinding.metadata.getId()) {
scrollToSelected(); scrollToSelected();
} else if (view.getId() == queueControlBinding.liveSync.getId()) { } else if (view.getId() == queueControlBinding.liveSync.getId()) {
@ -445,7 +438,14 @@ public final class PlayQueueActivity extends AppCompatActivity
//////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////
@Override @Override
public void onQueueUpdate(final PlayQueue queue) { public void onQueueUpdate(@Nullable final PlayQueue queue) {
if (queue == null) {
queueControlBinding.playQueue.setAdapter(null);
} else {
final PlayQueueAdapter adapter = new PlayQueueAdapter(this, queue);
adapter.setSelectedListener(getOnSelectedListener());
queueControlBinding.playQueue.setAdapter(adapter);
}
} }
@Override @Override
@ -454,7 +454,6 @@ public final class PlayQueueActivity extends AppCompatActivity
onStateChanged(state); onStateChanged(state);
onPlayModeChanged(repeatMode, shuffled); onPlayModeChanged(repeatMode, shuffled);
onPlaybackParameterChanged(parameters); onPlaybackParameterChanged(parameters);
onMaybePlaybackAdapterChanged();
onMaybeMuteChanged(); onMaybeMuteChanged();
} }
@ -582,17 +581,6 @@ public final class PlayQueueActivity extends AppCompatActivity
} }
} }
private void onMaybePlaybackAdapterChanged() {
if (player == null) {
return;
}
final PlayQueueAdapter maybeNewAdapter = player.getPlayQueueAdapter();
if (maybeNewAdapter != null
&& queueControlBinding.playQueue.getAdapter() != maybeNewAdapter) {
queueControlBinding.playQueue.setAdapter(maybeNewAdapter);
}
}
private void onMaybeMuteChanged() { private void onMaybeMuteChanged() {
if (menu != null && player != null) { if (menu != null && player != null) {
final MenuItem item = menu.findItem(R.id.action_mute); final MenuItem item = menu.findItem(R.id.action_mute);

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,149 @@
/*
* Copyright 2017 Mauricio Colli <mauriciocolli@outlook.com>
* Part of NewPipe
*
* License: GPL-3.0+
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* 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 <http://www.gnu.org/licenses/>.
*/
package org.schabi.newpipe.player;
import static org.schabi.newpipe.util.Localization.assureCorrectAppLanguage;
import android.app.Service;
import android.content.Context;
import android.content.Intent;
import android.os.Binder;
import android.os.IBinder;
import android.util.Log;
import org.schabi.newpipe.player.mediasession.MediaSessionPlayerUi;
import org.schabi.newpipe.util.ThemeHelper;
/**
* One service for all players.
*/
public final class PlayerService extends Service {
private static final String TAG = PlayerService.class.getSimpleName();
private static final boolean DEBUG = Player.DEBUG;
private Player player;
private final IBinder mBinder = new PlayerService.LocalBinder();
/*//////////////////////////////////////////////////////////////////////////
// Service's LifeCycle
//////////////////////////////////////////////////////////////////////////*/
@Override
public void onCreate() {
if (DEBUG) {
Log.d(TAG, "onCreate() called");
}
assureCorrectAppLanguage(this);
ThemeHelper.setTheme(this);
player = new Player(this);
}
@Override
public int onStartCommand(final Intent intent, final int flags, final int startId) {
if (DEBUG) {
Log.d(TAG, "onStartCommand() called with: intent = [" + intent
+ "], flags = [" + flags + "], startId = [" + startId + "]");
}
if (Intent.ACTION_MEDIA_BUTTON.equals(intent.getAction())
&& player.getPlayQueue() == null) {
// No need to process media button's actions if the player is not working, otherwise the
// player service would strangely start with nothing to play
return START_NOT_STICKY;
}
player.handleIntent(intent);
player.UIs().get(MediaSessionPlayerUi.class)
.ifPresent(ui -> ui.handleMediaButtonIntent(intent));
return START_NOT_STICKY;
}
public void stopForImmediateReusing() {
if (DEBUG) {
Log.d(TAG, "stopForImmediateReusing() called");
}
if (!player.exoPlayerIsNull()) {
player.saveWasPlaying();
// Releases wifi & cpu, disables keepScreenOn, etc.
// We can't just pause the player here because it will make transition
// from one stream to a new stream not smooth
player.smoothStopForImmediateReusing();
}
}
@Override
public void onTaskRemoved(final Intent rootIntent) {
super.onTaskRemoved(rootIntent);
if (!player.videoPlayerSelected()) {
return;
}
onDestroy();
// Unload from memory completely
Runtime.getRuntime().halt(0);
}
@Override
public void onDestroy() {
if (DEBUG) {
Log.d(TAG, "destroy() called");
}
cleanup();
}
private void cleanup() {
if (player != null) {
player.destroy();
player = null;
}
}
public void stopService() {
cleanup();
stopSelf();
}
@Override
protected void attachBaseContext(final Context base) {
super.attachBaseContext(AudioServiceLeakFix.preventLeakOf(base));
}
@Override
public IBinder onBind(final Intent intent) {
return mBinder;
}
public class LocalBinder extends Binder {
public PlayerService getService() {
return PlayerService.this;
}
public Player getPlayer() {
return PlayerService.this.player;
}
}
}

View File

@ -1,17 +0,0 @@
package org.schabi.newpipe.player;
import android.os.Binder;
import androidx.annotation.NonNull;
class PlayerServiceBinder extends Binder {
private final Player player;
PlayerServiceBinder(@NonNull final Player player) {
this.player = player;
}
Player getPlayerInstance() {
return player;
}
}

View File

@ -1,79 +0,0 @@
package org.schabi.newpipe.player;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import org.schabi.newpipe.player.playqueue.PlayQueue;
import java.io.Serializable;
public class PlayerState implements Serializable {
@NonNull
private final PlayQueue playQueue;
private final int repeatMode;
private final float playbackSpeed;
private final float playbackPitch;
@Nullable
private final String playbackQuality;
private final boolean playbackSkipSilence;
private final boolean wasPlaying;
PlayerState(@NonNull final PlayQueue playQueue, final int repeatMode,
final float playbackSpeed, final float playbackPitch,
final boolean playbackSkipSilence, final boolean wasPlaying) {
this(playQueue, repeatMode, playbackSpeed, playbackPitch, null,
playbackSkipSilence, wasPlaying);
}
PlayerState(@NonNull final PlayQueue playQueue, final int repeatMode,
final float playbackSpeed, final float playbackPitch,
@Nullable final String playbackQuality, final boolean playbackSkipSilence,
final boolean wasPlaying) {
this.playQueue = playQueue;
this.repeatMode = repeatMode;
this.playbackSpeed = playbackSpeed;
this.playbackPitch = playbackPitch;
this.playbackQuality = playbackQuality;
this.playbackSkipSilence = playbackSkipSilence;
this.wasPlaying = wasPlaying;
}
/*//////////////////////////////////////////////////////////////////////////
// Serdes
//////////////////////////////////////////////////////////////////////////*/
/*//////////////////////////////////////////////////////////////////////////
// Getters
//////////////////////////////////////////////////////////////////////////*/
@NonNull
public PlayQueue getPlayQueue() {
return playQueue;
}
public int getRepeatMode() {
return repeatMode;
}
public float getPlaybackSpeed() {
return playbackSpeed;
}
public float getPlaybackPitch() {
return playbackPitch;
}
@Nullable
public String getPlaybackQuality() {
return playbackQuality;
}
public boolean isPlaybackSkipSilence() {
return playbackSkipSilence;
}
public boolean wasPlaying() {
return wasPlaying;
}
}

View File

@ -0,0 +1,32 @@
package org.schabi.newpipe.player;
import static org.schabi.newpipe.player.Player.PLAYER_TYPE;
import android.content.Intent;
public enum PlayerType {
MAIN,
AUDIO,
POPUP;
/**
* @return an integer representing this {@link PlayerType}, to be used to save it in intents
* @see #retrieveFromIntent(Intent) Use retrieveFromIntent() to retrieve and convert player type
* integers from an intent
*/
public int valueForIntent() {
return ordinal();
}
/**
* @param intent the intent to retrieve a player type from
* @return the player type integer retrieved from the intent, converted back into a {@link
* PlayerType}, or {@link PlayerType#MAIN} if there is no player type extra in the
* intent
* @throws ArrayIndexOutOfBoundsException if the intent contains an invalid player type integer
* @see #valueForIntent() Use valueForIntent() to obtain valid player type integers
*/
public static PlayerType retrieveFromIntent(final Intent intent) {
return values()[intent.getIntExtra(PLAYER_TYPE, MAIN.valueForIntent())];
}
}

View File

@ -1,5 +1,5 @@
/* /*
* Based on ExoPlayer's DefaultHttpDataSource, version 2.17.1. * Based on ExoPlayer's DefaultHttpDataSource, version 2.18.1.
* *
* Original source code copyright (C) 2016 The Android Open Source Project, licensed under the * Original source code copyright (C) 2016 The Android Open Source Project, licensed under the
* Apache License, Version 2.0. * Apache License, Version 2.0.

View File

@ -1,520 +0,0 @@
package org.schabi.newpipe.player.event
import android.content.Context
import android.os.Handler
import android.util.Log
import android.view.GestureDetector
import android.view.MotionEvent
import android.view.View
import android.view.ViewConfiguration
import org.schabi.newpipe.ktx.animate
import org.schabi.newpipe.player.MainPlayer
import org.schabi.newpipe.player.Player
import org.schabi.newpipe.player.helper.PlayerHelper
import org.schabi.newpipe.player.helper.PlayerHelper.savePopupPositionAndSizeToPrefs
import kotlin.math.abs
import kotlin.math.hypot
import kotlin.math.max
import kotlin.math.min
/**
* Base gesture handling for [Player]
*
* This class contains the logic for the player gestures like View preparations
* and provides some abstract methods to make it easier separating the logic from the UI.
*/
abstract class BasePlayerGestureListener(
@JvmField
protected val player: Player,
@JvmField
protected val service: MainPlayer
) : GestureDetector.SimpleOnGestureListener(), View.OnTouchListener {
// ///////////////////////////////////////////////////////////////////
// Abstract methods for VIDEO and POPUP
// ///////////////////////////////////////////////////////////////////
abstract fun onDoubleTap(event: MotionEvent, portion: DisplayPortion)
abstract fun onSingleTap(playerType: MainPlayer.PlayerType)
abstract fun onScroll(
playerType: MainPlayer.PlayerType,
portion: DisplayPortion,
initialEvent: MotionEvent,
movingEvent: MotionEvent,
distanceX: Float,
distanceY: Float
)
abstract fun onScrollEnd(playerType: MainPlayer.PlayerType, event: MotionEvent)
// ///////////////////////////////////////////////////////////////////
// Abstract methods for POPUP (exclusive)
// ///////////////////////////////////////////////////////////////////
abstract fun onPopupResizingStart()
abstract fun onPopupResizingEnd()
private var initialPopupX: Int = -1
private var initialPopupY: Int = -1
private var isMovingInMain = false
private var isMovingInPopup = false
private var isResizing = false
private val tossFlingVelocity = PlayerHelper.getTossFlingVelocity()
// [popup] initial coordinates and distance between fingers
private var initPointerDistance = -1.0
private var initFirstPointerX = -1f
private var initFirstPointerY = -1f
private var initSecPointerX = -1f
private var initSecPointerY = -1f
// ///////////////////////////////////////////////////////////////////
// onTouch implementation
// ///////////////////////////////////////////////////////////////////
override fun onTouch(v: View, event: MotionEvent): Boolean {
return if (player.popupPlayerSelected()) {
onTouchInPopup(v, event)
} else {
onTouchInMain(v, event)
}
}
private fun onTouchInMain(v: View, event: MotionEvent): Boolean {
player.gestureDetector.onTouchEvent(event)
if (event.action == MotionEvent.ACTION_UP && isMovingInMain) {
isMovingInMain = false
onScrollEnd(MainPlayer.PlayerType.VIDEO, event)
}
return when (event.action) {
MotionEvent.ACTION_DOWN, MotionEvent.ACTION_MOVE -> {
v.parent.requestDisallowInterceptTouchEvent(player.isFullscreen)
true
}
MotionEvent.ACTION_UP -> {
v.parent.requestDisallowInterceptTouchEvent(false)
false
}
else -> true
}
}
private fun onTouchInPopup(v: View, event: MotionEvent): Boolean {
player.gestureDetector.onTouchEvent(event)
if (event.pointerCount == 2 && !isMovingInPopup && !isResizing) {
if (DEBUG) {
Log.d(TAG, "onTouch() 2 finger pointer detected, enabling resizing.")
}
onPopupResizingStart()
// record coordinates of fingers
initFirstPointerX = event.getX(0)
initFirstPointerY = event.getY(0)
initSecPointerX = event.getX(1)
initSecPointerY = event.getY(1)
// record distance between fingers
initPointerDistance = hypot(
initFirstPointerX - initSecPointerX.toDouble(),
initFirstPointerY - initSecPointerY.toDouble()
)
isResizing = true
}
if (event.action == MotionEvent.ACTION_MOVE && !isMovingInPopup && isResizing) {
if (DEBUG) {
Log.d(
TAG,
"onTouch() ACTION_MOVE > v = [$v], e1.getRaw =" +
"[${event.rawX}, ${event.rawY}]"
)
}
return handleMultiDrag(event)
}
if (event.action == MotionEvent.ACTION_UP) {
if (DEBUG) {
Log.d(
TAG,
"onTouch() ACTION_UP > v = [$v], e1.getRaw =" +
" [${event.rawX}, ${event.rawY}]"
)
}
if (isMovingInPopup) {
isMovingInPopup = false
onScrollEnd(MainPlayer.PlayerType.POPUP, event)
}
if (isResizing) {
isResizing = false
initPointerDistance = (-1).toDouble()
initFirstPointerX = (-1).toFloat()
initFirstPointerY = (-1).toFloat()
initSecPointerX = (-1).toFloat()
initSecPointerY = (-1).toFloat()
onPopupResizingEnd()
player.changeState(player.currentState)
}
if (!player.isPopupClosing) {
savePopupPositionAndSizeToPrefs(player)
}
}
v.performClick()
return true
}
private fun handleMultiDrag(event: MotionEvent): Boolean {
if (initPointerDistance != -1.0 && event.pointerCount == 2) {
// get the movements of the fingers
val firstPointerMove = hypot(
event.getX(0) - initFirstPointerX.toDouble(),
event.getY(0) - initFirstPointerY.toDouble()
)
val secPointerMove = hypot(
event.getX(1) - initSecPointerX.toDouble(),
event.getY(1) - initSecPointerY.toDouble()
)
// minimum threshold beyond which pinch gesture will work
val minimumMove = ViewConfiguration.get(service).scaledTouchSlop
if (max(firstPointerMove, secPointerMove) > minimumMove) {
// calculate current distance between the pointers
val currentPointerDistance = hypot(
event.getX(0) - event.getX(1).toDouble(),
event.getY(0) - event.getY(1).toDouble()
)
val popupWidth = player.popupLayoutParams!!.width.toDouble()
// change co-ordinates of popup so the center stays at the same position
val newWidth = popupWidth * currentPointerDistance / initPointerDistance
initPointerDistance = currentPointerDistance
player.popupLayoutParams!!.x += ((popupWidth - newWidth) / 2.0).toInt()
player.checkPopupPositionBounds()
player.updateScreenSize()
player.changePopupSize(min(player.screenWidth.toDouble(), newWidth).toInt())
return true
}
}
return false
}
// ///////////////////////////////////////////////////////////////////
// Simple gestures
// ///////////////////////////////////////////////////////////////////
override fun onDown(e: MotionEvent): Boolean {
if (DEBUG)
Log.d(TAG, "onDown called with e = [$e]")
if (isDoubleTapping && isDoubleTapEnabled) {
doubleTapControls?.onDoubleTapProgressDown(getDisplayPortion(e))
return true
}
return if (player.popupPlayerSelected())
onDownInPopup(e)
else
true
}
private fun onDownInPopup(e: MotionEvent): Boolean {
// Fix popup position when the user touch it, it may have the wrong one
// because the soft input is visible (the draggable area is currently resized).
player.updateScreenSize()
player.checkPopupPositionBounds()
player.popupLayoutParams?.let {
initialPopupX = it.x
initialPopupY = it.y
}
return super.onDown(e)
}
override fun onDoubleTap(e: MotionEvent): Boolean {
if (DEBUG)
Log.d(TAG, "onDoubleTap called with e = [$e]")
onDoubleTap(e, getDisplayPortion(e))
return true
}
override fun onSingleTapConfirmed(e: MotionEvent): Boolean {
if (DEBUG)
Log.d(TAG, "onSingleTapConfirmed() called with: e = [$e]")
if (isDoubleTapping)
return true
if (player.popupPlayerSelected()) {
if (player.exoPlayerIsNull())
return false
onSingleTap(MainPlayer.PlayerType.POPUP)
return true
} else {
super.onSingleTapConfirmed(e)
if (player.currentState == Player.STATE_BLOCKED)
return true
onSingleTap(MainPlayer.PlayerType.VIDEO)
}
return true
}
override fun onLongPress(e: MotionEvent?) {
if (player.popupPlayerSelected()) {
player.updateScreenSize()
player.checkPopupPositionBounds()
player.changePopupSize(player.screenWidth.toInt())
}
}
override fun onScroll(
initialEvent: MotionEvent,
movingEvent: MotionEvent,
distanceX: Float,
distanceY: Float
): Boolean {
return if (player.popupPlayerSelected()) {
onScrollInPopup(initialEvent, movingEvent, distanceX, distanceY)
} else {
onScrollInMain(initialEvent, movingEvent, distanceX, distanceY)
}
}
override fun onFling(
e1: MotionEvent?,
e2: MotionEvent?,
velocityX: Float,
velocityY: Float
): Boolean {
return if (player.popupPlayerSelected()) {
val absVelocityX = abs(velocityX)
val absVelocityY = abs(velocityY)
if (absVelocityX.coerceAtLeast(absVelocityY) > tossFlingVelocity) {
if (absVelocityX > tossFlingVelocity) {
player.popupLayoutParams!!.x = velocityX.toInt()
}
if (absVelocityY > tossFlingVelocity) {
player.popupLayoutParams!!.y = velocityY.toInt()
}
player.checkPopupPositionBounds()
player.windowManager!!.updateViewLayout(player.rootView, player.popupLayoutParams)
return true
}
return false
} else {
true
}
}
private fun onScrollInMain(
initialEvent: MotionEvent,
movingEvent: MotionEvent,
distanceX: Float,
distanceY: Float
): Boolean {
if (!player.isFullscreen) {
return false
}
val isTouchingStatusBar: Boolean = initialEvent.y < getStatusBarHeight(service)
val isTouchingNavigationBar: Boolean =
initialEvent.y > (player.rootView.height - getNavigationBarHeight(service))
if (isTouchingStatusBar || isTouchingNavigationBar) {
return false
}
val insideThreshold = abs(movingEvent.y - initialEvent.y) <= MOVEMENT_THRESHOLD
if (
!isMovingInMain && (insideThreshold || abs(distanceX) > abs(distanceY)) ||
player.currentState == Player.STATE_COMPLETED
) {
return false
}
isMovingInMain = true
onScroll(
MainPlayer.PlayerType.VIDEO,
getDisplayHalfPortion(initialEvent),
initialEvent,
movingEvent,
distanceX,
distanceY
)
return true
}
private fun onScrollInPopup(
initialEvent: MotionEvent,
movingEvent: MotionEvent,
distanceX: Float,
distanceY: Float
): Boolean {
if (isResizing) {
return super.onScroll(initialEvent, movingEvent, distanceX, distanceY)
}
if (!isMovingInPopup) {
player.closeOverlayButton.animate(true, 200)
}
isMovingInPopup = true
val diffX: Float = (movingEvent.rawX - initialEvent.rawX)
var posX: Float = (initialPopupX + diffX)
val diffY: Float = (movingEvent.rawY - initialEvent.rawY)
var posY: Float = (initialPopupY + diffY)
if (posX > player.screenWidth - player.popupLayoutParams!!.width) {
posX = (player.screenWidth - player.popupLayoutParams!!.width)
} else if (posX < 0) {
posX = 0f
}
if (posY > player.screenHeight - player.popupLayoutParams!!.height) {
posY = (player.screenHeight - player.popupLayoutParams!!.height)
} else if (posY < 0) {
posY = 0f
}
player.popupLayoutParams!!.x = posX.toInt()
player.popupLayoutParams!!.y = posY.toInt()
onScroll(
MainPlayer.PlayerType.POPUP,
getDisplayHalfPortion(initialEvent),
initialEvent,
movingEvent,
distanceX,
distanceY
)
player.windowManager!!.updateViewLayout(player.rootView, player.popupLayoutParams)
return true
}
// ///////////////////////////////////////////////////////////////////
// Multi double tapping
// ///////////////////////////////////////////////////////////////////
var doubleTapControls: DoubleTapListener? = null
private set
private val isDoubleTapEnabled: Boolean
get() = doubleTapDelay > 0
var isDoubleTapping = false
private set
fun doubleTapControls(listener: DoubleTapListener) = apply {
doubleTapControls = listener
}
private var doubleTapDelay = DOUBLE_TAP_DELAY
private val doubleTapHandler: Handler = Handler()
private val doubleTapRunnable = Runnable {
if (DEBUG)
Log.d(TAG, "doubleTapRunnable called")
isDoubleTapping = false
doubleTapControls?.onDoubleTapFinished()
}
fun startMultiDoubleTap(e: MotionEvent) {
if (!isDoubleTapping) {
if (DEBUG)
Log.d(TAG, "startMultiDoubleTap called with e = [$e]")
keepInDoubleTapMode()
doubleTapControls?.onDoubleTapStarted(getDisplayPortion(e))
}
}
fun keepInDoubleTapMode() {
if (DEBUG)
Log.d(TAG, "keepInDoubleTapMode called")
isDoubleTapping = true
doubleTapHandler.removeCallbacks(doubleTapRunnable)
doubleTapHandler.postDelayed(doubleTapRunnable, doubleTapDelay)
}
fun endMultiDoubleTap() {
if (DEBUG)
Log.d(TAG, "endMultiDoubleTap called")
isDoubleTapping = false
doubleTapHandler.removeCallbacks(doubleTapRunnable)
doubleTapControls?.onDoubleTapFinished()
}
// ///////////////////////////////////////////////////////////////////
// Utils
// ///////////////////////////////////////////////////////////////////
private fun getDisplayPortion(e: MotionEvent): DisplayPortion {
return if (player.playerType == MainPlayer.PlayerType.POPUP && player.popupLayoutParams != null) {
when {
e.x < player.popupLayoutParams!!.width / 3.0 -> DisplayPortion.LEFT
e.x > player.popupLayoutParams!!.width * 2.0 / 3.0 -> DisplayPortion.RIGHT
else -> DisplayPortion.MIDDLE
}
} else /* MainPlayer.PlayerType.VIDEO */ {
when {
e.x < player.rootView.width / 3.0 -> DisplayPortion.LEFT
e.x > player.rootView.width * 2.0 / 3.0 -> DisplayPortion.RIGHT
else -> DisplayPortion.MIDDLE
}
}
}
// Currently needed for scrolling since there is no action more the middle portion
private fun getDisplayHalfPortion(e: MotionEvent): DisplayPortion {
return if (player.playerType == MainPlayer.PlayerType.POPUP) {
when {
e.x < player.popupLayoutParams!!.width / 2.0 -> DisplayPortion.LEFT_HALF
else -> DisplayPortion.RIGHT_HALF
}
} else /* MainPlayer.PlayerType.VIDEO */ {
when {
e.x < player.rootView.width / 2.0 -> DisplayPortion.LEFT_HALF
else -> DisplayPortion.RIGHT_HALF
}
}
}
private fun getNavigationBarHeight(context: Context): Int {
val resId = context.resources
.getIdentifier("navigation_bar_height", "dimen", "android")
return if (resId > 0) {
context.resources.getDimensionPixelSize(resId)
} else 0
}
private fun getStatusBarHeight(context: Context): Int {
val resId = context.resources
.getIdentifier("status_bar_height", "dimen", "android")
return if (resId > 0) {
context.resources.getDimensionPixelSize(resId)
} else 0
}
companion object {
private const val TAG = "BasePlayerGestListener"
private val DEBUG = Player.DEBUG
private const val DOUBLE_TAP_DELAY = 550L
private const val MOVEMENT_THRESHOLD = 40
}
}

View File

@ -1,7 +0,0 @@
package org.schabi.newpipe.player.event
interface DoubleTapListener {
fun onDoubleTapStarted(portion: DisplayPortion) {}
fun onDoubleTapProgressDown(portion: DisplayPortion) {}
fun onDoubleTapFinished() {}
}

View File

@ -1,6 +1,5 @@
package org.schabi.newpipe.player.event; package org.schabi.newpipe.player.event;
import com.google.android.exoplayer2.PlaybackParameters; import com.google.android.exoplayer2.PlaybackParameters;
import org.schabi.newpipe.extractor.stream.StreamInfo; import org.schabi.newpipe.extractor.stream.StreamInfo;

View File

@ -1,256 +0,0 @@
package org.schabi.newpipe.player.event;
import static org.schabi.newpipe.ktx.AnimationType.ALPHA;
import static org.schabi.newpipe.ktx.AnimationType.SCALE_AND_ALPHA;
import static org.schabi.newpipe.ktx.ViewUtils.animate;
import static org.schabi.newpipe.player.Player.DEFAULT_CONTROLS_DURATION;
import static org.schabi.newpipe.player.Player.DEFAULT_CONTROLS_HIDE_TIME;
import static org.schabi.newpipe.player.Player.STATE_PLAYING;
import android.app.Activity;
import android.util.Log;
import android.view.MotionEvent;
import android.view.View;
import android.view.Window;
import android.view.WindowManager;
import android.widget.ProgressBar;
import androidx.annotation.NonNull;
import androidx.appcompat.content.res.AppCompatResources;
import org.schabi.newpipe.MainActivity;
import org.schabi.newpipe.R;
import org.schabi.newpipe.player.MainPlayer;
import org.schabi.newpipe.player.Player;
import org.schabi.newpipe.player.helper.PlayerHelper;
/**
* GestureListener for the player
*
* While {@link BasePlayerGestureListener} contains the logic behind the single gestures
* this class focuses on the visual aspect like hiding and showing the controls or changing
* volume/brightness during scrolling for specific events.
*/
public class PlayerGestureListener
extends BasePlayerGestureListener
implements View.OnTouchListener {
private static final String TAG = PlayerGestureListener.class.getSimpleName();
private static final boolean DEBUG = MainActivity.DEBUG;
private final int maxVolume;
public PlayerGestureListener(final Player player, final MainPlayer service) {
super(player, service);
maxVolume = player.getAudioReactor().getMaxVolume();
}
@Override
public void onDoubleTap(@NonNull final MotionEvent event,
@NonNull final DisplayPortion portion) {
if (DEBUG) {
Log.d(TAG, "onDoubleTap called with playerType = ["
+ player.getPlayerType() + "], portion = [" + portion + "]");
}
if (player.isSomePopupMenuVisible()) {
player.hideControls(0, 0);
}
if (portion == DisplayPortion.LEFT || portion == DisplayPortion.RIGHT) {
startMultiDoubleTap(event);
} else if (portion == DisplayPortion.MIDDLE) {
player.playPause();
}
}
@Override
public void onSingleTap(@NonNull final MainPlayer.PlayerType playerType) {
if (DEBUG) {
Log.d(TAG, "onSingleTap called with playerType = [" + player.getPlayerType() + "]");
}
if (player.isControlsVisible()) {
player.hideControls(150, 0);
return;
}
// -- Controls are not visible --
// When player is completed show controls and don't hide them later
if (player.getCurrentState() == Player.STATE_COMPLETED) {
player.showControls(0);
} else {
player.showControlsThenHide();
}
}
@Override
public void onScroll(@NonNull final MainPlayer.PlayerType playerType,
@NonNull final DisplayPortion portion,
@NonNull final MotionEvent initialEvent,
@NonNull final MotionEvent movingEvent,
final float distanceX, final float distanceY) {
if (DEBUG) {
Log.d(TAG, "onScroll called with playerType = ["
+ player.getPlayerType() + "], portion = [" + portion + "]");
}
if (playerType == MainPlayer.PlayerType.VIDEO) {
// -- Brightness and Volume control --
final boolean isBrightnessGestureEnabled =
PlayerHelper.isBrightnessGestureEnabled(service);
final boolean isVolumeGestureEnabled = PlayerHelper.isVolumeGestureEnabled(service);
if (isBrightnessGestureEnabled && isVolumeGestureEnabled) {
if (portion == DisplayPortion.LEFT_HALF) {
onScrollMainBrightness(distanceX, distanceY);
} else /* DisplayPortion.RIGHT_HALF */ {
onScrollMainVolume(distanceX, distanceY);
}
} else if (isBrightnessGestureEnabled) {
onScrollMainBrightness(distanceX, distanceY);
} else if (isVolumeGestureEnabled) {
onScrollMainVolume(distanceX, distanceY);
}
} else /* MainPlayer.PlayerType.POPUP */ {
// -- Determine if the ClosingOverlayView (red X) has to be shown or hidden --
final View closingOverlayView = player.getClosingOverlayView();
final boolean showClosingOverlayView = player.isInsideClosingRadius(movingEvent);
// Check if an view is in expected state and if not animate it into the correct state
final int expectedVisibility = showClosingOverlayView ? View.VISIBLE : View.GONE;
if (closingOverlayView.getVisibility() != expectedVisibility) {
animate(closingOverlayView, showClosingOverlayView, 200);
}
}
}
private void onScrollMainVolume(final float distanceX, final float distanceY) {
// If we just started sliding, change the progress bar to match the system volume
if (player.getVolumeRelativeLayout().getVisibility() != View.VISIBLE) {
final float volumePercent = player
.getAudioReactor().getVolume() / (float) maxVolume;
player.getVolumeProgressBar().setProgress(
(int) (volumePercent * player.getMaxGestureLength()));
}
player.getVolumeProgressBar().incrementProgressBy((int) distanceY);
final float currentProgressPercent = (float) player
.getVolumeProgressBar().getProgress() / player.getMaxGestureLength();
final int currentVolume = (int) (maxVolume * currentProgressPercent);
player.getAudioReactor().setVolume(currentVolume);
if (DEBUG) {
Log.d(TAG, "onScroll().volumeControl, currentVolume = " + currentVolume);
}
player.getVolumeImageView().setImageDrawable(
AppCompatResources.getDrawable(service, currentProgressPercent <= 0
? R.drawable.ic_volume_off
: currentProgressPercent < 0.25 ? R.drawable.ic_volume_mute
: currentProgressPercent < 0.75 ? R.drawable.ic_volume_down
: R.drawable.ic_volume_up)
);
if (player.getVolumeRelativeLayout().getVisibility() != View.VISIBLE) {
animate(player.getVolumeRelativeLayout(), true, 200, SCALE_AND_ALPHA);
}
if (player.getBrightnessRelativeLayout().getVisibility() == View.VISIBLE) {
player.getBrightnessRelativeLayout().setVisibility(View.GONE);
}
}
private void onScrollMainBrightness(final float distanceX, final float distanceY) {
final Activity parent = player.getParentActivity();
if (parent == null) {
return;
}
final Window window = parent.getWindow();
final WindowManager.LayoutParams layoutParams = window.getAttributes();
final ProgressBar bar = player.getBrightnessProgressBar();
final float oldBrightness = layoutParams.screenBrightness;
bar.setProgress((int) (bar.getMax() * Math.max(0, Math.min(1, oldBrightness))));
bar.incrementProgressBy((int) distanceY);
final float currentProgressPercent = (float) bar.getProgress() / bar.getMax();
layoutParams.screenBrightness = currentProgressPercent;
window.setAttributes(layoutParams);
// Save current brightness level
PlayerHelper.setScreenBrightness(parent, currentProgressPercent);
if (DEBUG) {
Log.d(TAG, "onScroll().brightnessControl, "
+ "currentBrightness = " + currentProgressPercent);
}
player.getBrightnessImageView().setImageDrawable(
AppCompatResources.getDrawable(service,
currentProgressPercent < 0.25
? R.drawable.ic_brightness_low
: currentProgressPercent < 0.75
? R.drawable.ic_brightness_medium
: R.drawable.ic_brightness_high)
);
if (player.getBrightnessRelativeLayout().getVisibility() != View.VISIBLE) {
animate(player.getBrightnessRelativeLayout(), true, 200, SCALE_AND_ALPHA);
}
if (player.getVolumeRelativeLayout().getVisibility() == View.VISIBLE) {
player.getVolumeRelativeLayout().setVisibility(View.GONE);
}
}
@Override
public void onScrollEnd(@NonNull final MainPlayer.PlayerType playerType,
@NonNull final MotionEvent event) {
if (DEBUG) {
Log.d(TAG, "onScrollEnd called with playerType = ["
+ player.getPlayerType() + "]");
}
if (player.isControlsVisible() && player.getCurrentState() == STATE_PLAYING) {
player.hideControls(DEFAULT_CONTROLS_DURATION, DEFAULT_CONTROLS_HIDE_TIME);
}
if (playerType == MainPlayer.PlayerType.VIDEO) {
if (player.getVolumeRelativeLayout().getVisibility() == View.VISIBLE) {
animate(player.getVolumeRelativeLayout(), false, 200, SCALE_AND_ALPHA,
200);
}
if (player.getBrightnessRelativeLayout().getVisibility() == View.VISIBLE) {
animate(player.getBrightnessRelativeLayout(), false, 200, SCALE_AND_ALPHA,
200);
}
} else /* Popup-Player */ {
if (player.isInsideClosingRadius(event)) {
player.closePopup();
} else if (!player.isPopupClosing()) {
animate(player.getCloseOverlayButton(), false, 200);
animate(player.getClosingOverlayView(), false, 200);
}
}
}
@Override
public void onPopupResizingStart() {
if (DEBUG) {
Log.d(TAG, "onPopupResizingStart called");
}
player.getLoadingPanel().setVisibility(View.GONE);
player.hideControls(0, 0);
animate(player.getFastSeekOverlay(), false, 0);
animate(player.getCurrentDisplaySeek(), false, 0, ALPHA, 0);
}
@Override
public void onPopupResizingEnd() {
if (DEBUG) {
Log.d(TAG, "onPopupResizingEnd called");
}
}
}

View File

@ -3,6 +3,8 @@ package org.schabi.newpipe.player.event;
import com.google.android.exoplayer2.PlaybackException; import com.google.android.exoplayer2.PlaybackException;
public interface PlayerServiceEventListener extends PlayerEventListener { public interface PlayerServiceEventListener extends PlayerEventListener {
void onViewCreated();
void onFullscreenStateChanged(boolean fullscreen); void onFullscreenStateChanged(boolean fullscreen);
void onScreenRotationButtonClicked(); void onScreenRotationButtonClicked();

View File

@ -1,11 +1,11 @@
package org.schabi.newpipe.player.event; package org.schabi.newpipe.player.event;
import org.schabi.newpipe.player.MainPlayer; import org.schabi.newpipe.player.PlayerService;
import org.schabi.newpipe.player.Player; import org.schabi.newpipe.player.Player;
public interface PlayerServiceExtendedEventListener extends PlayerServiceEventListener { public interface PlayerServiceExtendedEventListener extends PlayerServiceEventListener {
void onServiceConnected(Player player, void onServiceConnected(Player player,
MainPlayer playerService, PlayerService playerService,
boolean playAfterConnect); boolean playAfterConnect);
void onServiceDisconnected(); void onServiceDisconnected();
} }

View File

@ -0,0 +1,186 @@
package org.schabi.newpipe.player.gesture
import android.os.Handler
import android.os.Looper
import android.util.Log
import android.view.GestureDetector
import android.view.MotionEvent
import android.view.View
import org.schabi.newpipe.databinding.PlayerBinding
import org.schabi.newpipe.player.Player
import org.schabi.newpipe.player.ui.VideoPlayerUi
/**
* Base gesture handling for [Player]
*
* This class contains the logic for the player gestures like View preparations
* and provides some abstract methods to make it easier separating the logic from the UI.
*/
abstract class BasePlayerGestureListener(
private val playerUi: VideoPlayerUi,
) : GestureDetector.SimpleOnGestureListener(), View.OnTouchListener {
protected val player: Player = playerUi.player
protected val binding: PlayerBinding = playerUi.binding
override fun onTouch(v: View, event: MotionEvent): Boolean {
playerUi.gestureDetector.onTouchEvent(event)
return false
}
private fun onDoubleTap(
event: MotionEvent,
portion: DisplayPortion
) {
if (DEBUG) {
Log.d(
TAG,
"onDoubleTap called with playerType = [" +
player.playerType + "], portion = [" + portion + "]"
)
}
if (playerUi.isSomePopupMenuVisible) {
playerUi.hideControls(0, 0)
}
if (portion === DisplayPortion.LEFT || portion === DisplayPortion.RIGHT) {
startMultiDoubleTap(event)
} else if (portion === DisplayPortion.MIDDLE) {
player.playPause()
}
}
protected fun onSingleTap() {
if (playerUi.isControlsVisible) {
playerUi.hideControls(150, 0)
return
}
// -- Controls are not visible --
// When player is completed show controls and don't hide them later
if (player.currentState == Player.STATE_COMPLETED) {
playerUi.showControls(0)
} else {
playerUi.showControlsThenHide()
}
}
open fun onScrollEnd(event: MotionEvent) {
if (DEBUG) {
Log.d(
TAG,
"onScrollEnd called with playerType = [" +
player.playerType + "]"
)
}
if (playerUi.isControlsVisible && player.currentState == Player.STATE_PLAYING) {
playerUi.hideControls(
VideoPlayerUi.DEFAULT_CONTROLS_DURATION,
VideoPlayerUi.DEFAULT_CONTROLS_HIDE_TIME
)
}
}
// ///////////////////////////////////////////////////////////////////
// Simple gestures
// ///////////////////////////////////////////////////////////////////
override fun onDown(e: MotionEvent): Boolean {
if (DEBUG)
Log.d(TAG, "onDown called with e = [$e]")
if (isDoubleTapping && isDoubleTapEnabled) {
doubleTapControls?.onDoubleTapProgressDown(getDisplayPortion(e))
return true
}
if (onDownNotDoubleTapping(e)) {
return super.onDown(e)
}
return true
}
/**
* @return true if `super.onDown(e)` should be called, false otherwise
*/
open fun onDownNotDoubleTapping(e: MotionEvent): Boolean {
return false // do not call super.onDown(e) by default, overridden for popup player
}
override fun onDoubleTap(e: MotionEvent): Boolean {
if (DEBUG)
Log.d(TAG, "onDoubleTap called with e = [$e]")
onDoubleTap(e, getDisplayPortion(e))
return true
}
// ///////////////////////////////////////////////////////////////////
// Multi double tapping
// ///////////////////////////////////////////////////////////////////
private var doubleTapControls: DoubleTapListener? = null
private val isDoubleTapEnabled: Boolean
get() = doubleTapDelay > 0
var isDoubleTapping = false
private set
fun doubleTapControls(listener: DoubleTapListener) = apply {
doubleTapControls = listener
}
private var doubleTapDelay = DOUBLE_TAP_DELAY
private val doubleTapHandler: Handler = Handler(Looper.getMainLooper())
private val doubleTapRunnable = Runnable {
if (DEBUG)
Log.d(TAG, "doubleTapRunnable called")
isDoubleTapping = false
doubleTapControls?.onDoubleTapFinished()
}
private fun startMultiDoubleTap(e: MotionEvent) {
if (!isDoubleTapping) {
if (DEBUG)
Log.d(TAG, "startMultiDoubleTap called with e = [$e]")
keepInDoubleTapMode()
doubleTapControls?.onDoubleTapStarted(getDisplayPortion(e))
}
}
fun keepInDoubleTapMode() {
if (DEBUG)
Log.d(TAG, "keepInDoubleTapMode called")
isDoubleTapping = true
doubleTapHandler.removeCallbacks(doubleTapRunnable)
doubleTapHandler.postDelayed(doubleTapRunnable, doubleTapDelay)
}
fun endMultiDoubleTap() {
if (DEBUG)
Log.d(TAG, "endMultiDoubleTap called")
isDoubleTapping = false
doubleTapHandler.removeCallbacks(doubleTapRunnable)
doubleTapControls?.onDoubleTapFinished()
}
// ///////////////////////////////////////////////////////////////////
// Utils
// ///////////////////////////////////////////////////////////////////
abstract fun getDisplayPortion(e: MotionEvent): DisplayPortion
// Currently needed for scrolling since there is no action more the middle portion
abstract fun getDisplayHalfPortion(e: MotionEvent): DisplayPortion
companion object {
private const val TAG = "BasePlayerGestListener"
private val DEBUG = Player.DEBUG
private const val DOUBLE_TAP_DELAY = 550L
}
}

View File

@ -1,4 +1,4 @@
package org.schabi.newpipe.player.event; package org.schabi.newpipe.player.gesture;
import android.content.Context; import android.content.Context;
import android.graphics.Rect; import android.graphics.Rect;
@ -8,24 +8,25 @@ import android.view.View;
import android.widget.FrameLayout; import android.widget.FrameLayout;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.coordinatorlayout.widget.CoordinatorLayout; import androidx.coordinatorlayout.widget.CoordinatorLayout;
import com.google.android.material.bottomsheet.BottomSheetBehavior; import com.google.android.material.bottomsheet.BottomSheetBehavior;
import org.schabi.newpipe.R; import org.schabi.newpipe.R;
import java.util.Arrays;
import java.util.List; import java.util.List;
public class CustomBottomSheetBehavior extends BottomSheetBehavior<FrameLayout> { public class CustomBottomSheetBehavior extends BottomSheetBehavior<FrameLayout> {
public CustomBottomSheetBehavior(final Context context, final AttributeSet attrs) { public CustomBottomSheetBehavior(@NonNull final Context context,
@Nullable final AttributeSet attrs) {
super(context, attrs); super(context, attrs);
} }
Rect globalRect = new Rect(); Rect globalRect = new Rect();
private boolean skippingInterception = false; private boolean skippingInterception = false;
private final List<Integer> skipInterceptionOfElements = Arrays.asList( private final List<Integer> skipInterceptionOfElements = List.of(
R.id.detail_content_root_layout, R.id.relatedItemsLayout, R.id.detail_content_root_layout, R.id.relatedItemsLayout,
R.id.itemsListPanel, R.id.view_pager, R.id.tab_layout, R.id.bottomControls, R.id.itemsListPanel, R.id.view_pager, R.id.tab_layout, R.id.bottomControls,
R.id.playPauseButton, R.id.playPreviousButton, R.id.playNextButton); R.id.playPauseButton, R.id.playPreviousButton, R.id.playNextButton);
@ -33,7 +34,7 @@ public class CustomBottomSheetBehavior extends BottomSheetBehavior<FrameLayout>
@Override @Override
public boolean onInterceptTouchEvent(@NonNull final CoordinatorLayout parent, public boolean onInterceptTouchEvent(@NonNull final CoordinatorLayout parent,
@NonNull final FrameLayout child, @NonNull final FrameLayout child,
final MotionEvent event) { @NonNull final MotionEvent event) {
// Drop following when action ends // Drop following when action ends
if (event.getAction() == MotionEvent.ACTION_CANCEL if (event.getAction() == MotionEvent.ACTION_CANCEL
|| event.getAction() == MotionEvent.ACTION_UP) { || event.getAction() == MotionEvent.ACTION_UP) {
@ -57,7 +58,7 @@ public class CustomBottomSheetBehavior extends BottomSheetBehavior<FrameLayout>
if (getState() == BottomSheetBehavior.STATE_EXPANDED if (getState() == BottomSheetBehavior.STATE_EXPANDED
&& event.getAction() == MotionEvent.ACTION_DOWN) { && event.getAction() == MotionEvent.ACTION_DOWN) {
// Without overriding scrolling will not work when user touches these elements // Without overriding scrolling will not work when user touches these elements
for (final Integer element : skipInterceptionOfElements) { for (final int element : skipInterceptionOfElements) {
final View view = child.findViewById(element); final View view = child.findViewById(element);
if (view != null) { if (view != null) {
final boolean visible = view.getGlobalVisibleRect(globalRect); final boolean visible = view.getGlobalVisibleRect(globalRect);

View File

@ -1,4 +1,4 @@
package org.schabi.newpipe.player.event package org.schabi.newpipe.player.gesture
enum class DisplayPortion { enum class DisplayPortion {
LEFT, MIDDLE, RIGHT, LEFT_HALF, RIGHT_HALF LEFT, MIDDLE, RIGHT, LEFT_HALF, RIGHT_HALF

View File

@ -0,0 +1,7 @@
package org.schabi.newpipe.player.gesture
interface DoubleTapListener {
fun onDoubleTapStarted(portion: DisplayPortion)
fun onDoubleTapProgressDown(portion: DisplayPortion)
fun onDoubleTapFinished()
}

View File

@ -0,0 +1,234 @@
package org.schabi.newpipe.player.gesture
import android.util.Log
import android.view.MotionEvent
import android.view.View
import android.view.View.OnTouchListener
import android.widget.ProgressBar
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.content.res.AppCompatResources
import androidx.core.view.isVisible
import org.schabi.newpipe.MainActivity
import org.schabi.newpipe.R
import org.schabi.newpipe.ktx.AnimationType
import org.schabi.newpipe.ktx.animate
import org.schabi.newpipe.player.Player
import org.schabi.newpipe.player.helper.AudioReactor
import org.schabi.newpipe.player.helper.PlayerHelper
import org.schabi.newpipe.player.ui.MainPlayerUi
import org.schabi.newpipe.util.ThemeHelper.getAndroidDimenPx
import kotlin.math.abs
import kotlin.math.max
import kotlin.math.min
/**
* GestureListener for the player
*
* While [BasePlayerGestureListener] contains the logic behind the single gestures
* this class focuses on the visual aspect like hiding and showing the controls or changing
* volume/brightness during scrolling for specific events.
*/
class MainPlayerGestureListener(
private val playerUi: MainPlayerUi
) : BasePlayerGestureListener(playerUi), OnTouchListener {
private var isMoving = false
override fun onTouch(v: View, event: MotionEvent): Boolean {
super.onTouch(v, event)
if (event.action == MotionEvent.ACTION_UP && isMoving) {
isMoving = false
onScrollEnd(event)
}
return when (event.action) {
MotionEvent.ACTION_DOWN, MotionEvent.ACTION_MOVE -> {
v.parent?.requestDisallowInterceptTouchEvent(playerUi.isFullscreen)
true
}
MotionEvent.ACTION_UP -> {
v.parent?.requestDisallowInterceptTouchEvent(false)
false
}
else -> true
}
}
override fun onSingleTapConfirmed(e: MotionEvent): Boolean {
if (DEBUG)
Log.d(TAG, "onSingleTapConfirmed() called with: e = [$e]")
if (isDoubleTapping)
return true
super.onSingleTapConfirmed(e)
if (player.currentState != Player.STATE_BLOCKED)
onSingleTap()
return true
}
private fun onScrollVolume(distanceY: Float) {
val bar: ProgressBar = binding.volumeProgressBar
val audioReactor: AudioReactor = player.audioReactor
// If we just started sliding, change the progress bar to match the system volume
if (!binding.volumeRelativeLayout.isVisible) {
val volumePercent: Float = audioReactor.volume / audioReactor.maxVolume.toFloat()
bar.progress = (volumePercent * bar.max).toInt()
}
// Update progress bar
binding.volumeProgressBar.incrementProgressBy(distanceY.toInt())
// Update volume
val currentProgressPercent: Float = bar.progress / bar.max.toFloat()
val currentVolume = (audioReactor.maxVolume * currentProgressPercent).toInt()
audioReactor.volume = currentVolume
if (DEBUG) {
Log.d(TAG, "onScroll().volumeControl, currentVolume = $currentVolume")
}
// Update player center image
binding.volumeImageView.setImageDrawable(
AppCompatResources.getDrawable(
player.context,
when {
currentProgressPercent <= 0 -> R.drawable.ic_volume_off
currentProgressPercent < 0.25 -> R.drawable.ic_volume_mute
currentProgressPercent < 0.75 -> R.drawable.ic_volume_down
else -> R.drawable.ic_volume_up
}
)
)
// Make sure the correct layout is visible
if (!binding.volumeRelativeLayout.isVisible) {
binding.volumeRelativeLayout.animate(true, 200, AnimationType.SCALE_AND_ALPHA)
}
binding.brightnessRelativeLayout.isVisible = false
}
private fun onScrollBrightness(distanceY: Float) {
val parent: AppCompatActivity = playerUi.parentActivity.orElse(null) ?: return
val window = parent.window
val layoutParams = window.attributes
val bar: ProgressBar = binding.brightnessProgressBar
// Update progress bar
val oldBrightness = layoutParams.screenBrightness
bar.progress = (bar.max * max(0f, min(1f, oldBrightness))).toInt()
bar.incrementProgressBy(distanceY.toInt())
// Update brightness
val currentProgressPercent = bar.progress.toFloat() / bar.max
layoutParams.screenBrightness = currentProgressPercent
window.attributes = layoutParams
// Save current brightness level
PlayerHelper.setScreenBrightness(parent, currentProgressPercent)
if (DEBUG) {
Log.d(
TAG,
"onScroll().brightnessControl, " +
"currentBrightness = " + currentProgressPercent
)
}
// Update player center image
binding.brightnessImageView.setImageDrawable(
AppCompatResources.getDrawable(
player.context,
when {
currentProgressPercent < 0.25 -> R.drawable.ic_brightness_low
currentProgressPercent < 0.75 -> R.drawable.ic_brightness_medium
else -> R.drawable.ic_brightness_high
}
)
)
// Make sure the correct layout is visible
if (!binding.brightnessRelativeLayout.isVisible) {
binding.brightnessRelativeLayout.animate(true, 200, AnimationType.SCALE_AND_ALPHA)
}
binding.volumeRelativeLayout.isVisible = false
}
override fun onScrollEnd(event: MotionEvent) {
super.onScrollEnd(event)
if (binding.volumeRelativeLayout.isVisible) {
binding.volumeRelativeLayout.animate(false, 200, AnimationType.SCALE_AND_ALPHA, 200)
}
if (binding.brightnessRelativeLayout.isVisible) {
binding.brightnessRelativeLayout.animate(false, 200, AnimationType.SCALE_AND_ALPHA, 200)
}
}
override fun onScroll(
initialEvent: MotionEvent,
movingEvent: MotionEvent,
distanceX: Float,
distanceY: Float
): Boolean {
if (!playerUi.isFullscreen) {
return false
}
// Calculate heights of status and navigation bars
val statusBarHeight = getAndroidDimenPx(player.context, "status_bar_height")
val navigationBarHeight = getAndroidDimenPx(player.context, "navigation_bar_height")
// Do not handle this event if initially it started from status or navigation bars
val isTouchingStatusBar = initialEvent.y < statusBarHeight
val isTouchingNavigationBar = initialEvent.y > (binding.root.height - navigationBarHeight)
if (isTouchingStatusBar || isTouchingNavigationBar) {
return false
}
val insideThreshold = abs(movingEvent.y - initialEvent.y) <= MOVEMENT_THRESHOLD
if (
!isMoving && (insideThreshold || abs(distanceX) > abs(distanceY)) ||
player.currentState == Player.STATE_COMPLETED
) {
return false
}
isMoving = true
// -- Brightness and Volume control --
val isBrightnessGestureEnabled = PlayerHelper.isBrightnessGestureEnabled(player.context)
val isVolumeGestureEnabled = PlayerHelper.isVolumeGestureEnabled(player.context)
if (isBrightnessGestureEnabled && isVolumeGestureEnabled) {
if (getDisplayHalfPortion(initialEvent) === DisplayPortion.LEFT_HALF) {
onScrollBrightness(distanceY)
} else /* DisplayPortion.RIGHT_HALF */ {
onScrollVolume(distanceY)
}
} else if (isBrightnessGestureEnabled) {
onScrollBrightness(distanceY)
} else if (isVolumeGestureEnabled) {
onScrollVolume(distanceY)
}
return true
}
override fun getDisplayPortion(e: MotionEvent): DisplayPortion {
return when {
e.x < binding.root.width / 3.0 -> DisplayPortion.LEFT
e.x > binding.root.width * 2.0 / 3.0 -> DisplayPortion.RIGHT
else -> DisplayPortion.MIDDLE
}
}
override fun getDisplayHalfPortion(e: MotionEvent): DisplayPortion {
return when {
e.x < binding.root.width / 2.0 -> DisplayPortion.LEFT_HALF
else -> DisplayPortion.RIGHT_HALF
}
}
companion object {
private val TAG = MainPlayerGestureListener::class.java.simpleName
private val DEBUG = MainActivity.DEBUG
private const val MOVEMENT_THRESHOLD = 40
}
}

View File

@ -0,0 +1,283 @@
package org.schabi.newpipe.player.gesture
import android.util.Log
import android.view.MotionEvent
import android.view.View
import android.view.ViewConfiguration
import androidx.core.math.MathUtils
import org.schabi.newpipe.MainActivity
import org.schabi.newpipe.ktx.AnimationType
import org.schabi.newpipe.ktx.animate
import org.schabi.newpipe.player.ui.PopupPlayerUi
import kotlin.math.abs
import kotlin.math.hypot
import kotlin.math.max
import kotlin.math.min
class PopupPlayerGestureListener(
private val playerUi: PopupPlayerUi,
) : BasePlayerGestureListener(playerUi) {
private var isMoving = false
private var initialPopupX: Int = -1
private var initialPopupY: Int = -1
private var isResizing = false
// initial coordinates and distance between fingers
private var initPointerDistance = -1.0
private var initFirstPointerX = -1f
private var initFirstPointerY = -1f
private var initSecPointerX = -1f
private var initSecPointerY = -1f
override fun onTouch(v: View, event: MotionEvent): Boolean {
super.onTouch(v, event)
if (event.pointerCount == 2 && !isMoving && !isResizing) {
if (DEBUG) {
Log.d(TAG, "onTouch() 2 finger pointer detected, enabling resizing.")
}
onPopupResizingStart()
// record coordinates of fingers
initFirstPointerX = event.getX(0)
initFirstPointerY = event.getY(0)
initSecPointerX = event.getX(1)
initSecPointerY = event.getY(1)
// record distance between fingers
initPointerDistance = hypot(
initFirstPointerX - initSecPointerX.toDouble(),
initFirstPointerY - initSecPointerY.toDouble()
)
isResizing = true
}
if (event.action == MotionEvent.ACTION_MOVE && !isMoving && isResizing) {
if (DEBUG) {
Log.d(
TAG,
"onTouch() ACTION_MOVE > v = [$v], e1.getRaw =" +
"[${event.rawX}, ${event.rawY}]"
)
}
return handleMultiDrag(event)
}
if (event.action == MotionEvent.ACTION_UP) {
if (DEBUG) {
Log.d(
TAG,
"onTouch() ACTION_UP > v = [$v], e1.getRaw =" +
" [${event.rawX}, ${event.rawY}]"
)
}
if (isMoving) {
isMoving = false
onScrollEnd(event)
}
if (isResizing) {
isResizing = false
initPointerDistance = (-1).toDouble()
initFirstPointerX = (-1).toFloat()
initFirstPointerY = (-1).toFloat()
initSecPointerX = (-1).toFloat()
initSecPointerY = (-1).toFloat()
onPopupResizingEnd()
player.changeState(player.currentState)
}
if (!playerUi.isPopupClosing) {
playerUi.savePopupPositionAndSizeToPrefs()
}
}
v.performClick()
return true
}
override fun onScrollEnd(event: MotionEvent) {
super.onScrollEnd(event)
if (playerUi.isInsideClosingRadius(event)) {
playerUi.closePopup()
} else if (!playerUi.isPopupClosing) {
playerUi.closeOverlayBinding.closeButton.animate(false, 200)
binding.closingOverlay.animate(false, 200)
}
}
private fun handleMultiDrag(event: MotionEvent): Boolean {
if (initPointerDistance == -1.0 || event.pointerCount != 2) {
return false
}
// get the movements of the fingers
val firstPointerMove = hypot(
event.getX(0) - initFirstPointerX.toDouble(),
event.getY(0) - initFirstPointerY.toDouble()
)
val secPointerMove = hypot(
event.getX(1) - initSecPointerX.toDouble(),
event.getY(1) - initSecPointerY.toDouble()
)
// minimum threshold beyond which pinch gesture will work
val minimumMove = ViewConfiguration.get(player.context).scaledTouchSlop
if (max(firstPointerMove, secPointerMove) <= minimumMove) {
return false
}
// calculate current distance between the pointers
val currentPointerDistance = hypot(
event.getX(0) - event.getX(1).toDouble(),
event.getY(0) - event.getY(1).toDouble()
)
val popupWidth = playerUi.popupLayoutParams.width.toDouble()
// change co-ordinates of popup so the center stays at the same position
val newWidth = popupWidth * currentPointerDistance / initPointerDistance
initPointerDistance = currentPointerDistance
playerUi.popupLayoutParams.x += ((popupWidth - newWidth) / 2.0).toInt()
playerUi.checkPopupPositionBounds()
playerUi.updateScreenSize()
playerUi.changePopupSize(min(playerUi.screenWidth.toDouble(), newWidth).toInt())
return true
}
private fun onPopupResizingStart() {
if (DEBUG) {
Log.d(TAG, "onPopupResizingStart called")
}
binding.loadingPanel.visibility = View.GONE
playerUi.hideControls(0, 0)
binding.fastSeekOverlay.animate(false, 0)
binding.currentDisplaySeek.animate(false, 0, AnimationType.ALPHA, 0)
}
private fun onPopupResizingEnd() {
if (DEBUG) {
Log.d(TAG, "onPopupResizingEnd called")
}
}
override fun onLongPress(e: MotionEvent?) {
playerUi.updateScreenSize()
playerUi.checkPopupPositionBounds()
playerUi.changePopupSize(playerUi.screenWidth)
}
override fun onFling(
e1: MotionEvent?,
e2: MotionEvent?,
velocityX: Float,
velocityY: Float
): Boolean {
return if (player.popupPlayerSelected()) {
val absVelocityX = abs(velocityX)
val absVelocityY = abs(velocityY)
if (absVelocityX.coerceAtLeast(absVelocityY) > TOSS_FLING_VELOCITY) {
if (absVelocityX > TOSS_FLING_VELOCITY) {
playerUi.popupLayoutParams.x = velocityX.toInt()
}
if (absVelocityY > TOSS_FLING_VELOCITY) {
playerUi.popupLayoutParams.y = velocityY.toInt()
}
playerUi.checkPopupPositionBounds()
playerUi.windowManager.updateViewLayout(binding.root, playerUi.popupLayoutParams)
return true
}
return false
} else {
true
}
}
override fun onDownNotDoubleTapping(e: MotionEvent): Boolean {
// Fix popup position when the user touch it, it may have the wrong one
// because the soft input is visible (the draggable area is currently resized).
playerUi.updateScreenSize()
playerUi.checkPopupPositionBounds()
playerUi.popupLayoutParams.let {
initialPopupX = it.x
initialPopupY = it.y
}
return true // we want `super.onDown(e)` to be called
}
override fun onSingleTapConfirmed(e: MotionEvent): Boolean {
if (DEBUG)
Log.d(TAG, "onSingleTapConfirmed() called with: e = [$e]")
if (isDoubleTapping)
return true
if (player.exoPlayerIsNull())
return false
onSingleTap()
return true
}
override fun onScroll(
initialEvent: MotionEvent,
movingEvent: MotionEvent,
distanceX: Float,
distanceY: Float
): Boolean {
if (isResizing) {
return super.onScroll(initialEvent, movingEvent, distanceX, distanceY)
}
if (!isMoving) {
playerUi.closeOverlayBinding.closeButton.animate(true, 200)
}
isMoving = true
val diffX = (movingEvent.rawX - initialEvent.rawX)
val posX = MathUtils.clamp(
initialPopupX + diffX,
0f, (playerUi.screenWidth - playerUi.popupLayoutParams.width).toFloat()
)
val diffY = (movingEvent.rawY - initialEvent.rawY)
val posY = MathUtils.clamp(
initialPopupY + diffY,
0f, (playerUi.screenHeight - playerUi.popupLayoutParams.height).toFloat()
)
playerUi.popupLayoutParams.x = posX.toInt()
playerUi.popupLayoutParams.y = posY.toInt()
// -- Determine if the ClosingOverlayView (red X) has to be shown or hidden --
val showClosingOverlayView: Boolean = playerUi.isInsideClosingRadius(movingEvent)
// Check if an view is in expected state and if not animate it into the correct state
val expectedVisibility = if (showClosingOverlayView) View.VISIBLE else View.GONE
if (binding.closingOverlay.visibility != expectedVisibility) {
binding.closingOverlay.animate(showClosingOverlayView, 200)
}
playerUi.windowManager.updateViewLayout(binding.root, playerUi.popupLayoutParams)
return true
}
override fun getDisplayPortion(e: MotionEvent): DisplayPortion {
return when {
e.x < playerUi.popupLayoutParams.width / 3.0 -> DisplayPortion.LEFT
e.x > playerUi.popupLayoutParams.width * 2.0 / 3.0 -> DisplayPortion.RIGHT
else -> DisplayPortion.MIDDLE
}
}
override fun getDisplayHalfPortion(e: MotionEvent): DisplayPortion {
return when {
e.x < playerUi.popupLayoutParams.width / 2.0 -> DisplayPortion.LEFT_HALF
else -> DisplayPortion.RIGHT_HALF
}
}
companion object {
private val TAG = PopupPlayerGestureListener::class.java.simpleName
private val DEBUG = MainActivity.DEBUG
private const val TOSS_FLING_VELOCITY = 2500
}
}

View File

@ -39,8 +39,8 @@ final class CacheFactory implements DataSource.Factory {
.createDataSource(); .createDataSource();
final FileDataSource fileSource = new FileDataSource(); final FileDataSource fileSource = new FileDataSource();
final CacheDataSink dataSink final CacheDataSink dataSink =
= new CacheDataSink(cache, PlayerHelper.getPreferredFileSize()); new CacheDataSink(cache, PlayerHelper.getPreferredFileSize());
return new CacheDataSource(cache, dataSource, fileSource, dataSink, CACHE_FLAGS, null); return new CacheDataSource(cache, dataSource, fileSource, dataSink, CACHE_FLAGS, null);
} }
} }

View File

@ -1,226 +0,0 @@
package org.schabi.newpipe.player.helper;
import android.content.Context;
import android.content.Intent;
import android.graphics.Bitmap;
import android.support.v4.media.MediaMetadataCompat;
import android.support.v4.media.session.MediaSessionCompat;
import android.support.v4.media.session.PlaybackStateCompat;
import android.util.Log;
import android.view.KeyEvent;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.media.session.MediaButtonReceiver;
import com.google.android.exoplayer2.ForwardingPlayer;
import com.google.android.exoplayer2.Player;
import com.google.android.exoplayer2.ext.mediasession.MediaSessionConnector;
import org.schabi.newpipe.MainActivity;
import org.schabi.newpipe.player.mediasession.MediaSessionCallback;
import org.schabi.newpipe.player.mediasession.PlayQueueNavigator;
import java.util.Optional;
public class MediaSessionManager {
private static final String TAG = MediaSessionManager.class.getSimpleName();
public static final boolean DEBUG = MainActivity.DEBUG;
@NonNull
private final MediaSessionCompat mediaSession;
@NonNull
private final MediaSessionConnector sessionConnector;
private int lastTitleHashCode;
private int lastArtistHashCode;
private long lastDuration;
private int lastAlbumArtHashCode;
public MediaSessionManager(@NonNull final Context context,
@NonNull final Player player,
@NonNull final MediaSessionCallback callback) {
mediaSession = new MediaSessionCompat(context, TAG);
mediaSession.setActive(true);
mediaSession.setPlaybackState(new PlaybackStateCompat.Builder()
.setState(PlaybackStateCompat.STATE_NONE, -1, 1)
.setActions(PlaybackStateCompat.ACTION_SEEK_TO
| PlaybackStateCompat.ACTION_PLAY
| PlaybackStateCompat.ACTION_PAUSE // was play and pause now play/pause
| PlaybackStateCompat.ACTION_SKIP_TO_NEXT
| PlaybackStateCompat.ACTION_SKIP_TO_PREVIOUS
| PlaybackStateCompat.ACTION_SET_REPEAT_MODE
| PlaybackStateCompat.ACTION_STOP)
.build());
sessionConnector = new MediaSessionConnector(mediaSession);
sessionConnector.setQueueNavigator(new PlayQueueNavigator(mediaSession, callback));
sessionConnector.setPlayer(new ForwardingPlayer(player) {
@Override
public void play() {
callback.play();
}
@Override
public void pause() {
callback.pause();
}
});
}
@Nullable
@SuppressWarnings("UnusedReturnValue")
public KeyEvent handleMediaButtonIntent(final Intent intent) {
return MediaButtonReceiver.handleIntent(mediaSession, intent);
}
public MediaSessionCompat.Token getSessionToken() {
return mediaSession.getSessionToken();
}
/**
* sets the Metadata - if required.
*
* @param title {@link MediaMetadataCompat#METADATA_KEY_TITLE}
* @param artist {@link MediaMetadataCompat#METADATA_KEY_ARTIST}
* @param optAlbumArt {@link MediaMetadataCompat#METADATA_KEY_ALBUM_ART}
* @param duration {@link MediaMetadataCompat#METADATA_KEY_DURATION}
* - should be a negative value for unknown durations, e.g. for livestreams
*/
public void setMetadata(@NonNull final String title,
@NonNull final String artist,
@NonNull final Optional<Bitmap> optAlbumArt,
final long duration
) {
if (DEBUG) {
Log.d(TAG, "setMetadata called:"
+ " t: " + title
+ " a: " + artist
+ " thumb: " + (
optAlbumArt.isPresent()
? optAlbumArt.get().hashCode()
: "<none>")
+ " d: " + duration);
}
if (!mediaSession.isActive()) {
if (DEBUG) {
Log.d(TAG, "setMetadata: mediaSession not active - exiting");
}
return;
}
if (!checkIfMetadataShouldBeSet(title, artist, optAlbumArt, duration)) {
if (DEBUG) {
Log.d(TAG, "setMetadata: No update required - exiting");
}
return;
}
if (DEBUG) {
Log.d(TAG, "setMetadata: N_Metadata update:"
+ " t: " + title
+ " a: " + artist
+ " thumb: " + (
optAlbumArt.isPresent()
? optAlbumArt.get().hashCode()
: "<none>")
+ " d: " + duration);
}
final MediaMetadataCompat.Builder builder = new MediaMetadataCompat.Builder()
.putString(MediaMetadataCompat.METADATA_KEY_TITLE, title)
.putString(MediaMetadataCompat.METADATA_KEY_ARTIST, artist)
.putLong(MediaMetadataCompat.METADATA_KEY_DURATION, duration);
if (optAlbumArt.isPresent()) {
builder.putBitmap(MediaMetadataCompat.METADATA_KEY_ALBUM_ART, optAlbumArt.get());
builder.putBitmap(MediaMetadataCompat.METADATA_KEY_DISPLAY_ICON, optAlbumArt.get());
}
mediaSession.setMetadata(builder.build());
lastTitleHashCode = title.hashCode();
lastArtistHashCode = artist.hashCode();
lastDuration = duration;
optAlbumArt.ifPresent(bitmap -> lastAlbumArtHashCode = bitmap.hashCode());
}
private boolean checkIfMetadataShouldBeSet(
@NonNull final String title,
@NonNull final String artist,
@NonNull final Optional<Bitmap> optAlbumArt,
final long duration
) {
// Check if the values have changed since the last time
if (title.hashCode() != lastTitleHashCode
|| artist.hashCode() != lastArtistHashCode
|| duration != lastDuration
|| (optAlbumArt.isPresent() && optAlbumArt.get().hashCode() != lastAlbumArtHashCode)
) {
if (DEBUG) {
Log.d(TAG,
"checkIfMetadataShouldBeSet: true - reason: changed values since last");
}
return true;
}
// Check if the currently set metadata is valid
if (getMetadataTitle() == null
|| getMetadataArtist() == null
// Note that the duration can be <= 0 for live streams
) {
if (DEBUG) {
if (getMetadataTitle() == null) {
Log.d(TAG,
"N_getMetadataTitle: title == null");
} else if (getMetadataArtist() == null) {
Log.d(TAG,
"N_getMetadataArtist: artist == null");
}
}
return true;
}
// If we got an album art check if the current set AlbumArt is null
if (optAlbumArt.isPresent() && getMetadataAlbumArt() == null) {
if (DEBUG) {
Log.d(TAG, "N_getMetadataAlbumArt: thumb == null");
}
return true;
}
// Default - no update required
return false;
}
@Nullable
private Bitmap getMetadataAlbumArt() {
return mediaSession.getController().getMetadata()
.getBitmap(MediaMetadataCompat.METADATA_KEY_ALBUM_ART);
}
@Nullable
private String getMetadataTitle() {
return mediaSession.getController().getMetadata()
.getString(MediaMetadataCompat.METADATA_KEY_TITLE);
}
@Nullable
private String getMetadataArtist() {
return mediaSession.getController().getMetadata()
.getString(MediaMetadataCompat.METADATA_KEY_ARTIST);
}
/**
* Should be called on player destruction to prevent leakage.
*/
public void dispose() {
sessionConnector.setPlayer(null);
sessionConnector.setQueueNavigator(null);
mediaSession.setActive(false);
mediaSession.release();
}
}

View File

@ -11,7 +11,6 @@ import android.graphics.drawable.Drawable;
import android.graphics.drawable.LayerDrawable; import android.graphics.drawable.LayerDrawable;
import android.os.Bundle; import android.os.Bundle;
import android.util.Log; import android.util.Log;
import android.view.LayoutInflater;
import android.view.View; import android.view.View;
import android.widget.CheckBox; import android.widget.CheckBox;
import android.widget.SeekBar; import android.widget.SeekBar;
@ -21,16 +20,16 @@ import androidx.annotation.NonNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.annotation.StringRes; import androidx.annotation.StringRes;
import androidx.appcompat.app.AlertDialog; import androidx.appcompat.app.AlertDialog;
import androidx.core.math.MathUtils;
import androidx.fragment.app.DialogFragment; import androidx.fragment.app.DialogFragment;
import androidx.preference.PreferenceManager; import androidx.preference.PreferenceManager;
import org.schabi.newpipe.R; import org.schabi.newpipe.R;
import org.schabi.newpipe.databinding.DialogPlaybackParameterBinding; import org.schabi.newpipe.databinding.DialogPlaybackParameterBinding;
import org.schabi.newpipe.player.Player; import org.schabi.newpipe.player.ui.VideoPlayerUi;
import org.schabi.newpipe.util.SimpleOnSeekBarChangeListener; import org.schabi.newpipe.util.SimpleOnSeekBarChangeListener;
import org.schabi.newpipe.util.SliderStrategy; import org.schabi.newpipe.util.SliderStrategy;
import java.util.HashMap;
import java.util.Map; import java.util.Map;
import java.util.Objects; import java.util.Objects;
import java.util.function.Consumer; import java.util.function.Consumer;
@ -149,7 +148,7 @@ public class PlaybackParameterDialog extends DialogFragment {
assureCorrectAppLanguage(getContext()); assureCorrectAppLanguage(getContext());
Icepick.restoreInstanceState(this, savedInstanceState); Icepick.restoreInstanceState(this, savedInstanceState);
binding = DialogPlaybackParameterBinding.inflate(LayoutInflater.from(getContext())); binding = DialogPlaybackParameterBinding.inflate(getLayoutInflater());
initUI(); initUI();
final AlertDialog.Builder dialogBuilder = new AlertDialog.Builder(requireActivity()) final AlertDialog.Builder dialogBuilder = new AlertDialog.Builder(requireActivity())
@ -207,7 +206,7 @@ public class PlaybackParameterDialog extends DialogFragment {
? View.VISIBLE ? View.VISIBLE
: View.GONE); : View.GONE);
animateRotation(binding.pitchToogleControlModes, animateRotation(binding.pitchToogleControlModes,
Player.DEFAULT_CONTROLS_DURATION, VideoPlayerUi.DEFAULT_CONTROLS_DURATION,
isCurrentlyVisible ? 180 : 0); isCurrentlyVisible ? 180 : 0);
}); });
@ -334,10 +333,8 @@ public class PlaybackParameterDialog extends DialogFragment {
} }
private Map<Boolean, TextView> getPitchControlModeComponentMappings() { private Map<Boolean, TextView> getPitchControlModeComponentMappings() {
final Map<Boolean, TextView> mappings = new HashMap<>(); return Map.of(PITCH_CTRL_MODE_PERCENT, binding.pitchControlModePercent,
mappings.put(PITCH_CTRL_MODE_PERCENT, binding.pitchControlModePercent); PITCH_CTRL_MODE_SEMITONE, binding.pitchControlModeSemitone);
mappings.put(PITCH_CTRL_MODE_SEMITONE, binding.pitchControlModeSemitone);
return mappings;
} }
private void changePitchControlMode(final boolean semitones) { private void changePitchControlMode(final boolean semitones) {
@ -407,13 +404,11 @@ public class PlaybackParameterDialog extends DialogFragment {
} }
private Map<Double, TextView> getStepSizeComponentMappings() { private Map<Double, TextView> getStepSizeComponentMappings() {
final Map<Double, TextView> mappings = new HashMap<>(); return Map.of(STEP_1_PERCENT_VALUE, binding.stepSizeOnePercent,
mappings.put(STEP_1_PERCENT_VALUE, binding.stepSizeOnePercent); STEP_5_PERCENT_VALUE, binding.stepSizeFivePercent,
mappings.put(STEP_5_PERCENT_VALUE, binding.stepSizeFivePercent); STEP_10_PERCENT_VALUE, binding.stepSizeTenPercent,
mappings.put(STEP_10_PERCENT_VALUE, binding.stepSizeTenPercent); STEP_25_PERCENT_VALUE, binding.stepSizeTwentyFivePercent,
mappings.put(STEP_25_PERCENT_VALUE, binding.stepSizeTwentyFivePercent); STEP_100_PERCENT_VALUE, binding.stepSizeOneHundredPercent);
mappings.put(STEP_100_PERCENT_VALUE, binding.stepSizeOneHundredPercent);
return mappings;
} }
private void setStepSizeToUI(final double newStepSize) { private void setStepSizeToUI(final double newStepSize) {
@ -532,7 +527,7 @@ public class PlaybackParameterDialog extends DialogFragment {
} }
private void setAndUpdateTempo(final double newTempo) { private void setAndUpdateTempo(final double newTempo) {
this.tempo = calcValidTempo(newTempo); this.tempo = MathUtils.clamp(newTempo, MIN_PITCH_OR_SPEED, MAX_PITCH_OR_SPEED);
binding.tempoSeekbar.setProgress(QUADRATIC_STRATEGY.progressOf(tempo)); binding.tempoSeekbar.setProgress(QUADRATIC_STRATEGY.progressOf(tempo));
setText(binding.tempoCurrentText, PlayerHelper::formatSpeed, tempo); setText(binding.tempoCurrentText, PlayerHelper::formatSpeed, tempo);
@ -551,13 +546,8 @@ public class PlaybackParameterDialog extends DialogFragment {
pitchPercent); pitchPercent);
} }
private double calcValidTempo(final double newTempo) {
return Math.max(MIN_PITCH_OR_SPEED, Math.min(MAX_PITCH_OR_SPEED, newTempo));
}
private double calcValidPitch(final double newPitch) { private double calcValidPitch(final double newPitch) {
final double calcPitch = final double calcPitch = MathUtils.clamp(newPitch, MIN_PITCH_OR_SPEED, MAX_PITCH_OR_SPEED);
Math.max(MIN_PITCH_OR_SPEED, Math.min(MAX_PITCH_OR_SPEED, newPitch));
if (!isCurrentPitchControlModeSemitone()) { if (!isCurrentPitchControlModeSemitone()) {
return calcPitch; return calcPitch;

View File

@ -208,8 +208,8 @@ public class PlayerDataSource {
Log.w(TAG, "instantiateCacheIfNeeded: could not create cache dir"); Log.w(TAG, "instantiateCacheIfNeeded: could not create cache dir");
} }
final LeastRecentlyUsedCacheEvictor evictor final LeastRecentlyUsedCacheEvictor evictor =
= new LeastRecentlyUsedCacheEvictor(PlayerHelper.getPreferredCacheSize()); new LeastRecentlyUsedCacheEvictor(PlayerHelper.getPreferredCacheSize());
cache = new SimpleCache(cacheDir, evictor, new StandaloneDatabaseProvider(context)); cache = new SimpleCache(cacheDir, evictor, new StandaloneDatabaseProvider(context));
} }
} }

View File

@ -3,8 +3,6 @@ package org.schabi.newpipe.player.helper;
import static com.google.android.exoplayer2.Player.REPEAT_MODE_ALL; import static com.google.android.exoplayer2.Player.REPEAT_MODE_ALL;
import static com.google.android.exoplayer2.Player.REPEAT_MODE_OFF; import static com.google.android.exoplayer2.Player.REPEAT_MODE_OFF;
import static com.google.android.exoplayer2.Player.REPEAT_MODE_ONE; import static com.google.android.exoplayer2.Player.REPEAT_MODE_ONE;
import static org.schabi.newpipe.player.Player.IDLE_WINDOW_FLAGS;
import static org.schabi.newpipe.player.Player.PLAYER_TYPE;
import static org.schabi.newpipe.player.helper.PlayerHelper.AutoplayType.AUTOPLAY_TYPE_ALWAYS; import static org.schabi.newpipe.player.helper.PlayerHelper.AutoplayType.AUTOPLAY_TYPE_ALWAYS;
import static org.schabi.newpipe.player.helper.PlayerHelper.AutoplayType.AUTOPLAY_TYPE_NEVER; import static org.schabi.newpipe.player.helper.PlayerHelper.AutoplayType.AUTOPLAY_TYPE_NEVER;
import static org.schabi.newpipe.player.helper.PlayerHelper.AutoplayType.AUTOPLAY_TYPE_WIFI; import static org.schabi.newpipe.player.helper.PlayerHelper.AutoplayType.AUTOPLAY_TYPE_WIFI;
@ -15,14 +13,8 @@ import static java.lang.annotation.RetentionPolicy.SOURCE;
import android.annotation.SuppressLint; import android.annotation.SuppressLint;
import android.content.Context; import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences; import android.content.SharedPreferences;
import android.graphics.PixelFormat;
import android.os.Build;
import android.provider.Settings; import android.provider.Settings;
import android.view.Gravity;
import android.view.ViewGroup;
import android.view.WindowManager;
import android.view.accessibility.CaptioningManager; import android.view.accessibility.CaptioningManager;
import androidx.annotation.IntDef; import androidx.annotation.IntDef;
@ -49,7 +41,6 @@ import org.schabi.newpipe.extractor.stream.StreamInfo;
import org.schabi.newpipe.extractor.stream.StreamInfoItem; import org.schabi.newpipe.extractor.stream.StreamInfoItem;
import org.schabi.newpipe.extractor.stream.SubtitlesStream; import org.schabi.newpipe.extractor.stream.SubtitlesStream;
import org.schabi.newpipe.extractor.utils.Utils; import org.schabi.newpipe.extractor.utils.Utils;
import org.schabi.newpipe.player.MainPlayer;
import org.schabi.newpipe.player.Player; import org.schabi.newpipe.player.Player;
import org.schabi.newpipe.player.playqueue.PlayQueue; import org.schabi.newpipe.player.playqueue.PlayQueue;
import org.schabi.newpipe.player.playqueue.PlayQueueItem; import org.schabi.newpipe.player.playqueue.PlayQueueItem;
@ -71,25 +62,11 @@ import java.util.concurrent.TimeUnit;
public final class PlayerHelper { public final class PlayerHelper {
private static final StringBuilder STRING_BUILDER = new StringBuilder(); private static final StringBuilder STRING_BUILDER = new StringBuilder();
private static final Formatter STRING_FORMATTER private static final Formatter STRING_FORMATTER =
= new Formatter(STRING_BUILDER, Locale.getDefault()); new Formatter(STRING_BUILDER, Locale.getDefault());
private static final NumberFormat SPEED_FORMATTER = new DecimalFormat("0.##x"); private static final NumberFormat SPEED_FORMATTER = new DecimalFormat("0.##x");
private static final NumberFormat PITCH_FORMATTER = new DecimalFormat("##%"); private static final NumberFormat PITCH_FORMATTER = new DecimalFormat("##%");
/**
* Maximum opacity allowed for Android 12 and higher to allow touches on other apps when using
* NewPipe's popup player.
*
* <p>
* This value is hardcoded instead of being get dynamically with the method linked of the
* constant documentation below, because it is not static and popup player layout parameters
* are generated with static methods.
* </p>
*
* @see WindowManager.LayoutParams#FLAG_NOT_TOUCHABLE
*/
private static final float MAXIMUM_OPACITY_ALLOWED_FOR_S_AND_HIGHER = 0.8f;
@Retention(SOURCE) @Retention(SOURCE)
@IntDef({AUTOPLAY_TYPE_ALWAYS, AUTOPLAY_TYPE_WIFI, @IntDef({AUTOPLAY_TYPE_ALWAYS, AUTOPLAY_TYPE_WIFI,
AUTOPLAY_TYPE_NEVER}) AUTOPLAY_TYPE_NEVER})
@ -339,10 +316,6 @@ public final class PlayerHelper {
return true; return true;
} }
public static int getTossFlingVelocity() {
return 2500;
}
@NonNull @NonNull
public static CaptionStyleCompat getCaptionStyle(@NonNull final Context context) { public static CaptionStyleCompat getCaptionStyle(@NonNull final Context context) {
final CaptioningManager captioningManager = ContextCompat.getSystemService(context, final CaptioningManager captioningManager = ContextCompat.getSystemService(context,
@ -452,12 +425,6 @@ public final class PlayerHelper {
// Utils used by player // Utils used by player
//////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////
public static MainPlayer.PlayerType retrievePlayerTypeFromIntent(final Intent intent) {
// If you want to open popup from the app just include Constants.POPUP_ONLY into an extra
return MainPlayer.PlayerType.values()[
intent.getIntExtra(PLAYER_TYPE, MainPlayer.PlayerType.VIDEO.ordinal())];
}
public static boolean isPlaybackResumeEnabled(final Player player) { public static boolean isPlaybackResumeEnabled(final Player player) {
return player.getPrefs().getBoolean( return player.getPrefs().getBoolean(
player.getContext().getString(R.string.enable_watch_history_key), true) player.getContext().getString(R.string.enable_watch_history_key), true)
@ -528,90 +495,10 @@ public final class PlayerHelper {
.apply(); .apply();
} }
/**
* @param player {@code screenWidth} and {@code screenHeight} must have been initialized
* @return the popup starting layout params
*/
@SuppressLint("RtlHardcoded")
public static WindowManager.LayoutParams retrievePopupLayoutParamsFromPrefs(
final Player player) {
final boolean popupRememberSizeAndPos = player.getPrefs().getBoolean(
player.getContext().getString(R.string.popup_remember_size_pos_key), true);
final float defaultSize =
player.getContext().getResources().getDimension(R.dimen.popup_default_width);
final float popupWidth = popupRememberSizeAndPos
? player.getPrefs().getFloat(player.getContext().getString(
R.string.popup_saved_width_key), defaultSize)
: defaultSize;
final float popupHeight = getMinimumVideoHeight(popupWidth);
final WindowManager.LayoutParams popupLayoutParams = new WindowManager.LayoutParams(
(int) popupWidth, (int) popupHeight,
popupLayoutParamType(),
IDLE_WINDOW_FLAGS,
PixelFormat.TRANSLUCENT);
popupLayoutParams.gravity = Gravity.LEFT | Gravity.TOP;
popupLayoutParams.softInputMode = WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE;
final int centerX = (int) (player.getScreenWidth() / 2f - popupWidth / 2f);
final int centerY = (int) (player.getScreenHeight() / 2f - popupHeight / 2f);
popupLayoutParams.x = popupRememberSizeAndPos
? player.getPrefs().getInt(player.getContext().getString(
R.string.popup_saved_x_key), centerX) : centerX;
popupLayoutParams.y = popupRememberSizeAndPos
? player.getPrefs().getInt(player.getContext().getString(
R.string.popup_saved_y_key), centerY) : centerY;
return popupLayoutParams;
}
public static void savePopupPositionAndSizeToPrefs(final Player player) {
if (player.getPopupLayoutParams() != null) {
player.getPrefs().edit()
.putFloat(player.getContext().getString(R.string.popup_saved_width_key),
player.getPopupLayoutParams().width)
.putInt(player.getContext().getString(R.string.popup_saved_x_key),
player.getPopupLayoutParams().x)
.putInt(player.getContext().getString(R.string.popup_saved_y_key),
player.getPopupLayoutParams().y)
.apply();
}
}
public static float getMinimumVideoHeight(final float width) { public static float getMinimumVideoHeight(final float width) {
return width / (16.0f / 9.0f); // Respect the 16:9 ratio that most videos have return width / (16.0f / 9.0f); // Respect the 16:9 ratio that most videos have
} }
@SuppressLint("RtlHardcoded")
public static WindowManager.LayoutParams buildCloseOverlayLayoutParams() {
final int flags = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
| WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE
| WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM;
final WindowManager.LayoutParams closeOverlayLayoutParams = new WindowManager.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT,
popupLayoutParamType(),
flags,
PixelFormat.TRANSLUCENT);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
// 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_S_AND_HIGHER;
}
closeOverlayLayoutParams.gravity = Gravity.LEFT | Gravity.TOP;
closeOverlayLayoutParams.softInputMode =
WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE;
return closeOverlayLayoutParams;
}
public static int popupLayoutParamType() {
return Build.VERSION.SDK_INT < Build.VERSION_CODES.O
? WindowManager.LayoutParams.TYPE_PHONE
: WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY;
}
public static int retrieveSeekDurationFromPreferences(final Player player) { public static int retrieveSeekDurationFromPreferences(final Player player) {
return Integer.parseInt(Objects.requireNonNull(player.getPrefs().getString( return Integer.parseInt(Objects.requireNonNull(player.getPrefs().getString(
player.getContext().getString(R.string.seek_duration_key), player.getContext().getString(R.string.seek_duration_key),

View File

@ -16,8 +16,9 @@ import com.google.android.exoplayer2.PlaybackParameters;
import org.schabi.newpipe.App; import org.schabi.newpipe.App;
import org.schabi.newpipe.MainActivity; import org.schabi.newpipe.MainActivity;
import org.schabi.newpipe.extractor.stream.StreamInfo; import org.schabi.newpipe.extractor.stream.StreamInfo;
import org.schabi.newpipe.player.MainPlayer; import org.schabi.newpipe.player.PlayerService;
import org.schabi.newpipe.player.Player; import org.schabi.newpipe.player.Player;
import org.schabi.newpipe.player.PlayerType;
import org.schabi.newpipe.player.event.PlayerServiceEventListener; import org.schabi.newpipe.player.event.PlayerServiceEventListener;
import org.schabi.newpipe.player.event.PlayerServiceExtendedEventListener; import org.schabi.newpipe.player.event.PlayerServiceExtendedEventListener;
import org.schabi.newpipe.player.playqueue.PlayQueue; import org.schabi.newpipe.player.playqueue.PlayQueue;
@ -42,17 +43,17 @@ public final class PlayerHolder {
private final PlayerServiceConnection serviceConnection = new PlayerServiceConnection(); private final PlayerServiceConnection serviceConnection = new PlayerServiceConnection();
private boolean bound; private boolean bound;
@Nullable private MainPlayer playerService; @Nullable private PlayerService playerService;
@Nullable private Player player; @Nullable private Player player;
/** /**
* Returns the current {@link MainPlayer.PlayerType} of the {@link MainPlayer} service, * Returns the current {@link PlayerType} of the {@link PlayerService} service,
* otherwise `null` if no service running. * otherwise `null` if no service is running.
* *
* @return Current PlayerType * @return Current PlayerType
*/ */
@Nullable @Nullable
public MainPlayer.PlayerType getType() { public PlayerType getType() {
if (player == null) { if (player == null) {
return null; return null;
} }
@ -122,7 +123,7 @@ public final class PlayerHolder {
// and NullPointerExceptions inside the service because the service will be // and NullPointerExceptions inside the service because the service will be
// bound twice. Prevent it with unbinding first // bound twice. Prevent it with unbinding first
unbind(context); unbind(context);
ContextCompat.startForegroundService(context, new Intent(context, MainPlayer.class)); ContextCompat.startForegroundService(context, new Intent(context, PlayerService.class));
serviceConnection.doPlayAfterConnect(playAfterConnect); serviceConnection.doPlayAfterConnect(playAfterConnect);
bind(context); bind(context);
} }
@ -130,7 +131,7 @@ public final class PlayerHolder {
public void stopService() { public void stopService() {
final Context context = getCommonContext(); final Context context = getCommonContext();
unbind(context); unbind(context);
context.stopService(new Intent(context, MainPlayer.class)); context.stopService(new Intent(context, PlayerService.class));
} }
class PlayerServiceConnection implements ServiceConnection { class PlayerServiceConnection implements ServiceConnection {
@ -156,7 +157,7 @@ public final class PlayerHolder {
if (DEBUG) { if (DEBUG) {
Log.d(TAG, "Player service is connected"); Log.d(TAG, "Player service is connected");
} }
final MainPlayer.LocalBinder localBinder = (MainPlayer.LocalBinder) service; final PlayerService.LocalBinder localBinder = (PlayerService.LocalBinder) service;
playerService = localBinder.getService(); playerService = localBinder.getService();
player = localBinder.getPlayer(); player = localBinder.getPlayer();
@ -172,7 +173,7 @@ public final class PlayerHolder {
Log.d(TAG, "bind() called"); Log.d(TAG, "bind() called");
} }
final Intent serviceIntent = new Intent(context, MainPlayer.class); final Intent serviceIntent = new Intent(context, PlayerService.class);
bound = context.bindService(serviceIntent, serviceConnection, bound = context.bindService(serviceIntent, serviceConnection,
Context.BIND_AUTO_CREATE); Context.BIND_AUTO_CREATE);
if (!bound) { if (!bound) {
@ -211,6 +212,13 @@ public final class PlayerHolder {
private final PlayerServiceEventListener internalListener = private final PlayerServiceEventListener internalListener =
new PlayerServiceEventListener() { new PlayerServiceEventListener() {
@Override
public void onViewCreated() {
if (listener != null) {
listener.onViewCreated();
}
}
@Override @Override
public void onFullscreenStateChanged(final boolean fullscreen) { public void onFullscreenStateChanged(final boolean fullscreen) {
if (listener != null) { if (listener != null) {

View File

@ -1,5 +1,7 @@
package org.schabi.newpipe.player.helper; package org.schabi.newpipe.player.helper;
import androidx.core.math.MathUtils;
/** /**
* Converts between percent and 12-tone equal temperament semitones. * Converts between percent and 12-tone equal temperament semitones.
* <br/> * <br/>
@ -33,6 +35,6 @@ public final class PlayerSemitoneHelper {
} }
private static int ensureSemitonesInRange(final int semitones) { private static int ensureSemitonesInRange(final int semitones) {
return Math.max(-SEMITONE_COUNT, Math.min(SEMITONE_COUNT, semitones)); return MathUtils.clamp(semitones, -SEMITONE_COUNT, SEMITONE_COUNT);
} }
} }

View File

@ -1,47 +0,0 @@
package org.schabi.newpipe.player.listeners.view
import android.util.Log
import android.view.View
import androidx.appcompat.widget.PopupMenu
import org.schabi.newpipe.MainActivity
import org.schabi.newpipe.player.Player
import org.schabi.newpipe.player.helper.PlaybackParameterDialog
/**
* Click listener for the playbackSpeed textview of the player
*/
class PlaybackSpeedClickListener(
private val player: Player,
private val playbackSpeedPopupMenu: PopupMenu
) : View.OnClickListener {
companion object {
private const val TAG: String = "PlaybSpeedClickListener"
}
override fun onClick(v: View) {
if (MainActivity.DEBUG) {
Log.d(TAG, "onPlaybackSpeedClicked() called")
}
if (player.videoPlayerSelected()) {
PlaybackParameterDialog.newInstance(
player.playbackSpeed.toDouble(),
player.playbackPitch.toDouble(),
player.playbackSkipSilence
) { speed: Float, pitch: Float, skipSilence: Boolean ->
player.setPlaybackParameters(
speed,
pitch,
skipSilence
)
}
.show(player.parentActivity!!.supportFragmentManager, null)
} else {
playbackSpeedPopupMenu.show()
player.isSomePopupMenuVisible = true
}
player.manageControlsAfterOnClick(v)
}
}

View File

@ -1,41 +0,0 @@
package org.schabi.newpipe.player.listeners.view
import android.annotation.SuppressLint
import android.util.Log
import android.view.View
import androidx.appcompat.widget.PopupMenu
import org.schabi.newpipe.MainActivity
import org.schabi.newpipe.extractor.MediaFormat
import org.schabi.newpipe.player.Player
/**
* Click listener for the qualityTextView of the player
*/
class QualityClickListener(
private val player: Player,
private val qualityPopupMenu: PopupMenu
) : View.OnClickListener {
companion object {
private const val TAG: String = "QualityClickListener"
}
@SuppressLint("SetTextI18n") // we don't need I18N because of a " "
override fun onClick(v: View) {
if (MainActivity.DEBUG) {
Log.d(TAG, "onQualitySelectorClicked() called")
}
qualityPopupMenu.show()
player.isSomePopupMenuVisible = true
val videoStream = player.selectedVideoStream
if (videoStream != null) {
player.binding.qualityTextView.text =
MediaFormat.getNameById(videoStream.formatId) + " " + videoStream.getResolution()
}
player.saveWasPlaying()
player.manageControlsAfterOnClick(v)
}
}

View File

@ -3,6 +3,7 @@ package org.schabi.newpipe.player.mediaitem;
import android.net.Uri; import android.net.Uri;
import com.google.android.exoplayer2.MediaItem; import com.google.android.exoplayer2.MediaItem;
import com.google.android.exoplayer2.MediaItem.RequestMetadata;
import com.google.android.exoplayer2.MediaMetadata; import com.google.android.exoplayer2.MediaMetadata;
import com.google.android.exoplayer2.Player; import com.google.android.exoplayer2.Player;
@ -76,7 +77,6 @@ public interface MediaItemTag {
@NonNull @NonNull
default MediaItem asMediaItem() { default MediaItem asMediaItem() {
final MediaMetadata mediaMetadata = new MediaMetadata.Builder() final MediaMetadata mediaMetadata = new MediaMetadata.Builder()
.setMediaUri(Uri.parse(getStreamUrl()))
.setArtworkUri(Uri.parse(getThumbnailUrl())) .setArtworkUri(Uri.parse(getThumbnailUrl()))
.setArtist(getUploaderName()) .setArtist(getUploaderName())
.setDescription(getTitle()) .setDescription(getTitle())
@ -84,10 +84,15 @@ public interface MediaItemTag {
.setTitle(getTitle()) .setTitle(getTitle())
.build(); .build();
final RequestMetadata requestMetaData = new RequestMetadata.Builder()
.setMediaUri(Uri.parse(getStreamUrl()))
.build();
return MediaItem.fromUri(getStreamUrl()) return MediaItem.fromUri(getStreamUrl())
.buildUpon() .buildUpon()
.setMediaId(makeMediaId()) .setMediaId(makeMediaId())
.setMediaMetadata(mediaMetadata) .setMediaMetadata(mediaMetadata)
.setRequestMetadata(requestMetaData)
.setTag(this) .setTag(this)
.build(); .build();
} }

View File

@ -1,21 +0,0 @@
package org.schabi.newpipe.player.mediasession;
import android.support.v4.media.MediaDescriptionCompat;
public interface MediaSessionCallback {
void playPrevious();
void playNext();
void playItemAtIndex(int index);
int getCurrentPlayingIndex();
int getQueueSize();
MediaDescriptionCompat getQueueMetadata(int index);
void play();
void pause();
}

View File

@ -0,0 +1,134 @@
package org.schabi.newpipe.player.mediasession;
import static org.schabi.newpipe.MainActivity.DEBUG;
import android.content.Intent;
import android.graphics.Bitmap;
import android.support.v4.media.MediaMetadataCompat;
import android.support.v4.media.session.MediaSessionCompat;
import android.util.Log;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.media.session.MediaButtonReceiver;
import com.google.android.exoplayer2.ForwardingPlayer;
import com.google.android.exoplayer2.ext.mediasession.MediaSessionConnector;
import org.schabi.newpipe.R;
import org.schabi.newpipe.player.Player;
import org.schabi.newpipe.player.ui.PlayerUi;
import org.schabi.newpipe.player.ui.VideoPlayerUi;
import org.schabi.newpipe.util.StreamTypeUtil;
import java.util.Optional;
public class MediaSessionPlayerUi extends PlayerUi {
private static final String TAG = "MediaSessUi";
private MediaSessionCompat mediaSession;
private MediaSessionConnector sessionConnector;
public MediaSessionPlayerUi(@NonNull final Player player) {
super(player);
}
@Override
public void initPlayer() {
super.initPlayer();
destroyPlayer(); // release previously used resources
mediaSession = new MediaSessionCompat(context, TAG);
mediaSession.setActive(true);
sessionConnector = new MediaSessionConnector(mediaSession);
sessionConnector.setQueueNavigator(new PlayQueueNavigator(mediaSession, player));
sessionConnector.setPlayer(getForwardingPlayer());
sessionConnector.setMetadataDeduplicationEnabled(true);
sessionConnector.setMediaMetadataProvider(exoPlayer -> buildMediaMetadata());
}
@Override
public void destroyPlayer() {
super.destroyPlayer();
if (sessionConnector != null) {
sessionConnector.setPlayer(null);
sessionConnector.setQueueNavigator(null);
sessionConnector = null;
}
if (mediaSession != null) {
mediaSession.setActive(false);
mediaSession.release();
mediaSession = null;
}
}
@Override
public void onThumbnailLoaded(@Nullable final Bitmap bitmap) {
super.onThumbnailLoaded(bitmap);
if (sessionConnector != null) {
// the thumbnail is now loaded: invalidate the metadata to trigger a metadata update
sessionConnector.invalidateMediaSessionMetadata();
}
}
public void handleMediaButtonIntent(final Intent intent) {
MediaButtonReceiver.handleIntent(mediaSession, intent);
}
public Optional<MediaSessionCompat.Token> getSessionToken() {
return Optional.ofNullable(mediaSession).map(MediaSessionCompat::getSessionToken);
}
private ForwardingPlayer getForwardingPlayer() {
// ForwardingPlayer means that all media session actions called on this player are
// forwarded directly to the connected exoplayer, except for the overridden methods. So
// override play and pause since our player adds more functionality to them over exoplayer.
return new ForwardingPlayer(player.getExoPlayer()) {
@Override
public void play() {
player.play();
// hide the player controls even if the play command came from the media session
player.UIs().get(VideoPlayerUi.class).ifPresent(ui -> ui.hideControls(0, 0));
}
@Override
public void pause() {
player.pause();
}
};
}
private MediaMetadataCompat buildMediaMetadata() {
if (DEBUG) {
Log.d(TAG, "buildMediaMetadata called");
}
// set title and artist
final MediaMetadataCompat.Builder builder = new MediaMetadataCompat.Builder()
.putString(MediaMetadataCompat.METADATA_KEY_TITLE, player.getVideoTitle())
.putString(MediaMetadataCompat.METADATA_KEY_ARTIST, player.getUploaderName());
// set duration (-1 for livestreams or if unknown, see the METADATA_KEY_DURATION docs)
final long duration = player.getCurrentStreamInfo()
.filter(info -> !StreamTypeUtil.isLiveStream(info.getStreamType()))
.map(info -> info.getDuration() * 1000L)
.orElse(-1L);
builder.putLong(MediaMetadataCompat.METADATA_KEY_DURATION, duration);
// set album art, unless the user asked not to, or there is no thumbnail available
final boolean showThumbnail = player.getPrefs().getBoolean(
context.getString(R.string.show_thumbnail_key), true);
Optional.ofNullable(player.getThumbnail())
.filter(bitmap -> showThumbnail)
.ifPresent(bitmap -> {
builder.putBitmap(MediaMetadataCompat.METADATA_KEY_ALBUM_ART, bitmap);
builder.putBitmap(MediaMetadataCompat.METADATA_KEY_DISPLAY_ICON, bitmap);
});
return builder.build();
}
}

View File

@ -1,106 +1,152 @@
package org.schabi.newpipe.player.mediasession; package org.schabi.newpipe.player.mediasession;
import android.os.Bundle;
import android.os.ResultReceiver;
import android.support.v4.media.session.MediaSessionCompat;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.google.android.exoplayer2.Player;
import com.google.android.exoplayer2.ext.mediasession.MediaSessionConnector;
import com.google.android.exoplayer2.util.Util;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import static android.support.v4.media.session.PlaybackStateCompat.ACTION_SKIP_TO_NEXT; import static android.support.v4.media.session.PlaybackStateCompat.ACTION_SKIP_TO_NEXT;
import static android.support.v4.media.session.PlaybackStateCompat.ACTION_SKIP_TO_PREVIOUS; import static android.support.v4.media.session.PlaybackStateCompat.ACTION_SKIP_TO_PREVIOUS;
import static android.support.v4.media.session.PlaybackStateCompat.ACTION_SKIP_TO_QUEUE_ITEM; import static android.support.v4.media.session.PlaybackStateCompat.ACTION_SKIP_TO_QUEUE_ITEM;
import android.net.Uri;
import android.os.Bundle;
import android.os.ResultReceiver;
import android.support.v4.media.MediaDescriptionCompat;
import android.support.v4.media.MediaMetadataCompat;
import android.support.v4.media.session.MediaSessionCompat;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.google.android.exoplayer2.ext.mediasession.MediaSessionConnector;
import com.google.android.exoplayer2.util.Util;
import org.schabi.newpipe.player.Player;
import org.schabi.newpipe.player.playqueue.PlayQueue;
import org.schabi.newpipe.player.playqueue.PlayQueueItem;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Optional;
public class PlayQueueNavigator implements MediaSessionConnector.QueueNavigator { public class PlayQueueNavigator implements MediaSessionConnector.QueueNavigator {
public static final int DEFAULT_MAX_QUEUE_SIZE = 10; private static final int MAX_QUEUE_SIZE = 10;
private final MediaSessionCompat mediaSession; private final MediaSessionCompat mediaSession;
private final MediaSessionCallback callback; private final Player player;
private final int maxQueueSize;
private long activeQueueItemId; private long activeQueueItemId;
public PlayQueueNavigator(@NonNull final MediaSessionCompat mediaSession, public PlayQueueNavigator(@NonNull final MediaSessionCompat mediaSession,
@NonNull final MediaSessionCallback callback) { @NonNull final Player player) {
this.mediaSession = mediaSession; this.mediaSession = mediaSession;
this.callback = callback; this.player = player;
this.maxQueueSize = DEFAULT_MAX_QUEUE_SIZE;
this.activeQueueItemId = MediaSessionCompat.QueueItem.UNKNOWN_ID; this.activeQueueItemId = MediaSessionCompat.QueueItem.UNKNOWN_ID;
} }
@Override @Override
public long getSupportedQueueNavigatorActions(@Nullable final Player player) { public long getSupportedQueueNavigatorActions(
@Nullable final com.google.android.exoplayer2.Player exoPlayer) {
return ACTION_SKIP_TO_NEXT | ACTION_SKIP_TO_PREVIOUS | ACTION_SKIP_TO_QUEUE_ITEM; return ACTION_SKIP_TO_NEXT | ACTION_SKIP_TO_PREVIOUS | ACTION_SKIP_TO_QUEUE_ITEM;
} }
@Override @Override
public void onTimelineChanged(@NonNull final Player player) { public void onTimelineChanged(@NonNull final com.google.android.exoplayer2.Player exoPlayer) {
publishFloatingQueueWindow(); publishFloatingQueueWindow();
} }
@Override @Override
public void onCurrentMediaItemIndexChanged(@NonNull final Player player) { public void onCurrentMediaItemIndexChanged(
@NonNull final com.google.android.exoplayer2.Player exoPlayer) {
if (activeQueueItemId == MediaSessionCompat.QueueItem.UNKNOWN_ID if (activeQueueItemId == MediaSessionCompat.QueueItem.UNKNOWN_ID
|| player.getCurrentTimeline().getWindowCount() > maxQueueSize) { || exoPlayer.getCurrentTimeline().getWindowCount() > MAX_QUEUE_SIZE) {
publishFloatingQueueWindow(); publishFloatingQueueWindow();
} else if (!player.getCurrentTimeline().isEmpty()) { } else if (!exoPlayer.getCurrentTimeline().isEmpty()) {
activeQueueItemId = player.getCurrentMediaItemIndex(); activeQueueItemId = exoPlayer.getCurrentMediaItemIndex();
} }
} }
@Override @Override
public long getActiveQueueItemId(@Nullable final Player player) { public long getActiveQueueItemId(
return callback.getCurrentPlayingIndex(); @Nullable final com.google.android.exoplayer2.Player exoPlayer) {
return Optional.ofNullable(player.getPlayQueue()).map(PlayQueue::getIndex).orElse(-1);
} }
@Override @Override
public void onSkipToPrevious(@NonNull final Player player) { public void onSkipToPrevious(@NonNull final com.google.android.exoplayer2.Player exoPlayer) {
callback.playPrevious(); player.playPrevious();
} }
@Override @Override
public void onSkipToQueueItem(@NonNull final Player player, final long id) { public void onSkipToQueueItem(@NonNull final com.google.android.exoplayer2.Player exoPlayer,
callback.playItemAtIndex((int) id); final long id) {
if (player.getPlayQueue() != null) {
player.selectQueueItem(player.getPlayQueue().getItem((int) id));
}
} }
@Override @Override
public void onSkipToNext(@NonNull final Player player) { public void onSkipToNext(@NonNull final com.google.android.exoplayer2.Player exoPlayer) {
callback.playNext(); player.playNext();
} }
private void publishFloatingQueueWindow() { private void publishFloatingQueueWindow() {
if (callback.getQueueSize() == 0) { final int windowCount = Optional.ofNullable(player.getPlayQueue())
.map(PlayQueue::size)
.orElse(0);
if (windowCount == 0) {
mediaSession.setQueue(Collections.emptyList()); mediaSession.setQueue(Collections.emptyList());
activeQueueItemId = MediaSessionCompat.QueueItem.UNKNOWN_ID; activeQueueItemId = MediaSessionCompat.QueueItem.UNKNOWN_ID;
return; return;
} }
// Yes this is almost a copypasta, got a problem with that? =\ // Yes this is almost a copypasta, got a problem with that? =\
final int windowCount = callback.getQueueSize(); final int currentWindowIndex = player.getPlayQueue().getIndex();
final int currentWindowIndex = callback.getCurrentPlayingIndex(); final int queueSize = Math.min(MAX_QUEUE_SIZE, windowCount);
final int queueSize = Math.min(maxQueueSize, windowCount);
final int startIndex = Util.constrainValue(currentWindowIndex - ((queueSize - 1) / 2), 0, final int startIndex = Util.constrainValue(currentWindowIndex - ((queueSize - 1) / 2), 0,
windowCount - queueSize); windowCount - queueSize);
final List<MediaSessionCompat.QueueItem> queue = new ArrayList<>(); final List<MediaSessionCompat.QueueItem> queue = new ArrayList<>();
for (int i = startIndex; i < startIndex + queueSize; i++) { for (int i = startIndex; i < startIndex + queueSize; i++) {
queue.add(new MediaSessionCompat.QueueItem(callback.getQueueMetadata(i), i)); queue.add(new MediaSessionCompat.QueueItem(getQueueMetadata(i), i));
} }
mediaSession.setQueue(queue); mediaSession.setQueue(queue);
activeQueueItemId = currentWindowIndex; activeQueueItemId = currentWindowIndex;
} }
public MediaDescriptionCompat getQueueMetadata(final int index) {
if (player.getPlayQueue() == null) {
return null;
}
final PlayQueueItem item = player.getPlayQueue().getItem(index);
if (item == null) {
return null;
}
final MediaDescriptionCompat.Builder descBuilder = new MediaDescriptionCompat.Builder()
.setMediaId(String.valueOf(index))
.setTitle(item.getTitle())
.setSubtitle(item.getUploader());
// set additional metadata for A2DP/AVRCP (Audio/Video Bluetooth profiles)
final Bundle additionalMetadata = new Bundle();
additionalMetadata.putString(MediaMetadataCompat.METADATA_KEY_TITLE, item.getTitle());
additionalMetadata.putString(MediaMetadataCompat.METADATA_KEY_ARTIST, item.getUploader());
additionalMetadata
.putLong(MediaMetadataCompat.METADATA_KEY_DURATION, item.getDuration() * 1000);
additionalMetadata.putLong(MediaMetadataCompat.METADATA_KEY_TRACK_NUMBER, index + 1L);
additionalMetadata
.putLong(MediaMetadataCompat.METADATA_KEY_NUM_TRACKS, player.getPlayQueue().size());
descBuilder.setExtras(additionalMetadata);
final Uri thumbnailUri = Uri.parse(item.getThumbnailUrl());
if (thumbnailUri != null) {
descBuilder.setIconUri(thumbnailUri);
}
return descBuilder.build();
}
@Override @Override
public boolean onCommand(@NonNull final Player player, public boolean onCommand(@NonNull final com.google.android.exoplayer2.Player exoPlayer,
@NonNull final String command, @NonNull final String command,
@Nullable final Bundle extras, @Nullable final Bundle extras,
@Nullable final ResultReceiver cb) { @Nullable final ResultReceiver cb) {

View File

@ -16,7 +16,7 @@ import org.schabi.newpipe.player.mediaitem.ExceptionTag;
import org.schabi.newpipe.player.playqueue.PlayQueueItem; import org.schabi.newpipe.player.playqueue.PlayQueueItem;
import java.io.IOException; import java.io.IOException;
import java.util.Collections; import java.util.List;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
@ -56,9 +56,7 @@ public class FailedMediaSource extends BaseMediaSource implements ManagedMediaSo
this.playQueueItem = playQueueItem; this.playQueueItem = playQueueItem;
this.error = error; this.error = error;
this.retryTimestamp = retryTimestamp; this.retryTimestamp = retryTimestamp;
this.mediaItem = ExceptionTag this.mediaItem = ExceptionTag.of(playQueueItem, List.of(error)).withExtras(this)
.of(playQueueItem, Collections.singletonList(error))
.withExtras(this)
.asMediaItem(); .asMediaItem();
} }

View File

@ -1,4 +1,4 @@
package org.schabi.newpipe.player; package org.schabi.newpipe.player.notification;
import android.content.Context; import android.content.Context;
import android.content.SharedPreferences; import android.content.SharedPreferences;
@ -7,20 +7,48 @@ import androidx.annotation.DrawableRes;
import androidx.annotation.IntDef; import androidx.annotation.IntDef;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import org.schabi.newpipe.App;
import org.schabi.newpipe.R; import org.schabi.newpipe.R;
import org.schabi.newpipe.util.Localization; import org.schabi.newpipe.util.Localization;
import java.lang.annotation.Retention; import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy; import java.lang.annotation.RetentionPolicy;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays;
import java.util.List; import java.util.List;
import java.util.SortedSet; import java.util.SortedSet;
import java.util.TreeSet; import java.util.TreeSet;
public final class NotificationConstants { public final class NotificationConstants {
private NotificationConstants() { } private NotificationConstants() {
}
/*//////////////////////////////////////////////////////////////////////////
// Intent actions
//////////////////////////////////////////////////////////////////////////*/
private static final String BASE_ACTION =
App.PACKAGE_NAME + ".player.MainPlayer.";
public static final String ACTION_CLOSE =
BASE_ACTION + "CLOSE";
public static final String ACTION_PLAY_PAUSE =
BASE_ACTION + ".player.MainPlayer.PLAY_PAUSE";
public static final String ACTION_REPEAT =
BASE_ACTION + ".player.MainPlayer.REPEAT";
public static final String ACTION_PLAY_NEXT =
BASE_ACTION + ".player.MainPlayer.ACTION_PLAY_NEXT";
public static final String ACTION_PLAY_PREVIOUS =
BASE_ACTION + ".player.MainPlayer.ACTION_PLAY_PREVIOUS";
public static final String ACTION_FAST_REWIND =
BASE_ACTION + ".player.MainPlayer.ACTION_FAST_REWIND";
public static final String ACTION_FAST_FORWARD =
BASE_ACTION + ".player.MainPlayer.ACTION_FAST_FORWARD";
public static final String ACTION_SHUFFLE =
BASE_ACTION + ".player.MainPlayer.ACTION_SHUFFLE";
public static final String ACTION_RECREATE_NOTIFICATION =
BASE_ACTION + ".player.MainPlayer.ACTION_RECREATE_NOTIFICATION";
public static final int NOTHING = 0; public static final int NOTHING = 0;
@ -86,7 +114,7 @@ public final class NotificationConstants {
}; };
public static final Integer[] SLOT_COMPACT_DEFAULTS = {0, 1, 2}; public static final List<Integer> SLOT_COMPACT_DEFAULTS = List.of(0, 1, 2);
public static final int[] SLOT_COMPACT_PREF_KEYS = { public static final int[] SLOT_COMPACT_PREF_KEYS = {
R.string.notification_slot_compact_0_key, R.string.notification_slot_compact_0_key,
@ -152,7 +180,7 @@ public final class NotificationConstants {
if (compactSlot == Integer.MAX_VALUE) { if (compactSlot == Integer.MAX_VALUE) {
// settings not yet populated, return default values // settings not yet populated, return default values
return new ArrayList<>(Arrays.asList(SLOT_COMPACT_DEFAULTS)); return new ArrayList<>(SLOT_COMPACT_DEFAULTS);
} }
// a negative value (-1) is set when the user does not want a particular compact slot // a negative value (-1) is set when the user does not want a particular compact slot

View File

@ -0,0 +1,125 @@
package org.schabi.newpipe.player.notification;
import static org.schabi.newpipe.player.helper.PlayerHelper.MinimizeMode.MINIMIZE_ON_EXIT_MODE_NONE;
import static org.schabi.newpipe.player.notification.NotificationConstants.ACTION_RECREATE_NOTIFICATION;
import android.content.Intent;
import android.graphics.Bitmap;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.google.android.exoplayer2.Player.RepeatMode;
import org.schabi.newpipe.extractor.stream.StreamInfo;
import org.schabi.newpipe.player.Player;
import org.schabi.newpipe.player.helper.PlayerHelper;
import org.schabi.newpipe.player.ui.PlayerUi;
public final class NotificationPlayerUi extends PlayerUi {
private boolean foregroundNotificationAlreadyCreated = false;
private final NotificationUtil notificationUtil;
public NotificationPlayerUi(@NonNull final Player player) {
super(player);
notificationUtil = new NotificationUtil(player);
}
@Override
public void initPlayer() {
super.initPlayer();
if (!foregroundNotificationAlreadyCreated) {
notificationUtil.createNotificationAndStartForeground();
foregroundNotificationAlreadyCreated = true;
}
}
@Override
public void destroy() {
super.destroy();
notificationUtil.cancelNotificationAndStopForeground();
}
@Override
public void onThumbnailLoaded(@Nullable final Bitmap bitmap) {
super.onThumbnailLoaded(bitmap);
notificationUtil.updateThumbnail();
}
@Override
public void onBlocked() {
super.onBlocked();
notificationUtil.createNotificationIfNeededAndUpdate(false);
}
@Override
public void onPlaying() {
super.onPlaying();
notificationUtil.createNotificationIfNeededAndUpdate(false);
}
@Override
public void onBuffering() {
super.onBuffering();
if (notificationUtil.shouldUpdateBufferingSlot()) {
notificationUtil.createNotificationIfNeededAndUpdate(false);
}
}
@Override
public void onPaused() {
super.onPaused();
// Remove running notification when user does not want minimization to background or popup
if (PlayerHelper.getMinimizeOnExitAction(context) == MINIMIZE_ON_EXIT_MODE_NONE
&& player.videoPlayerSelected()) {
notificationUtil.cancelNotificationAndStopForeground();
} else {
notificationUtil.createNotificationIfNeededAndUpdate(false);
}
}
@Override
public void onPausedSeek() {
super.onPausedSeek();
notificationUtil.createNotificationIfNeededAndUpdate(false);
}
@Override
public void onCompleted() {
super.onCompleted();
notificationUtil.createNotificationIfNeededAndUpdate(false);
}
@Override
public void onRepeatModeChanged(@RepeatMode final int repeatMode) {
super.onRepeatModeChanged(repeatMode);
notificationUtil.createNotificationIfNeededAndUpdate(false);
}
@Override
public void onShuffleModeEnabledChanged(final boolean shuffleModeEnabled) {
super.onShuffleModeEnabledChanged(shuffleModeEnabled);
notificationUtil.createNotificationIfNeededAndUpdate(false);
}
@Override
public void onBroadcastReceived(final Intent intent) {
super.onBroadcastReceived(intent);
if (ACTION_RECREATE_NOTIFICATION.equals(intent.getAction())) {
notificationUtil.createNotificationIfNeededAndUpdate(true);
}
}
@Override
public void onMetadataChanged(@NonNull final StreamInfo info) {
super.onMetadataChanged(info);
notificationUtil.createNotificationIfNeededAndUpdate(true);
}
@Override
public void onPlayQueueEdited() {
super.onPlayQueueEdited();
notificationUtil.createNotificationIfNeededAndUpdate(false);
}
}

View File

@ -1,16 +1,15 @@
package org.schabi.newpipe.player; package org.schabi.newpipe.player.notification;
import android.annotation.SuppressLint; import android.annotation.SuppressLint;
import android.app.PendingIntent; import android.app.PendingIntent;
import android.app.Service;
import android.content.Intent; import android.content.Intent;
import android.content.pm.ServiceInfo; import android.content.pm.ServiceInfo;
import android.graphics.Bitmap; import android.graphics.Bitmap;
import android.graphics.Matrix;
import android.os.Build; import android.os.Build;
import android.util.Log; import android.util.Log;
import androidx.annotation.DrawableRes; import androidx.annotation.DrawableRes;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.annotation.StringRes; import androidx.annotation.StringRes;
import androidx.core.app.NotificationCompat; import androidx.core.app.NotificationCompat;
@ -20,48 +19,45 @@ import androidx.core.content.ContextCompat;
import org.schabi.newpipe.MainActivity; import org.schabi.newpipe.MainActivity;
import org.schabi.newpipe.R; import org.schabi.newpipe.R;
import org.schabi.newpipe.player.Player;
import org.schabi.newpipe.player.mediasession.MediaSessionPlayerUi;
import org.schabi.newpipe.util.NavigationHelper; import org.schabi.newpipe.util.NavigationHelper;
import java.util.List; import java.util.List;
import java.util.Objects;
import java.util.Optional;
import static android.app.PendingIntent.FLAG_UPDATE_CURRENT; import static android.app.PendingIntent.FLAG_UPDATE_CURRENT;
import static androidx.media.app.NotificationCompat.MediaStyle;
import static com.google.android.exoplayer2.Player.REPEAT_MODE_ALL; import static com.google.android.exoplayer2.Player.REPEAT_MODE_ALL;
import static com.google.android.exoplayer2.Player.REPEAT_MODE_ONE; import static com.google.android.exoplayer2.Player.REPEAT_MODE_ONE;
import static org.schabi.newpipe.player.MainPlayer.ACTION_CLOSE; import static org.schabi.newpipe.player.notification.NotificationConstants.ACTION_CLOSE;
import static org.schabi.newpipe.player.MainPlayer.ACTION_FAST_FORWARD; import static org.schabi.newpipe.player.notification.NotificationConstants.ACTION_FAST_FORWARD;
import static org.schabi.newpipe.player.MainPlayer.ACTION_FAST_REWIND; import static org.schabi.newpipe.player.notification.NotificationConstants.ACTION_FAST_REWIND;
import static org.schabi.newpipe.player.MainPlayer.ACTION_PLAY_NEXT; import static org.schabi.newpipe.player.notification.NotificationConstants.ACTION_PLAY_NEXT;
import static org.schabi.newpipe.player.MainPlayer.ACTION_PLAY_PAUSE; import static org.schabi.newpipe.player.notification.NotificationConstants.ACTION_PLAY_PAUSE;
import static org.schabi.newpipe.player.MainPlayer.ACTION_PLAY_PREVIOUS; import static org.schabi.newpipe.player.notification.NotificationConstants.ACTION_PLAY_PREVIOUS;
import static org.schabi.newpipe.player.MainPlayer.ACTION_REPEAT; import static org.schabi.newpipe.player.notification.NotificationConstants.ACTION_REPEAT;
import static org.schabi.newpipe.player.MainPlayer.ACTION_SHUFFLE; import static org.schabi.newpipe.player.notification.NotificationConstants.ACTION_SHUFFLE;
/** /**
* This is a utility class for player notifications. * This is a utility class for player notifications.
*
* @author cool-student
*/ */
public final class NotificationUtil { public final class NotificationUtil {
private static final String TAG = NotificationUtil.class.getSimpleName(); private static final String TAG = NotificationUtil.class.getSimpleName();
private static final boolean DEBUG = Player.DEBUG; private static final boolean DEBUG = Player.DEBUG;
private static final int NOTIFICATION_ID = 123789; private static final int NOTIFICATION_ID = 123789;
@Nullable private static NotificationUtil instance = null;
@NotificationConstants.Action @NotificationConstants.Action
private final int[] notificationSlots = NotificationConstants.SLOT_DEFAULTS.clone(); private final int[] notificationSlots = NotificationConstants.SLOT_DEFAULTS.clone();
private NotificationManagerCompat notificationManager; private NotificationManagerCompat notificationManager;
private NotificationCompat.Builder notificationBuilder; private NotificationCompat.Builder notificationBuilder;
private NotificationUtil() { private final Player player;
}
public static NotificationUtil getInstance() { public NotificationUtil(final Player player) {
if (instance == null) { this.player = player;
instance = new NotificationUtil();
}
return instance;
} }
@ -72,20 +68,31 @@ public final class NotificationUtil {
/** /**
* Creates the notification if it does not exist already and recreates it if forceRecreate is * Creates the notification if it does not exist already and recreates it if forceRecreate is
* true. Updates the notification with the data in the player. * true. Updates the notification with the data in the player.
* @param player the player currently open, to take data from
* @param forceRecreate whether to force the recreation of the notification even if it already * @param forceRecreate whether to force the recreation of the notification even if it already
* exists * exists
*/ */
synchronized void createNotificationIfNeededAndUpdate(final Player player, public synchronized void createNotificationIfNeededAndUpdate(final boolean forceRecreate) {
final boolean forceRecreate) {
if (forceRecreate || notificationBuilder == null) { if (forceRecreate || notificationBuilder == null) {
notificationBuilder = createNotification(player); notificationBuilder = createNotification();
} }
updateNotification(player); updateNotification();
notificationManager.notify(NOTIFICATION_ID, notificationBuilder.build()); notificationManager.notify(NOTIFICATION_ID, notificationBuilder.build());
} }
private synchronized NotificationCompat.Builder createNotification(final Player player) { public synchronized void updateThumbnail() {
if (notificationBuilder != null) {
if (DEBUG) {
Log.d(TAG, "updateThumbnail() called with thumbnail = [" + Integer.toHexString(
Optional.ofNullable(player.getThumbnail()).map(Objects::hashCode).orElse(0))
+ "], title = [" + player.getVideoTitle() + "]");
}
setLargeIcon(notificationBuilder);
notificationManager.notify(NOTIFICATION_ID, notificationBuilder.build());
}
}
private synchronized NotificationCompat.Builder createNotification() {
if (DEBUG) { if (DEBUG) {
Log.d(TAG, "createNotification()"); Log.d(TAG, "createNotification()");
} }
@ -94,7 +101,7 @@ public final class NotificationUtil {
new NotificationCompat.Builder(player.getContext(), new NotificationCompat.Builder(player.getContext(),
player.getContext().getString(R.string.notification_channel_id)); player.getContext().getString(R.string.notification_channel_id));
initializeNotificationSlots(player); initializeNotificationSlots();
// count the number of real slots, to make sure compact slots indices are not out of bound // count the number of real slots, to make sure compact slots indices are not out of bound
int nonNothingSlotCount = 5; int nonNothingSlotCount = 5;
@ -108,14 +115,15 @@ public final class NotificationUtil {
// build the compact slot indices array (need code to convert from Integer... because Java) // build the compact slot indices array (need code to convert from Integer... because Java)
final List<Integer> compactSlotList = NotificationConstants.getCompactSlotsFromPreferences( final List<Integer> compactSlotList = NotificationConstants.getCompactSlotsFromPreferences(
player.getContext(), player.getPrefs(), nonNothingSlotCount); player.getContext(), player.getPrefs(), nonNothingSlotCount);
final int[] compactSlots = new int[compactSlotList.size()]; final int[] compactSlots = compactSlotList.stream().mapToInt(Integer::intValue).toArray();
for (int i = 0; i < compactSlotList.size(); i++) {
compactSlots[i] = compactSlotList.get(i);
}
builder.setStyle(new androidx.media.app.NotificationCompat.MediaStyle() final MediaStyle mediaStyle = new MediaStyle().setShowActionsInCompactView(compactSlots);
.setMediaSession(player.getMediaSessionManager().getSessionToken()) player.UIs()
.setShowActionsInCompactView(compactSlots)) .get(MediaSessionPlayerUi.class)
.flatMap(MediaSessionPlayerUi::getSessionToken)
.ifPresent(mediaStyle::setMediaSession);
builder.setStyle(mediaStyle)
.setPriority(NotificationCompat.PRIORITY_HIGH) .setPriority(NotificationCompat.PRIORITY_HIGH)
.setVisibility(NotificationCompat.VISIBILITY_PUBLIC) .setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
.setCategory(NotificationCompat.CATEGORY_TRANSPORT) .setCategory(NotificationCompat.CATEGORY_TRANSPORT)
@ -128,35 +136,33 @@ public final class NotificationUtil {
.setDeleteIntent(PendingIntent.getBroadcast(player.getContext(), NOTIFICATION_ID, .setDeleteIntent(PendingIntent.getBroadcast(player.getContext(), NOTIFICATION_ID,
new Intent(ACTION_CLOSE), FLAG_UPDATE_CURRENT)); new Intent(ACTION_CLOSE), FLAG_UPDATE_CURRENT));
// set the initial value for the video thumbnail, updatable with updateNotificationThumbnail
setLargeIcon(builder);
return builder; return builder;
} }
/** /**
* Updates the notification builder and the button icons depending on the playback state. * Updates the notification builder and the button icons depending on the playback state.
* @param player the player currently open, to take data from
*/ */
private synchronized void updateNotification(final Player player) { private synchronized void updateNotification() {
if (DEBUG) { if (DEBUG) {
Log.d(TAG, "updateNotification()"); Log.d(TAG, "updateNotification()");
} }
// also update content intent, in case the user switched players // also update content intent, in case the user switched players
notificationBuilder.setContentIntent(PendingIntent.getActivity(player.getContext(), notificationBuilder.setContentIntent(PendingIntent.getActivity(player.getContext(),
NOTIFICATION_ID, getIntentForNotification(player), FLAG_UPDATE_CURRENT)); NOTIFICATION_ID, getIntentForNotification(), FLAG_UPDATE_CURRENT));
notificationBuilder.setContentTitle(player.getVideoTitle()); notificationBuilder.setContentTitle(player.getVideoTitle());
notificationBuilder.setContentText(player.getUploaderName()); notificationBuilder.setContentText(player.getUploaderName());
notificationBuilder.setTicker(player.getVideoTitle()); notificationBuilder.setTicker(player.getVideoTitle());
updateActions(notificationBuilder, player);
final boolean showThumbnail = player.getPrefs().getBoolean( updateActions(notificationBuilder);
player.getContext().getString(R.string.show_thumbnail_key), true);
if (showThumbnail) {
setLargeIcon(notificationBuilder, player);
}
} }
@SuppressLint("RestrictedApi") @SuppressLint("RestrictedApi")
boolean shouldUpdateBufferingSlot() { public boolean shouldUpdateBufferingSlot() {
if (notificationBuilder == null) { if (notificationBuilder == null) {
// if there is no notification active, there is no point in updating it // if there is no notification active, there is no point in updating it
return false; return false;
@ -174,22 +180,22 @@ public final class NotificationUtil {
} }
void createNotificationAndStartForeground(final Player player, final Service service) { public void createNotificationAndStartForeground() {
if (notificationBuilder == null) { if (notificationBuilder == null) {
notificationBuilder = createNotification(player); notificationBuilder = createNotification();
} }
updateNotification(player); updateNotification();
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
service.startForeground(NOTIFICATION_ID, notificationBuilder.build(), player.getService().startForeground(NOTIFICATION_ID, notificationBuilder.build(),
ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PLAYBACK); ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PLAYBACK);
} else { } else {
service.startForeground(NOTIFICATION_ID, notificationBuilder.build()); player.getService().startForeground(NOTIFICATION_ID, notificationBuilder.build());
} }
} }
void cancelNotificationAndStopForeground(final Service service) { public void cancelNotificationAndStopForeground() {
ServiceCompat.stopForeground(service, ServiceCompat.STOP_FOREGROUND_REMOVE); ServiceCompat.stopForeground(player.getService(), ServiceCompat.STOP_FOREGROUND_REMOVE);
if (notificationManager != null) { if (notificationManager != null) {
notificationManager.cancel(NOTIFICATION_ID); notificationManager.cancel(NOTIFICATION_ID);
@ -203,7 +209,7 @@ public final class NotificationUtil {
// ACTIONS // ACTIONS
///////////////////////////////////////////////////// /////////////////////////////////////////////////////
private void initializeNotificationSlots(final Player player) { private void initializeNotificationSlots() {
for (int i = 0; i < 5; ++i) { for (int i = 0; i < 5; ++i) {
notificationSlots[i] = player.getPrefs().getInt( notificationSlots[i] = player.getPrefs().getInt(
player.getContext().getString(NotificationConstants.SLOT_PREF_KEYS[i]), player.getContext().getString(NotificationConstants.SLOT_PREF_KEYS[i]),
@ -212,17 +218,16 @@ public final class NotificationUtil {
} }
@SuppressLint("RestrictedApi") @SuppressLint("RestrictedApi")
private void updateActions(final NotificationCompat.Builder builder, final Player player) { private void updateActions(final NotificationCompat.Builder builder) {
builder.mActions.clear(); builder.mActions.clear();
for (int i = 0; i < 5; ++i) { for (int i = 0; i < 5; ++i) {
addAction(builder, player, notificationSlots[i]); addAction(builder, notificationSlots[i]);
} }
} }
private void addAction(final NotificationCompat.Builder builder, private void addAction(final NotificationCompat.Builder builder,
final Player player,
@NotificationConstants.Action final int slot) { @NotificationConstants.Action final int slot) {
final NotificationCompat.Action action = getAction(player, slot); final NotificationCompat.Action action = getAction(slot);
if (action != null) { if (action != null) {
builder.addAction(action); builder.addAction(action);
} }
@ -230,41 +235,40 @@ public final class NotificationUtil {
@Nullable @Nullable
private NotificationCompat.Action getAction( private NotificationCompat.Action getAction(
final Player player,
@NotificationConstants.Action final int selectedAction) { @NotificationConstants.Action final int selectedAction) {
final int baseActionIcon = NotificationConstants.ACTION_ICONS[selectedAction]; final int baseActionIcon = NotificationConstants.ACTION_ICONS[selectedAction];
switch (selectedAction) { switch (selectedAction) {
case NotificationConstants.PREVIOUS: case NotificationConstants.PREVIOUS:
return getAction(player, baseActionIcon, return getAction(baseActionIcon,
R.string.exo_controls_previous_description, ACTION_PLAY_PREVIOUS); R.string.exo_controls_previous_description, ACTION_PLAY_PREVIOUS);
case NotificationConstants.NEXT: case NotificationConstants.NEXT:
return getAction(player, baseActionIcon, return getAction(baseActionIcon,
R.string.exo_controls_next_description, ACTION_PLAY_NEXT); R.string.exo_controls_next_description, ACTION_PLAY_NEXT);
case NotificationConstants.REWIND: case NotificationConstants.REWIND:
return getAction(player, baseActionIcon, return getAction(baseActionIcon,
R.string.exo_controls_rewind_description, ACTION_FAST_REWIND); R.string.exo_controls_rewind_description, ACTION_FAST_REWIND);
case NotificationConstants.FORWARD: case NotificationConstants.FORWARD:
return getAction(player, baseActionIcon, return getAction(baseActionIcon,
R.string.exo_controls_fastforward_description, ACTION_FAST_FORWARD); R.string.exo_controls_fastforward_description, ACTION_FAST_FORWARD);
case NotificationConstants.SMART_REWIND_PREVIOUS: case NotificationConstants.SMART_REWIND_PREVIOUS:
if (player.getPlayQueue() != null && player.getPlayQueue().size() > 1) { if (player.getPlayQueue() != null && player.getPlayQueue().size() > 1) {
return getAction(player, R.drawable.exo_notification_previous, return getAction(R.drawable.exo_notification_previous,
R.string.exo_controls_previous_description, ACTION_PLAY_PREVIOUS); R.string.exo_controls_previous_description, ACTION_PLAY_PREVIOUS);
} else { } else {
return getAction(player, R.drawable.exo_controls_rewind, return getAction(R.drawable.exo_controls_rewind,
R.string.exo_controls_rewind_description, ACTION_FAST_REWIND); R.string.exo_controls_rewind_description, ACTION_FAST_REWIND);
} }
case NotificationConstants.SMART_FORWARD_NEXT: case NotificationConstants.SMART_FORWARD_NEXT:
if (player.getPlayQueue() != null && player.getPlayQueue().size() > 1) { if (player.getPlayQueue() != null && player.getPlayQueue().size() > 1) {
return getAction(player, R.drawable.exo_notification_next, return getAction(R.drawable.exo_notification_next,
R.string.exo_controls_next_description, ACTION_PLAY_NEXT); R.string.exo_controls_next_description, ACTION_PLAY_NEXT);
} else { } else {
return getAction(player, R.drawable.exo_controls_fastforward, return getAction(R.drawable.exo_controls_fastforward,
R.string.exo_controls_fastforward_description, ACTION_FAST_FORWARD); R.string.exo_controls_fastforward_description, ACTION_FAST_FORWARD);
} }
@ -278,44 +282,45 @@ public final class NotificationUtil {
null); null);
} }
// fallthrough
case NotificationConstants.PLAY_PAUSE: case NotificationConstants.PLAY_PAUSE:
if (player.getCurrentState() == Player.STATE_COMPLETED) { if (player.getCurrentState() == Player.STATE_COMPLETED) {
return getAction(player, R.drawable.ic_replay, return getAction(R.drawable.ic_replay,
R.string.exo_controls_pause_description, ACTION_PLAY_PAUSE); R.string.exo_controls_pause_description, ACTION_PLAY_PAUSE);
} else if (player.isPlaying() } else if (player.isPlaying()
|| player.getCurrentState() == Player.STATE_PREFLIGHT || player.getCurrentState() == Player.STATE_PREFLIGHT
|| player.getCurrentState() == Player.STATE_BLOCKED || player.getCurrentState() == Player.STATE_BLOCKED
|| player.getCurrentState() == Player.STATE_BUFFERING) { || player.getCurrentState() == Player.STATE_BUFFERING) {
return getAction(player, R.drawable.exo_notification_pause, return getAction(R.drawable.exo_notification_pause,
R.string.exo_controls_pause_description, ACTION_PLAY_PAUSE); R.string.exo_controls_pause_description, ACTION_PLAY_PAUSE);
} else { } else {
return getAction(player, R.drawable.exo_notification_play, return getAction(R.drawable.exo_notification_play,
R.string.exo_controls_play_description, ACTION_PLAY_PAUSE); R.string.exo_controls_play_description, ACTION_PLAY_PAUSE);
} }
case NotificationConstants.REPEAT: case NotificationConstants.REPEAT:
if (player.getRepeatMode() == REPEAT_MODE_ALL) { if (player.getRepeatMode() == REPEAT_MODE_ALL) {
return getAction(player, R.drawable.exo_media_action_repeat_all, return getAction(R.drawable.exo_media_action_repeat_all,
R.string.exo_controls_repeat_all_description, ACTION_REPEAT); R.string.exo_controls_repeat_all_description, ACTION_REPEAT);
} else if (player.getRepeatMode() == REPEAT_MODE_ONE) { } else if (player.getRepeatMode() == REPEAT_MODE_ONE) {
return getAction(player, R.drawable.exo_media_action_repeat_one, return getAction(R.drawable.exo_media_action_repeat_one,
R.string.exo_controls_repeat_one_description, ACTION_REPEAT); R.string.exo_controls_repeat_one_description, ACTION_REPEAT);
} else /* player.getRepeatMode() == REPEAT_MODE_OFF */ { } else /* player.getRepeatMode() == REPEAT_MODE_OFF */ {
return getAction(player, R.drawable.exo_media_action_repeat_off, return getAction(R.drawable.exo_media_action_repeat_off,
R.string.exo_controls_repeat_off_description, ACTION_REPEAT); R.string.exo_controls_repeat_off_description, ACTION_REPEAT);
} }
case NotificationConstants.SHUFFLE: case NotificationConstants.SHUFFLE:
if (player.getPlayQueue() != null && player.getPlayQueue().isShuffled()) { if (player.getPlayQueue() != null && player.getPlayQueue().isShuffled()) {
return getAction(player, R.drawable.exo_controls_shuffle_on, return getAction(R.drawable.exo_controls_shuffle_on,
R.string.exo_controls_shuffle_on_description, ACTION_SHUFFLE); R.string.exo_controls_shuffle_on_description, ACTION_SHUFFLE);
} else { } else {
return getAction(player, R.drawable.exo_controls_shuffle_off, return getAction(R.drawable.exo_controls_shuffle_off,
R.string.exo_controls_shuffle_off_description, ACTION_SHUFFLE); R.string.exo_controls_shuffle_off_description, ACTION_SHUFFLE);
} }
case NotificationConstants.CLOSE: case NotificationConstants.CLOSE:
return getAction(player, R.drawable.ic_close, return getAction(R.drawable.ic_close,
R.string.close, ACTION_CLOSE); R.string.close, ACTION_CLOSE);
case NotificationConstants.NOTHING: case NotificationConstants.NOTHING:
@ -325,8 +330,7 @@ public final class NotificationUtil {
} }
} }
private NotificationCompat.Action getAction(final Player player, private NotificationCompat.Action getAction(@DrawableRes final int drawable,
@DrawableRes final int drawable,
@StringRes final int title, @StringRes final int title,
final String intentAction) { final String intentAction) {
return new NotificationCompat.Action(drawable, player.getContext().getString(title), return new NotificationCompat.Action(drawable, player.getContext().getString(title),
@ -334,7 +338,7 @@ public final class NotificationUtil {
new Intent(intentAction), FLAG_UPDATE_CURRENT)); new Intent(intentAction), FLAG_UPDATE_CURRENT));
} }
private Intent getIntentForNotification(final Player player) { private Intent getIntentForNotification() {
if (player.audioPlayerSelected() || player.popupPlayerSelected()) { if (player.audioPlayerSelected() || player.popupPlayerSelected()) {
// Means we play in popup or audio only. Let's show the play queue // Means we play in popup or audio only. Let's show the play queue
return NavigationHelper.getPlayQueueActivityIntent(player.getContext()); return NavigationHelper.getPlayQueueActivityIntent(player.getContext());
@ -354,28 +358,34 @@ public final class NotificationUtil {
// BITMAP // BITMAP
///////////////////////////////////////////////////// /////////////////////////////////////////////////////
private void setLargeIcon(final NotificationCompat.Builder builder, final Player player) { private void setLargeIcon(final NotificationCompat.Builder builder) {
final boolean showThumbnail = player.getPrefs().getBoolean(
player.getContext().getString(R.string.show_thumbnail_key), true);
final Bitmap thumbnail = player.getThumbnail();
if (thumbnail == null || !showThumbnail) {
// since the builder is reused, make sure the thumbnail is unset if there is not one
builder.setLargeIcon(null);
return;
}
final boolean scaleImageToSquareAspectRatio = player.getPrefs().getBoolean( final boolean scaleImageToSquareAspectRatio = player.getPrefs().getBoolean(
player.getContext().getString(R.string.scale_to_square_image_in_notifications_key), player.getContext().getString(R.string.scale_to_square_image_in_notifications_key),
false); false);
if (scaleImageToSquareAspectRatio) { if (scaleImageToSquareAspectRatio) {
builder.setLargeIcon(getBitmapWithSquareAspectRatio(player.getThumbnail())); builder.setLargeIcon(getBitmapWithSquareAspectRatio(thumbnail));
} else { } else {
builder.setLargeIcon(player.getThumbnail()); builder.setLargeIcon(thumbnail);
} }
} }
private Bitmap getBitmapWithSquareAspectRatio(final Bitmap bitmap) { private Bitmap getBitmapWithSquareAspectRatio(@NonNull final Bitmap bitmap) {
return getResizedBitmap(bitmap, bitmap.getWidth(), bitmap.getWidth()); // Find the smaller dimension and then take a center portion of the image that
} // has that size.
final int w = bitmap.getWidth();
private Bitmap getResizedBitmap(final Bitmap bitmap, final int newWidth, final int newHeight) { final int h = bitmap.getHeight();
final int width = bitmap.getWidth(); final int dstSize = Math.min(w, h);
final int height = bitmap.getHeight(); final int x = (w - dstSize) / 2;
final float scaleWidth = ((float) newWidth) / width; final int y = (h - dstSize) / 2;
final float scaleHeight = ((float) newHeight) / height; return Bitmap.createBitmap(bitmap, x, y, dstSize, dstSize);
final Matrix matrix = new Matrix();
matrix.postScale(scaleWidth, scaleHeight);
return Bitmap.createBitmap(bitmap, 0, 0, width, height, matrix, false);
} }
} }

View File

@ -1,99 +0,0 @@
package org.schabi.newpipe.player.playback;
import android.net.Uri;
import android.os.Bundle;
import android.support.v4.media.MediaDescriptionCompat;
import android.support.v4.media.MediaMetadataCompat;
import org.schabi.newpipe.player.Player;
import org.schabi.newpipe.player.mediasession.MediaSessionCallback;
import org.schabi.newpipe.player.playqueue.PlayQueueItem;
public class PlayerMediaSession implements MediaSessionCallback {
private final Player player;
public PlayerMediaSession(final Player player) {
this.player = player;
}
@Override
public void playPrevious() {
player.playPrevious();
}
@Override
public void playNext() {
player.playNext();
}
@Override
public void playItemAtIndex(final int index) {
if (player.getPlayQueue() == null) {
return;
}
player.selectQueueItem(player.getPlayQueue().getItem(index));
}
@Override
public int getCurrentPlayingIndex() {
if (player.getPlayQueue() == null) {
return -1;
}
return player.getPlayQueue().getIndex();
}
@Override
public int getQueueSize() {
if (player.getPlayQueue() == null) {
return -1;
}
return player.getPlayQueue().size();
}
@Override
public MediaDescriptionCompat getQueueMetadata(final int index) {
if (player.getPlayQueue() == null) {
return null;
}
final PlayQueueItem item = player.getPlayQueue().getItem(index);
if (item == null) {
return null;
}
final MediaDescriptionCompat.Builder descriptionBuilder
= new MediaDescriptionCompat.Builder()
.setMediaId(String.valueOf(index))
.setTitle(item.getTitle())
.setSubtitle(item.getUploader());
// set additional metadata for A2DP/AVRCP
final Bundle additionalMetadata = new Bundle();
additionalMetadata.putString(MediaMetadataCompat.METADATA_KEY_TITLE, item.getTitle());
additionalMetadata.putString(MediaMetadataCompat.METADATA_KEY_ARTIST, item.getUploader());
additionalMetadata
.putLong(MediaMetadataCompat.METADATA_KEY_DURATION, item.getDuration() * 1000);
additionalMetadata.putLong(MediaMetadataCompat.METADATA_KEY_TRACK_NUMBER, index + 1);
additionalMetadata
.putLong(MediaMetadataCompat.METADATA_KEY_NUM_TRACKS, player.getPlayQueue().size());
descriptionBuilder.setExtras(additionalMetadata);
final Uri thumbnailUri = Uri.parse(item.getThumbnailUrl());
if (thumbnailUri != null) {
descriptionBuilder.setIconUri(thumbnailUri);
}
return descriptionBuilder.build();
}
@Override
public void play() {
player.play();
// hide the player controls even if the play command came from the media session
player.hideControls(0, 0);
}
@Override
public void pause() {
player.pause();
}
}

View File

@ -4,7 +4,7 @@ import android.content.Context;
import android.view.SurfaceHolder; import android.view.SurfaceHolder;
import com.google.android.exoplayer2.Player; import com.google.android.exoplayer2.Player;
import com.google.android.exoplayer2.video.DummySurface; import com.google.android.exoplayer2.video.PlaceholderSurface;
/** /**
* Prevent error message: 'Unrecoverable player error occurred' * Prevent error message: 'Unrecoverable player error occurred'
@ -26,7 +26,7 @@ public final class SurfaceHolderCallback implements SurfaceHolder.Callback {
private final Context context; private final Context context;
private final Player player; private final Player player;
private DummySurface dummySurface; private PlaceholderSurface placeholderSurface;
public SurfaceHolderCallback(final Context context, final Player player) { public SurfaceHolderCallback(final Context context, final Player player) {
this.context = context; this.context = context;
@ -47,16 +47,16 @@ public final class SurfaceHolderCallback implements SurfaceHolder.Callback {
@Override @Override
public void surfaceDestroyed(final SurfaceHolder holder) { public void surfaceDestroyed(final SurfaceHolder holder) {
if (dummySurface == null) { if (placeholderSurface == null) {
dummySurface = DummySurface.newInstanceV17(context, false); placeholderSurface = PlaceholderSurface.newInstanceV17(context, false);
} }
player.setVideoSurface(dummySurface); player.setVideoSurface(placeholderSurface);
} }
public void release() { public void release() {
if (dummySurface != null) { if (placeholderSurface != null) {
dummySurface.release(); placeholderSurface.release();
dummySurface = null; placeholderSurface = null;
} }
} }
} }

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