mirror of
https://github.com/TeamNewPipe/NewPipe
synced 2025-02-09 07:30:06 +00:00
Merge pull request #8889 from TeamNewPipe/release-0.24.0
Release v0.24.0 (990)
This commit is contained in:
commit
0c63950429
4
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
4
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
@ -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."
|
||||||
|
5
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
5
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
@ -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."
|
||||||
|
4
.github/ISSUE_TEMPLATE/question.yml
vendored
4
.github/ISSUE_TEMPLATE/question.yml
vendored
@ -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)."
|
||||||
|
14
.github/workflows/ci.yml
vendored
14
.github/workflows/ci.yml
vendored
@ -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
|
||||||
|
|
||||||
@ -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:
|
||||||
|
4
.github/workflows/image-minimizer.yml
vendored
4
.github/workflows/image-minimizer.yml
vendored
@ -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
|
||||||
|
6
.github/workflows/no-response.yml
vendored
6
.github/workflows/no-response.yml
vendored
@ -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
124
README.md
@ -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> • <a href="#description">Description</a> • <a href="#features">Features</a> • <a href="#installation-and-updates">Installation and updates</a> • <a href="#contribution">Contribution</a> • <a href="#donate">Donate</a> • <a href="#license">License</a></p>
|
<p align="center"><a href="#screenshots">Screenshots</a> • <a href="#supported-services">Supported Services</a> • <a href="#description">Description</a> • <a href="#features">Features</a> • <a href="#installation-and-updates">Installation and updates</a> • <a href="#contribution">Contribution</a> • <a href="#donate">Donate</a> • <a href="#license">License</a></p>
|
||||||
<p align="center"><a href="https://newpipe.net">Website</a> • <a href="https://newpipe.net/blog/">Blog</a> • <a href="https://newpipe.net/FAQ/">FAQ</a> • <a href="https://newpipe.net/press/">Press</a></p>
|
<p align="center"><a href="https://newpipe.net">Website</a> • <a href="https://newpipe.net/blog/">Blog</a> • <a href="https://newpipe.net/FAQ/">FAQ</a> • <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.
|
|
||||||
|
@ -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
|
||||||
|
11
app/proguard-rules.pro
vendored
11
app/proguard-rules.pro
vendored
@ -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>;
|
||||||
|
@ -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>
|
||||||
|
@ -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);
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
|
@ -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(getString(R.string.streams_notification_channel_description))
|
.setDescription(
|
||||||
.build());
|
getString(R.string.streams_notification_channel_description))
|
||||||
|
.build()
|
||||||
|
);
|
||||||
|
|
||||||
final NotificationManagerCompat notificationManager = NotificationManagerCompat.from(this);
|
final NotificationManagerCompat notificationManager = NotificationManagerCompat.from(this);
|
||||||
notificationManager.createNotificationChannelsCompat(notificationChannelCompats);
|
notificationManager.createNotificationChannelsCompat(notificationChannelCompats);
|
||||||
|
@ -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()
|
||||||
|
@ -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);
|
||||||
}
|
}
|
||||||
|
@ -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);
|
||||||
}
|
}
|
||||||
|
@ -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();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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"
|
||||||
|
@ -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());
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
|
||||||
|
@ -12,12 +12,8 @@ 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
|
||||||
@ -25,32 +21,13 @@ object LicenseFragmentHelper {
|
|||||||
* 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 {
|
||||||
val licenseContent = StringBuilder()
|
|
||||||
val webViewData: String
|
|
||||||
try {
|
try {
|
||||||
BufferedReader(
|
return context.assets.open(license.filename).bufferedReader().use { it.readText() }
|
||||||
InputStreamReader(
|
|
||||||
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
|
// split the HTML file and insert the stylesheet into the HEAD of the file
|
||||||
webViewData = "$licenseContent".replace(
|
.replace("</head>", "<style>${getLicenseStylesheet(context)}</style></head>")
|
||||||
"</head>",
|
|
||||||
"<style>" + getLicenseStylesheet(context) + "</style></head>"
|
|
||||||
)
|
|
||||||
}
|
|
||||||
} catch (e: IOException) {
|
} catch (e: IOException) {
|
||||||
throw IllegalArgumentException(
|
throw IllegalArgumentException("Could not get license file: ${license.filename}", e)
|
||||||
"Could not get license file: " + license.filename, e
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
return webViewData
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -59,21 +36,17 @@ object LicenseFragmentHelper {
|
|||||||
*/
|
*/
|
||||||
private fun getLicenseStylesheet(context: Context): String {
|
private fun getLicenseStylesheet(context: Context): String {
|
||||||
val isLightTheme = ThemeHelper.isLightThemeSelected(context)
|
val isLightTheme = ThemeHelper.isLightThemeSelected(context)
|
||||||
return (
|
val licenseBackgroundColor = getHexRGBColor(
|
||||||
"body{padding:12px 15px;margin:0;" + "background:#" + getHexRGBColor(
|
context, if (isLightTheme) R.color.light_license_background_color else R.color.dark_license_background_color
|
||||||
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}"
|
|
||||||
)
|
)
|
||||||
|
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}"
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -87,29 +60,25 @@ object LicenseFragmentHelper {
|
|||||||
return context.getString(color).substring(3)
|
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 {
|
fun showLicense(context: Context?, component: SoftwareComponent): Disposable {
|
||||||
return showLicense(context, component.license) { alertDialog ->
|
return showLicense(context, component.license) {
|
||||||
alertDialog.setPositiveButton(R.string.dismiss) { dialog, _ ->
|
setPositiveButton(R.string.dismiss) { dialog, _ ->
|
||||||
dialog.dismiss()
|
dialog.dismiss()
|
||||||
}
|
}
|
||||||
alertDialog.setNeutralButton(R.string.open_website_license) { _, _ ->
|
setNeutralButton(R.string.open_website_license) { _, _ ->
|
||||||
ShareUtils.openUrlInBrowser(context!!, component.link)
|
ShareUtils.openUrlInBrowser(context!!, component.link)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun showLicense(context: Context?, license: License) = showLicense(context, license) {
|
||||||
|
setPositiveButton(R.string.ok) { dialog, _ -> dialog.dismiss() }
|
||||||
|
}
|
||||||
|
|
||||||
private fun showLicense(
|
private fun showLicense(
|
||||||
context: Context?,
|
context: Context?,
|
||||||
license: License,
|
license: License,
|
||||||
block: (AlertDialog.Builder) -> Unit
|
block: AlertDialog.Builder.() -> AlertDialog.Builder
|
||||||
): Disposable {
|
): Disposable {
|
||||||
return if (context == null) {
|
return if (context == null) {
|
||||||
Disposable.empty()
|
Disposable.empty()
|
||||||
@ -118,20 +87,17 @@ object LicenseFragmentHelper {
|
|||||||
.subscribeOn(Schedulers.io())
|
.subscribeOn(Schedulers.io())
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
.subscribe { formattedLicense ->
|
.subscribe { formattedLicense ->
|
||||||
val webViewData = Base64.encodeToString(
|
val webViewData =
|
||||||
formattedLicense.toByteArray(StandardCharsets.UTF_8), Base64.NO_PADDING
|
Base64.encodeToString(formattedLicense.toByteArray(), Base64.NO_PADDING)
|
||||||
)
|
|
||||||
val webView = WebView(context)
|
val webView = WebView(context)
|
||||||
webView.loadData(webViewData, "text/html; charset=UTF-8", "base64")
|
webView.loadData(webViewData, "text/html; charset=UTF-8", "base64")
|
||||||
|
|
||||||
AlertDialog.Builder(context).apply {
|
|
||||||
setTitle(license.name)
|
|
||||||
setView(webView)
|
|
||||||
Localization.assureCorrectAppLanguage(context)
|
Localization.assureCorrectAppLanguage(context)
|
||||||
block(this)
|
AlertDialog.Builder(context)
|
||||||
show()
|
.setTitle(license.name)
|
||||||
}
|
.setView(webView)
|
||||||
}
|
.block()
|
||||||
|
.show()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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 */
|
||||||
|
@ -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(
|
||||||
"""
|
"""
|
||||||
@ -86,60 +47,37 @@ abstract class FeedDAO {
|
|||||||
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(
|
||||||
"""
|
"""
|
||||||
|
@ -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;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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++) {
|
||||||
|
@ -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();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -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)
|
||||||
|
@ -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)
|
||||||
|
@ -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);
|
||||||
}
|
}
|
||||||
|
@ -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());
|
||||||
}
|
}
|
||||||
|
@ -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:
|
||||||
|
// make sure not to open any player if there is nothing currently loaded!
|
||||||
|
// FIXME removing this `if` causes the player service to start correctly, then stop,
|
||||||
|
// then restart badly without calling `startForeground()`, causing a crash when
|
||||||
|
// later closing the detail fragment
|
||||||
|
if (currentInfo != null) {
|
||||||
autoPlayEnabled = true; // forcefully start playing
|
autoPlayEnabled = true; // forcefully start playing
|
||||||
// FIXME Workaround #7427
|
// FIXME Workaround #7427
|
||||||
if (isPlayerAvailable()) {
|
if (isPlayerAvailable()) {
|
||||||
player.setRecovery();
|
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) {
|
||||||
|
// 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();
|
||||||
|
}
|
||||||
|
|
||||||
|
// do all the null checks in the posted lambda, too, since the player, the binding and the
|
||||||
|
// view could be set or unset before the lambda gets executed on the next main thread cycle
|
||||||
|
new Handler(Looper.getMainLooper()).post(() -> {
|
||||||
if (!isPlayerAvailable() || getView() == null) {
|
if (!isPlayerAvailable() || getView() == null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if viewHolder already contains a child
|
// setup the surface view height, so that it fits the video correctly
|
||||||
if (player.getRootView().getParent() != binding.playerPlaceholder) {
|
|
||||||
playerService.removeViewFromParent();
|
|
||||||
}
|
|
||||||
setHeightThumbnail();
|
setHeightThumbnail();
|
||||||
|
|
||||||
// Prevent from re-adding a view multiple times
|
player.UIs().get(MainPlayerUi.class).ifPresent(playerUi -> {
|
||||||
if (player.getRootView().getParent() == null) {
|
// sometimes binding would be null here, even though getView() != null above u.u
|
||||||
binding.playerPlaceholder.addView(player.getRootView());
|
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,11 +2009,9 @@ 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() {
|
||||||
if (DEBUG) {
|
if (DEBUG) {
|
||||||
@ -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,18 +2349,25 @@ 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;
|
||||||
|
case BottomSheetBehavior.STATE_HALF_EXPANDED:
|
||||||
break;
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -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,48 +42,32 @@ 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 VideoDetailPlayerCrasher() {
|
private static final List<Pair<String, Supplier<ExoPlaybackException>>>
|
||||||
// No impls
|
AVAILABLE_EXCEPTION_TYPES = List.of(
|
||||||
}
|
new Pair<>("Source", () -> ExoPlaybackException.createForSource(
|
||||||
|
new IOException(DEFAULT_MSG),
|
||||||
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
|
ERROR_CODE_BEHIND_LIVE_WINDOW
|
||||||
)
|
)),
|
||||||
);
|
new Pair<>("Renderer", () -> ExoPlaybackException.createForRenderer(
|
||||||
exceptionTypes.put(
|
new Exception(DEFAULT_MSG),
|
||||||
"Renderer",
|
|
||||||
() -> ExoPlaybackException.createForRenderer(
|
|
||||||
new Exception(defaultMsg),
|
|
||||||
"Dummy renderer",
|
"Dummy renderer",
|
||||||
0,
|
0,
|
||||||
null,
|
null,
|
||||||
C.FORMAT_HANDLED,
|
C.FORMAT_HANDLED,
|
||||||
/*isRecoverable=*/false,
|
/*isRecoverable=*/false,
|
||||||
ERROR_CODE_DECODING_FAILED
|
ERROR_CODE_DECODING_FAILED
|
||||||
)
|
)),
|
||||||
);
|
new Pair<>("Unexpected", () -> ExoPlaybackException.createForUnexpected(
|
||||||
exceptionTypes.put(
|
new RuntimeException(DEFAULT_MSG),
|
||||||
"Unexpected",
|
|
||||||
() -> ExoPlaybackException.createForUnexpected(
|
|
||||||
new RuntimeException(defaultMsg),
|
|
||||||
ERROR_CODE_UNSPECIFIED
|
ERROR_CODE_UNSPECIFIED
|
||||||
)
|
)),
|
||||||
);
|
new Pair<>("Remote", () -> ExoPlaybackException.createForRemote(DEFAULT_MSG))
|
||||||
exceptionTypes.put(
|
|
||||||
"Remote",
|
|
||||||
() -> ExoPlaybackException.createForRemote(defaultMsg)
|
|
||||||
);
|
);
|
||||||
|
|
||||||
return Collections.unmodifiableMap(exceptionTypes);
|
private VideoDetailPlayerCrasher() {
|
||||||
|
// No impls
|
||||||
}
|
}
|
||||||
|
|
||||||
private static Context getThemeWrapperContext(final Context context) {
|
private static Context getThemeWrapperContext(final 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);
|
||||||
|
@ -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,44 +261,27 @@ public abstract class BaseListFragment<I, N> extends BaseStateFragment<I>
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
infoListAdapter.setOnChannelSelectedListener(new OnClickGesture<>() {
|
infoListAdapter.setOnChannelSelectedListener(selectedItem -> {
|
||||||
@Override
|
|
||||||
public void selected(final ChannelInfoItem selectedItem) {
|
|
||||||
try {
|
try {
|
||||||
onItemSelected(selectedItem);
|
onItemSelected(selectedItem);
|
||||||
NavigationHelper.openChannelFragment(getFM(),
|
NavigationHelper.openChannelFragment(getFM(), selectedItem.getServiceId(),
|
||||||
selectedItem.getServiceId(),
|
selectedItem.getUrl(), selectedItem.getName());
|
||||||
selectedItem.getUrl(),
|
|
||||||
selectedItem.getName());
|
|
||||||
} catch (final Exception e) {
|
} catch (final Exception e) {
|
||||||
ErrorUtil.showUiErrorSnackbar(
|
ErrorUtil.showUiErrorSnackbar(this, "Opening channel fragment", e);
|
||||||
BaseListFragment.this, "Opening channel fragment", e);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
infoListAdapter.setOnPlaylistSelectedListener(new OnClickGesture<>() {
|
infoListAdapter.setOnPlaylistSelectedListener(selectedItem -> {
|
||||||
@Override
|
|
||||||
public void selected(final PlaylistInfoItem selectedItem) {
|
|
||||||
try {
|
try {
|
||||||
onItemSelected(selectedItem);
|
onItemSelected(selectedItem);
|
||||||
NavigationHelper.openPlaylistFragment(getFM(),
|
NavigationHelper.openPlaylistFragment(getFM(), selectedItem.getServiceId(),
|
||||||
selectedItem.getServiceId(),
|
selectedItem.getUrl(), selectedItem.getName());
|
||||||
selectedItem.getUrl(),
|
|
||||||
selectedItem.getName());
|
|
||||||
} catch (final Exception e) {
|
} catch (final Exception e) {
|
||||||
ErrorUtil.showUiErrorSnackbar(BaseListFragment.this,
|
ErrorUtil.showUiErrorSnackbar(this, "Opening playlist fragment", e);
|
||||||
"Opening playlist fragment", e);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
infoListAdapter.setOnCommentsSelectedListener(new OnClickGesture<>() {
|
infoListAdapter.setOnCommentsSelectedListener(this::onItemSelected);
|
||||||
@Override
|
|
||||||
public void selected(final CommentsInfoItem selectedItem) {
|
|
||||||
onItemSelected(selectedItem);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// 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();
|
||||||
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
/*//////////////////////////////////////////////////////////////////////////
|
/*//////////////////////////////////////////////////////////////////////////
|
||||||
|
@ -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;
|
||||||
|
@ -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(
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
|
@ -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())),
|
||||||
|
|
||||||
|
@ -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) {
|
||||||
|
@ -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);
|
||||||
|
@ -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());
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
|
@ -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);
|
||||||
|
@ -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())
|
||||||
|
@ -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);
|
||||||
|
@ -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"));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -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)
|
||||||
|
@ -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
|
||||||
|
)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// //////////////////////////////////////////////////////////////////////////
|
// //////////////////////////////////////////////////////////////////////////
|
||||||
|
@ -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 {
|
|
||||||
return FeedViewModel(
|
|
||||||
context.applicationContext,
|
|
||||||
groupId,
|
groupId,
|
||||||
// Read initial value from preferences
|
// Read initial value from preferences
|
||||||
getShowPlayedItemsFromPreferences(context.applicationContext)
|
getShowPlayedItemsFromPreferences(context.applicationContext),
|
||||||
) as T
|
getShowFutureItemsFromPreferences(context.applicationContext)
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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 {
|
||||||
|
override fun onBitmapLoaded(bitmap: Bitmap, from: Picasso.LoadedFrom) {
|
||||||
|
builder.setLargeIcon(bitmap) // set only if there is actually one
|
||||||
manager.notify(data.pseudoId, builder.build())
|
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 {
|
||||||
|
@ -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(() -> {
|
||||||
|
@ -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) {
|
||||||
|
@ -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);
|
||||||
|
@ -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
|
||||||
|
@ -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(
|
||||||
|
@ -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
|
||||||
|
@ -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) }
|
||||||
|
@ -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()));
|
||||||
|
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -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
149
app/src/main/java/org/schabi/newpipe/player/PlayerService.java
Normal file
149
app/src/main/java/org/schabi/newpipe/player/PlayerService.java
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
32
app/src/main/java/org/schabi/newpipe/player/PlayerType.java
Normal file
32
app/src/main/java/org/schabi/newpipe/player/PlayerType.java
Normal 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())];
|
||||||
|
}
|
||||||
|
}
|
@ -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.
|
||||||
|
@ -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
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,7 +0,0 @@
|
|||||||
package org.schabi.newpipe.player.event
|
|
||||||
|
|
||||||
interface DoubleTapListener {
|
|
||||||
fun onDoubleTapStarted(portion: DisplayPortion) {}
|
|
||||||
fun onDoubleTapProgressDown(portion: DisplayPortion) {}
|
|
||||||
fun onDoubleTapFinished() {}
|
|
||||||
}
|
|
@ -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;
|
||||||
|
@ -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");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
@ -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();
|
||||||
|
@ -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();
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
@ -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);
|
@ -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
|
@ -0,0 +1,7 @@
|
|||||||
|
package org.schabi.newpipe.player.gesture
|
||||||
|
|
||||||
|
interface DoubleTapListener {
|
||||||
|
fun onDoubleTapStarted(portion: DisplayPortion)
|
||||||
|
fun onDoubleTapProgressDown(portion: DisplayPortion)
|
||||||
|
fun onDoubleTapFinished()
|
||||||
|
}
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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();
|
|
||||||
}
|
|
||||||
}
|
|
@ -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;
|
||||||
|
@ -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));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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),
|
||||||
|
@ -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) {
|
||||||
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
@ -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();
|
||||||
}
|
}
|
||||||
|
@ -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();
|
|
||||||
}
|
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
@ -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) {
|
||||||
|
@ -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();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
@ -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);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -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();
|
|
||||||
}
|
|
||||||
}
|
|
@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -82,7 +82,7 @@ abstract class AbstractInfoPlayQueue<T extends ListInfo<StreamInfoItem>>
|
|||||||
public void onError(@NonNull final Throwable e) {
|
public void onError(@NonNull final Throwable e) {
|
||||||
Log.e(getTag(), "Error fetching more playlist, marking playlist as complete.", e);
|
Log.e(getTag(), "Error fetching more playlist, marking playlist as complete.", e);
|
||||||
isComplete = true;
|
isComplete = true;
|
||||||
append(); // Notify change
|
notifyChange();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@ -117,7 +117,7 @@ abstract class AbstractInfoPlayQueue<T extends ListInfo<StreamInfoItem>>
|
|||||||
public void onError(@NonNull final Throwable e) {
|
public void onError(@NonNull final Throwable e) {
|
||||||
Log.e(getTag(), "Error fetching more playlist, marking playlist as complete.", e);
|
Log.e(getTag(), "Error fetching more playlist, marking playlist as complete.", e);
|
||||||
isComplete = true;
|
isComplete = true;
|
||||||
append(); // Notify change
|
notifyChange();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user