diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml
index a0a9f9ef58d..d3befbc8114 100644
--- a/.github/ISSUE_TEMPLATE/bug_report.yml
+++ b/.github/ISSUE_TEMPLATE/bug_report.yml
@@ -1,6 +1,6 @@
name: Bug report
description: Create a bug report to help us improve
-labels: [bug]
+labels: [bug, needs triage]
body:
- type: markdown
attributes:
@@ -18,6 +18,8 @@ body:
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."
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."
required: true
- label: "This issue contains only one bug."
@@ -40,7 +42,7 @@ body:
label: Steps to reproduce the bug
description: |
What did you do for the bug to show up?
-
+
If you can't cause the bug to show up again reliably (and hence don't have a proper set of steps to give us), please still try to give as many details as possible on how you think you encountered the bug.
placeholder: |
1. Go to '...'
@@ -69,11 +71,11 @@ body:
label: Screenshots/Screen recordings
description: |
A picture or video is worth a thousand words.
-
+
If applicable, add screenshots or a screen recording to help explain your problem.
GitHub supports uploading them directly in the text box.
If your file is too big for Github to accept, try to compress it (ZIP-file) or feel free to paste a link to an image/video hoster here instead.
-
+
:heavy_exclamation_mark: DON'T POST SCREENSHOTS OF THE ERROR PAGE.
Instead, follow the instructions in the "Logs" section below.
diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml
index 83d6f029997..52b2a424109 100644
--- a/.github/ISSUE_TEMPLATE/feature_request.yml
+++ b/.github/ISSUE_TEMPLATE/feature_request.yml
@@ -1,6 +1,6 @@
name: Feature request
description: Suggest an idea for this project
-labels: [enhancement]
+labels: [enhancement, needs triage]
body:
- type: markdown
attributes:
@@ -8,7 +8,6 @@ body:
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.
-
- type: checkboxes
id: checklist
attributes:
@@ -16,6 +15,8 @@ body:
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."
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)."
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."
@@ -43,7 +44,7 @@ body:
Describe any problem or limitation you come across while using the app which would be solved by this feature.
validations:
required: true
-
+
- type: textarea
id: additional-information
attributes:
diff --git a/.github/ISSUE_TEMPLATE/question.yml b/.github/ISSUE_TEMPLATE/question.yml
index 4c42ab26a97..134f171f767 100644
--- a/.github/ISSUE_TEMPLATE/question.yml
+++ b/.github/ISSUE_TEMPLATE/question.yml
@@ -1,6 +1,6 @@
name: Question
description: Ask about anything NewPipe-related
-labels: [question]
+labels: [question, needs triage]
body:
- type: markdown
attributes:
@@ -16,6 +16,8 @@ body:
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."
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."
required: true
- label: "I have read and understood the [contribution guidelines](https://github.com/TeamNewPipe/NewPipe/blob/dev/.github/CONTRIBUTING.md)."
@@ -27,7 +29,7 @@ body:
label: What is/are your question(s)?
validations:
required: true
-
+
- type: textarea
id: additional-information
attributes:
diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md
index 10e40af2acb..abc1665eb8c 100644
--- a/.github/PULL_REQUEST_TEMPLATE.md
+++ b/.github/PULL_REQUEST_TEMPLATE.md
@@ -25,7 +25,7 @@
-
-#### APK testing
+#### APK testing
The APK can be found by going to the "Checks" tab below the title. On the left pane, click on "CI", scroll down to "artifacts" and click "app" to download the zip file which contains the debug APK of this PR.
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 306b8c2c8f0..d9342e72a79 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -6,7 +6,7 @@ on:
branches:
- dev
- master
- - release/**
+ - release**
paths-ignore:
- 'README.md'
- 'doc/**'
@@ -31,6 +31,10 @@ on:
jobs:
build-and-test-jvm:
runs-on: ubuntu-latest
+
+ permissions:
+ contents: read
+
steps:
- uses: actions/checkout@v3
- uses: gradle/wrapper-validation-action@v1
@@ -64,6 +68,10 @@ jobs:
matrix:
# api-level 19 is min sdk, but throws errors related to desugaring
api-level: [ 21, 29 ]
+
+ permissions:
+ contents: read
+
steps:
- uses: actions/checkout@v3
@@ -81,7 +89,7 @@ jobs:
# workaround to emulator bug: https://github.com/ReactiveCircus/android-emulator-runner/issues/160
emulator-build: 7425822
script: ./gradlew connectedCheck --stacktrace
-
+
- name: Upload test report when tests fail # because the printed out stacktrace (console) is too short, see also #7553
uses: actions/upload-artifact@v3
if: failure()
@@ -91,6 +99,10 @@ jobs:
sonar:
runs-on: ubuntu-latest
+
+ permissions:
+ contents: read
+
steps:
- uses: actions/checkout@v3
with:
diff --git a/.github/workflows/image-minimizer.yml b/.github/workflows/image-minimizer.yml
index c6ab6d5b387..b8bf9e1d296 100644
--- a/.github/workflows/image-minimizer.yml
+++ b/.github/workflows/image-minimizer.yml
@@ -6,6 +6,10 @@ on:
issues:
types: [opened, edited]
+permissions:
+ issues: write
+ pull-requests: write
+
jobs:
try-minimize:
runs-on: ubuntu-latest
diff --git a/.github/workflows/no-response.yml b/.github/workflows/no-response.yml
index 54e749dc0a4..b3495135ffa 100644
--- a/.github/workflows/no-response.yml
+++ b/.github/workflows/no-response.yml
@@ -9,6 +9,10 @@ on:
# Run daily at midnight.
- cron: '0 0 * * *'
+permissions:
+ issues: write
+ pull-requests: write
+
jobs:
noResponse:
runs-on: ubuntu-latest
@@ -17,4 +21,4 @@ jobs:
with:
token: ${{ github.token }}
daysUntilClose: 14
- responseRequiredLabel: waiting-for-author
+ responseRequiredLabel: waiting for author
diff --git a/README.md b/README.md
index c47f8c2f4fb..52e6eef1a96 100644
--- a/README.md
+++ b/README.md
@@ -1,6 +1,6 @@
NewPipe
-
A libre lightweight streaming frontend for Android.
+
A libre lightweight streaming front-end for Android.
*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).*
-WARNING: THIS IS A BETA VERSION, THEREFORE YOU MAY ENCOUNTER BUGS. IF YOU DO, OPEN AN ISSUE VIA OUR GITHUB REPOSITORY.
+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.
-PUTTING NEWPIPE OR ANY FORK OF IT INTO THE GOOGLE PLAY STORE VIOLATES THEIR TERMS AND CONDITIONS.
+PUTTING NEWPIPE, OR ANY FORK OF IT, INTO THE GOOGLE PLAY STORE VIOLATES THEIR TERMS AND CONDITIONS.
## Screenshots
@@ -38,62 +38,66 @@
[](fastlane/metadata/android/en-US/images/tenInchScreenshots/shot_11.png)
[](fastlane/metadata/android/en-US/images/tenInchScreenshots/shot_12.png)
-## Description
+### Supported Services
-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 currently supports these services:
-### Features
+
+* 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))
-* Search videos
-* No Login Required
-* Display general info about videos
-* Watch YouTube videos
-* Listen to YouTube videos
-* Popup mode (floating player)
-* Select streaming player to watch video with
-* Download videos
-* Download audio only
-* Open a video in Kodi
-* Show next/related videos
-* Search YouTube in a specific language
-* Watch/Block age restricted material
-* Display general info about channels
-* Search channels
-* Watch videos from a channel
-* Orbot/Tor support (not yet directly)
-* 1080p/2K/4K support
-* View history
-* Subscribe to channels
-* Search history
-* Search/watch playlists
-* Watch as enqueued playlists
-* Enqueue videos
-* Local playlists
-* Subtitles
-* Livestream support
-* Show comments
+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!
-### Supported Services
+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).
-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:
+## Description
-* YouTube
-* SoundCloud \[beta\]
-* media.ccc.de \[beta\]
-* PeerTube instances \[beta\]
-* Bandcamp \[beta\]
+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
+
+* Watch videos at resolutions up to 4K
+* Listen to audio in the background, only loading the audio stream to save data
+* Popup mode (floating player, aka Picture-in-Picture)
+* Watch live streams
+* Show/hide subtitles/closed captions
+* Search videos and audios (on YouTube, you can specify the content language as well)
+* Enqueue videos (and optionally save them as local playlists)
+* Show/hide general information about videos (such as description and tags)
+* Show/hide next/related videos
+* Show/hide comments
+* Search videos, audios, channels, playlists and albums
+* Browse videos and audios within a channel
+* Subscribe to channels (yes, without logging into any account!)
+* Get notifications about new videos from channels you're subscribed to
+* Create and edit channel groups (for easier browsing and management)
+* Browse video feeds generated from your channel groups
+* View and search your watch history
+* Search and watch playlists (these are remote playlists, which means they're fetched from the service you're browsing)
+* Create and edit local playlists (these are created and saved within the app, and have nothing to do with any service)
+* Download videos/audios/subtitles (closed captions)
+* Open in Kodi
+* Watch/Block age-restricted material
+
+
## Installation and updates
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/
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.
+ 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:
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
4. Import the data from step 1 via Settings > Content > Import Database
-## Contribution
-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!
+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.
-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).
## 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).
-
-
-
-
16A9J59ahMRqkLSZjhYj33n9j3fMztFxnh
-
+
+
+
+
16A9J59ahMRqkLSZjhYj33n9j3fMztFxnh
+
@@ -134,14 +137,9 @@ If you like NewPipe we'd be happy about a donation. You can either send bitcoin
## Privacy Policy
-The NewPipe project aims to provide a private, anonymous experience for using media web services.
-Therefore, the app does not collect any data without your consent. NewPipe's privacy policy explains in detail what data is sent and stored when you send a crash report, or comment in our blog. You can find the document [here](https://newpipe.net/legal/privacy/).
+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/).
## License
[![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
-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.
+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.
diff --git a/app/build.gradle b/app/build.gradle
index b297d57541b..9d941d5a75f 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -14,15 +14,12 @@ android {
defaultConfig {
applicationId "org.schabi.newpipe"
resValue "string", "app_name", "NewPipe"
- minSdk 19
+ minSdk 21
targetSdk 29
- versionCode 989
- versionName "0.23.3"
-
- multiDexEnabled true
+ versionCode 990
+ versionName "0.24.0"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
- vectorDrawables.useSupportLibrary = true
javaCompileOptions {
annotationProcessorOptions {
@@ -98,14 +95,14 @@ android {
}
ext {
- checkstyleVersion = '10.0'
+ checkstyleVersion = '10.3.1'
- androidxLifecycleVersion = '2.3.1'
- androidxRoomVersion = '2.4.2'
+ androidxLifecycleVersion = '2.5.1'
+ androidxRoomVersion = '2.4.3'
androidxWorkVersion = '2.7.1'
icepickVersion = '3.2.0'
- exoPlayerVersion = '2.17.1'
+ exoPlayerVersion = '2.18.1'
googleAutoServiceVersion = '1.0.1'
groupieVersion = '2.10.1'
markwonVersion = '4.6.2'
@@ -113,7 +110,7 @@ ext {
leakCanaryVersion = '2.5'
stethoVersion = '1.6.0'
mockitoVersion = '4.0.0'
- assertJVersion = '3.22.0'
+ assertJVersion = '3.23.1'
}
configurations {
@@ -182,7 +179,7 @@ sonarqube {
dependencies {
/** Desugaring **/
- coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.1.5'
+ coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.1.6'
/** NewPipe libraries **/
// 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
// This works thanks to JitPack: https://jitpack.io/
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 "com.puppycrawl.tools:checkstyle:${checkstyleVersion}"
- ktlint 'com.pinterest:ktlint:0.44.0'
+ ktlint 'com.pinterest:ktlint:0.45.2'
/** Kotlin **/
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:${kotlin_version}"
/** AndroidX **/
- implementation 'androidx.appcompat:appcompat:1.3.1'
+ implementation 'androidx.appcompat:appcompat:1.4.2'
implementation 'androidx.cardview:cardview:1.0.0'
- implementation 'androidx.constraintlayout:constraintlayout:2.1.3'
- implementation 'androidx.core:core-ktx:1.6.0'
+ implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
+ implementation 'androidx.core:core-ktx:1.8.0'
implementation 'androidx.documentfile:documentfile:1.0.1'
- implementation 'androidx.fragment:fragment-ktx:1.3.6'
- implementation "androidx.lifecycle:lifecycle-livedata:${androidxLifecycleVersion}"
- implementation "androidx.lifecycle:lifecycle-viewmodel:${androidxLifecycleVersion}"
+ implementation 'androidx.fragment:fragment-ktx:1.4.1'
+ implementation "androidx.lifecycle:lifecycle-livedata-ktx:${androidxLifecycleVersion}"
+ implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:${androidxLifecycleVersion}"
implementation 'androidx.localbroadcastmanager:localbroadcastmanager:1.1.0'
- implementation 'androidx.media:media:1.5.0'
- implementation 'androidx.multidex:multidex:2.0.1'
+ implementation 'androidx.media:media:1.6.0'
implementation 'androidx.preference:preference:1.2.0'
implementation 'androidx.recyclerview:recyclerview:1.2.1'
implementation "androidx.room:room-runtime:${androidxRoomVersion}"
@@ -220,10 +217,9 @@ dependencies {
// Newer version specified to prevent accessibility regressions with RecyclerView, see:
// https://developer.android.com/jetpack/androidx/releases/viewpager2#1.1.0-alpha01
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-rxjava3:${androidxWorkVersion}"
- implementation 'com.google.android.material:material:1.5.0'
+ implementation 'com.google.android.material:material:1.6.1'
/** Third-party libraries **/
// Instance state boilerplate elimination
@@ -234,11 +230,16 @@ dependencies {
implementation "org.jsoup:jsoup:1.15.3"
// HTTP client
- //noinspection GradleDependency --> do not update okhttp to keep supporting Android 4.4 users
- implementation "com.squareup.okhttp3:okhttp:3.12.13"
+ implementation "com.squareup.okhttp3:okhttp:4.10.0"
// 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}"
// Metadata generator for service descriptors
@@ -257,9 +258,6 @@ dependencies {
implementation "io.noties.markwon:core:${markwonVersion}"
implementation "io.noties.markwon:linkify:${markwonVersion}"
- // File picker
- implementation "com.nononsenseapps:filepicker:4.2.1"
-
// Crash reporting
implementation "ch.acra:acra-core:5.9.3"
@@ -273,7 +271,7 @@ dependencies {
implementation "com.jakewharton.rxbinding4:rxbinding:4.0.0"
// Date and time formatting
- implementation "org.ocpsoft.prettytime:prettytime:5.0.2.Final"
+ implementation "org.ocpsoft.prettytime:prettytime:5.0.3.Final"
/** Debugging **/
// Memory leak detection
diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro
index 4a54d8992e9..5e10d3916ab 100644
--- a/app/proguard-rules.pro
+++ b/app/proguard-rules.pro
@@ -18,7 +18,6 @@
-dontobfuscate
-keep class org.schabi.newpipe.extractor.timeago.patterns.** { *; }
--keep class org.ocpsoft.prettytime.i18n.** { *; }
-keep class org.mozilla.javascript.** { *; }
@@ -26,9 +25,6 @@
-keep class com.google.android.exoplayer2.** { *; }
-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
-dontwarn icepick.**
@@ -39,12 +35,11 @@
}
-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 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 {
static final long serialVersionUID;
!static !transient ;
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index f9c99819c4a..04e28c1eac8 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -44,7 +44,7 @@
diff --git a/app/src/main/java/androidx/fragment/app/FragmentStatePagerAdapterMenuWorkaround.java b/app/src/main/java/androidx/fragment/app/FragmentStatePagerAdapterMenuWorkaround.java
index 6394433773c..8d87e90bddf 100644
--- a/app/src/main/java/androidx/fragment/app/FragmentStatePagerAdapterMenuWorkaround.java
+++ b/app/src/main/java/androidx/fragment/app/FragmentStatePagerAdapterMenuWorkaround.java
@@ -282,11 +282,9 @@ public boolean isViewFromObject(@NonNull final View view, @NonNull final Object
@Nullable
public Parcelable saveState() {
Bundle state = null;
- if (mSavedState.size() > 0) {
+ if (!mSavedState.isEmpty()) {
state = new Bundle();
- final Fragment.SavedState[] fss = new Fragment.SavedState[mSavedState.size()];
- mSavedState.toArray(fss);
- state.putParcelableArray("states", fss);
+ state.putParcelableArray("states", mSavedState.toArray(new Fragment.SavedState[0]));
}
for (int i = 0; i < mFragments.size(); i++) {
final Fragment f = mFragments.get(i);
diff --git a/app/src/main/java/com/google/android/material/appbar/FlingBehavior.java b/app/src/main/java/com/google/android/material/appbar/FlingBehavior.java
index 3e5f408f754..52754e8fa9f 100644
--- a/app/src/main/java/com/google/android/material/appbar/FlingBehavior.java
+++ b/app/src/main/java/com/google/android/material/appbar/FlingBehavior.java
@@ -14,7 +14,6 @@
import org.schabi.newpipe.R;
import java.lang.reflect.Field;
-import java.util.Arrays;
import java.util.List;
// See https://stackoverflow.com/questions/56849221#57997489
@@ -27,7 +26,7 @@ public FlingBehavior(final Context context, final AttributeSet attrs) {
private boolean allowScroll = true;
private final Rect globalRect = new Rect();
- private final List skipInterceptionOfElements = Arrays.asList(
+ private final List skipInterceptionOfElements = List.of(
R.id.itemsListPanel, R.id.playbackSeekBar,
R.id.playPauseButton, R.id.playPreviousButton, R.id.playNextButton);
@@ -67,7 +66,7 @@ public boolean onRequestChildRectangleOnScreen(
public boolean onInterceptTouchEvent(@NonNull final CoordinatorLayout parent,
@NonNull final AppBarLayout child,
@NonNull final MotionEvent ev) {
- for (final Integer element : skipInterceptionOfElements) {
+ for (final int element : skipInterceptionOfElements) {
final View view = child.findViewById(element);
if (view != null) {
final boolean visible = view.getGlobalVisibleRect(globalRect);
@@ -132,8 +131,8 @@ private Field getLastNestedScrollingChildRefField() {
try {
final Class> headerBehaviorType = this.getClass().getSuperclass().getSuperclass();
if (headerBehaviorType != null) {
- final Field field
- = headerBehaviorType.getDeclaredField("lastNestedScrollingChildRef");
+ final Field field =
+ headerBehaviorType.getDeclaredField("lastNestedScrollingChildRef");
field.setAccessible(true);
return field;
}
diff --git a/app/src/main/java/org/schabi/newpipe/App.java b/app/src/main/java/org/schabi/newpipe/App.java
index 70c9474786a..f4410a31b2a 100644
--- a/app/src/main/java/org/schabi/newpipe/App.java
+++ b/app/src/main/java/org/schabi/newpipe/App.java
@@ -1,5 +1,6 @@
package org.schabi.newpipe;
+import android.app.Application;
import android.content.Context;
import android.content.SharedPreferences;
import android.util.Log;
@@ -7,7 +8,6 @@
import androidx.annotation.NonNull;
import androidx.core.app.NotificationChannelCompat;
import androidx.core.app.NotificationManagerCompat;
-import androidx.multidex.MultiDexApplication;
import androidx.preference.PreferenceManager;
import com.jakewharton.processphoenix.ProcessPhoenix;
@@ -27,9 +27,8 @@
import java.io.IOException;
import java.io.InterruptedIOException;
import java.net.SocketException;
-import java.util.ArrayList;
-import java.util.Collections;
import java.util.List;
+import java.util.Objects;
import io.reactivex.rxjava3.exceptions.CompositeException;
import io.reactivex.rxjava3.exceptions.MissingBackpressureException;
@@ -56,7 +55,7 @@
* along with NewPipe. If not, see .
*/
-public class App extends MultiDexApplication {
+public class App extends Application {
public static final String PACKAGE_NAME = BuildConfig.APPLICATION_ID;
private static final String TAG = App.class.toString();
private static App app;
@@ -140,7 +139,7 @@ public void accept(@NonNull final Throwable throwable) {
if (throwable instanceof UndeliverableException) {
// As UndeliverableException is a wrapper,
// get the cause of it to get the "real" exception
- actualThrowable = throwable.getCause();
+ actualThrowable = Objects.requireNonNull(throwable.getCause());
} else {
actualThrowable = throwable;
}
@@ -149,7 +148,7 @@ public void accept(@NonNull final Throwable throwable) {
if (actualThrowable instanceof CompositeException) {
errors = ((CompositeException) actualThrowable).getExceptions();
} else {
- errors = Collections.singletonList(actualThrowable);
+ errors = List.of(actualThrowable);
}
for (final Throwable error : errors) {
@@ -213,41 +212,37 @@ protected void initACRA() {
private void initNotificationChannels() {
// Keep the importance below DEFAULT to avoid making noise on every notification update for
// the main and update channels
- final List notificationChannelCompats = new ArrayList<>();
- notificationChannelCompats.add(new NotificationChannelCompat
- .Builder(getString(R.string.notification_channel_id),
+ final List notificationChannelCompats = List.of(
+ new NotificationChannelCompat.Builder(getString(R.string.notification_channel_id),
NotificationManagerCompat.IMPORTANCE_LOW)
- .setName(getString(R.string.notification_channel_name))
- .setDescription(getString(R.string.notification_channel_description))
- .build());
-
- notificationChannelCompats.add(new NotificationChannelCompat
- .Builder(getString(R.string.app_update_notification_channel_id),
+ .setName(getString(R.string.notification_channel_name))
+ .setDescription(getString(R.string.notification_channel_description))
+ .build(),
+ new NotificationChannelCompat
+ .Builder(getString(R.string.app_update_notification_channel_id),
NotificationManagerCompat.IMPORTANCE_LOW)
- .setName(getString(R.string.app_update_notification_channel_name))
- .setDescription(getString(R.string.app_update_notification_channel_description))
- .build());
-
- notificationChannelCompats.add(new NotificationChannelCompat
- .Builder(getString(R.string.hash_channel_id),
+ .setName(getString(R.string.app_update_notification_channel_name))
+ .setDescription(
+ getString(R.string.app_update_notification_channel_description))
+ .build(),
+ new NotificationChannelCompat.Builder(getString(R.string.hash_channel_id),
NotificationManagerCompat.IMPORTANCE_HIGH)
- .setName(getString(R.string.hash_channel_name))
- .setDescription(getString(R.string.hash_channel_description))
- .build());
-
- notificationChannelCompats.add(new NotificationChannelCompat
- .Builder(getString(R.string.error_report_channel_id),
+ .setName(getString(R.string.hash_channel_name))
+ .setDescription(getString(R.string.hash_channel_description))
+ .build(),
+ new NotificationChannelCompat.Builder(getString(R.string.error_report_channel_id),
NotificationManagerCompat.IMPORTANCE_LOW)
- .setName(getString(R.string.error_report_channel_name))
- .setDescription(getString(R.string.error_report_channel_description))
- .build());
-
- notificationChannelCompats.add(new NotificationChannelCompat
- .Builder(getString(R.string.streams_notification_channel_id),
- NotificationManagerCompat.IMPORTANCE_DEFAULT)
- .setName(getString(R.string.streams_notification_channel_name))
- .setDescription(getString(R.string.streams_notification_channel_description))
- .build());
+ .setName(getString(R.string.error_report_channel_name))
+ .setDescription(getString(R.string.error_report_channel_description))
+ .build(),
+ new NotificationChannelCompat
+ .Builder(getString(R.string.streams_notification_channel_id),
+ NotificationManagerCompat.IMPORTANCE_DEFAULT)
+ .setName(getString(R.string.streams_notification_channel_name))
+ .setDescription(
+ getString(R.string.streams_notification_channel_description))
+ .build()
+ );
final NotificationManagerCompat notificationManager = NotificationManagerCompat.from(this);
notificationManager.createNotificationChannelsCompat(notificationChannelCompats);
diff --git a/app/src/main/java/org/schabi/newpipe/DownloaderImpl.java b/app/src/main/java/org/schabi/newpipe/DownloaderImpl.java
index 1a3a8adee11..9ddbe96dfc9 100644
--- a/app/src/main/java/org/schabi/newpipe/DownloaderImpl.java
+++ b/app/src/main/java/org/schabi/newpipe/DownloaderImpl.java
@@ -1,7 +1,6 @@
package org.schabi.newpipe;
import android.content.Context;
-import android.os.Build;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
@@ -12,40 +11,27 @@
import org.schabi.newpipe.extractor.downloader.Request;
import org.schabi.newpipe.extractor.downloader.Response;
import org.schabi.newpipe.extractor.exceptions.ReCaptchaException;
-import org.schabi.newpipe.util.CookieUtils;
import org.schabi.newpipe.util.InfoCache;
-import org.schabi.newpipe.util.TLSSocketFactoryCompat;
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.HashMap;
import java.util.List;
import java.util.Map;
+import java.util.Objects;
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.RequestBody;
import okhttp3.ResponseBody;
-import static org.schabi.newpipe.MainActivity.DEBUG;
-
public final class DownloaderImpl extends Downloader {
- public static final String USER_AGENT
- = "Mozilla/5.0 (Windows NT 10.0; rv:91.0) Gecko/20100101 Firefox/91.0";
- public static final String YOUTUBE_RESTRICTED_MODE_COOKIE_KEY
- = "youtube_restricted_mode_key";
+ public static final String USER_AGENT =
+ "Mozilla/5.0 (Windows NT 10.0; rv:91.0) Gecko/20100101 Firefox/91.0";
+ public static final String YOUTUBE_RESTRICTED_MODE_COOKIE_KEY =
+ "youtube_restricted_mode_key";
public static final String YOUTUBE_RESTRICTED_MODE_COOKIE = "PREF=f2=8000000";
public static final String YOUTUBE_DOMAIN = "youtube.com";
@@ -54,9 +40,6 @@ public final class DownloaderImpl extends Downloader {
private final OkHttpClient client;
private DownloaderImpl(final OkHttpClient.Builder builder) {
- if (Build.VERSION.SDK_INT == Build.VERSION_CODES.KITKAT) {
- enableModernTLS(builder);
- }
this.client = builder
.readTimeout(30, TimeUnit.SECONDS)
// .cache(new Cache(new File(context.getExternalCacheDir(), "okhttp"),
@@ -81,69 +64,16 @@ public static DownloaderImpl getInstance() {
return instance;
}
- /**
- * Enable TLS 1.2 and 1.1 on Android Kitkat. This function is mostly taken
- * from the documentation of OkHttpClient.Builder.sslSocketFactory(_,_).
- *
- * If there is an error, the function will safely fall back to doing nothing
- * and printing the error to the console.
- *
- *
- * @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 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) {
- final List resultCookies = new ArrayList<>();
- if (url.contains(YOUTUBE_DOMAIN)) {
- final String youtubeCookie = getCookie(YOUTUBE_RESTRICTED_MODE_COOKIE_KEY);
- if (youtubeCookie != null) {
- resultCookies.add(youtubeCookie);
- }
- }
+ final String youtubeCookie = url.contains(YOUTUBE_DOMAIN)
+ ? getCookie(YOUTUBE_RESTRICTED_MODE_COOKIE_KEY) : null;
+
// Recaptcha cookie is always added TODO: not sure if this is necessary
- final String recaptchaCookie = getCookie(ReCaptchaActivity.RECAPTCHA_COOKIES_KEY);
- if (recaptchaCookie != null) {
- resultCookies.add(recaptchaCookie);
- }
- return CookieUtils.concatCookies(resultCookies);
+ return Stream.of(youtubeCookie, getCookie(ReCaptchaActivity.RECAPTCHA_COOKIES_KEY))
+ .filter(Objects::nonNull)
+ .flatMap(cookies -> Arrays.stream(cookies.split("; *")))
+ .distinct()
+ .collect(Collectors.joining("; "));
}
public String getCookie(final String key) {
@@ -203,7 +133,7 @@ public Response execute(@NonNull final Request request)
RequestBody requestBody = null;
if (dataToSend != null) {
- requestBody = RequestBody.create(null, dataToSend);
+ requestBody = RequestBody.create(dataToSend);
}
final okhttp3.Request.Builder requestBuilder = new okhttp3.Request.Builder()
diff --git a/app/src/main/java/org/schabi/newpipe/ExitActivity.java b/app/src/main/java/org/schabi/newpipe/ExitActivity.java
index 8da22db2d3a..bd1351f0c1f 100644
--- a/app/src/main/java/org/schabi/newpipe/ExitActivity.java
+++ b/app/src/main/java/org/schabi/newpipe/ExitActivity.java
@@ -3,7 +3,6 @@
import android.annotation.SuppressLint;
import android.app.Activity;
import android.content.Intent;
-import android.os.Build;
import android.os.Bundle;
import org.schabi.newpipe.util.NavigationHelper;
@@ -44,11 +43,7 @@ public static void exitAndRemoveFromRecentApps(final Activity activity) {
protected void onCreate(final Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
- finishAndRemoveTask();
- } else {
- finish();
- }
+ finishAndRemoveTask();
NavigationHelper.restartApp(this);
}
diff --git a/app/src/main/java/org/schabi/newpipe/MainActivity.java b/app/src/main/java/org/schabi/newpipe/MainActivity.java
index fcb9d9725d0..d4b2305c7e3 100644
--- a/app/src/main/java/org/schabi/newpipe/MainActivity.java
+++ b/app/src/main/java/org/schabi/newpipe/MainActivity.java
@@ -28,7 +28,6 @@
import android.content.IntentFilter;
import android.content.SharedPreferences;
import android.content.pm.PackageManager;
-import android.os.Build;
import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
@@ -86,7 +85,6 @@
import org.schabi.newpipe.util.SerializedCache;
import org.schabi.newpipe.util.ServiceHelper;
import org.schabi.newpipe.util.StateSaver;
-import org.schabi.newpipe.util.TLSSocketFactoryCompat;
import org.schabi.newpipe.util.ThemeHelper;
import org.schabi.newpipe.views.FocusOverlayView;
@@ -131,11 +129,6 @@ protected void onCreate(final Bundle 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.setTheme(this, ServiceHelper.getSelectedServiceId(this));
@@ -381,8 +374,7 @@ private void toggleServices() {
private void showServices() {
for (final StreamingService s : NewPipe.getServices()) {
- final String title = s.getServiceInfo().getName()
- + (ServiceHelper.isBeta(s) ? " (beta)" : "");
+ final String title = s.getServiceInfo().getName();
final MenuItem menuItem = drawerLayoutBinding.navigation.getMenu()
.add(R.id.menu_services_group, s.getServiceId(), ORDER, title)
@@ -390,7 +382,7 @@ private void showServices() {
// peertube specifics
if (s.getServiceId() == 3) {
- enhancePeertubeMenu(s, menuItem);
+ enhancePeertubeMenu(menuItem);
}
}
drawerLayoutBinding.navigation.getMenu()
@@ -398,9 +390,9 @@ private void showServices() {
.setChecked(true);
}
- private void enhancePeertubeMenu(final StreamingService s, final MenuItem menuItem) {
+ private void enhancePeertubeMenu(final MenuItem menuItem) {
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))
.getRoot();
final List instances = PeertubeHelper.getInstanceList(this);
@@ -480,8 +472,8 @@ protected void onResume() {
ErrorUtil.showUiErrorSnackbar(this, "Setting up service toggle", e);
}
- final SharedPreferences sharedPreferences
- = PreferenceManager.getDefaultSharedPreferences(this);
+ final SharedPreferences sharedPreferences =
+ PreferenceManager.getDefaultSharedPreferences(this);
if (sharedPreferences.getBoolean(Constants.KEY_THEME_CHANGE, false)) {
if (DEBUG) {
Log.d(TAG, "Theme has changed, recreating activity...");
@@ -653,8 +645,8 @@ public boolean onCreateOptionsMenu(final Menu menu) {
}
super.onCreateOptionsMenu(menu);
- final Fragment fragment
- = getSupportFragmentManager().findFragmentById(R.id.fragment_holder);
+ final Fragment fragment =
+ getSupportFragmentManager().findFragmentById(R.id.fragment_holder);
if (!(fragment instanceof SearchFragment)) {
toolbarLayoutBinding.toolbarSearchContainer.getRoot().setVisibility(View.GONE);
}
diff --git a/app/src/main/java/org/schabi/newpipe/PanicResponderActivity.java b/app/src/main/java/org/schabi/newpipe/PanicResponderActivity.java
index b4fbdfb2863..f0d1af81a66 100644
--- a/app/src/main/java/org/schabi/newpipe/PanicResponderActivity.java
+++ b/app/src/main/java/org/schabi/newpipe/PanicResponderActivity.java
@@ -3,7 +3,6 @@
import android.annotation.SuppressLint;
import android.app.Activity;
import android.content.Intent;
-import android.os.Build;
import android.os.Bundle;
/*
@@ -40,10 +39,6 @@ protected void onCreate(final Bundle savedInstanceState) {
ExitActivity.exitAndRemoveFromRecentApps(this);
}
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
- finishAndRemoveTask();
- } else {
- finish();
- }
+ finishAndRemoveTask();
}
}
diff --git a/app/src/main/java/org/schabi/newpipe/QueueItemMenuUtil.java b/app/src/main/java/org/schabi/newpipe/QueueItemMenuUtil.java
index c7604e51283..7c646d0e42a 100644
--- a/app/src/main/java/org/schabi/newpipe/QueueItemMenuUtil.java
+++ b/app/src/main/java/org/schabi/newpipe/QueueItemMenuUtil.java
@@ -16,7 +16,7 @@
import org.schabi.newpipe.util.NavigationHelper;
import org.schabi.newpipe.util.SparseItemUtil;
-import java.util.Collections;
+import java.util.List;
public final class QueueItemMenuUtil {
private QueueItemMenuUtil() {
@@ -53,7 +53,7 @@ public static void openPopupMenu(final PlayQueue playQueue,
case R.id.menu_item_append_playlist:
PlaylistDialog.createCorrespondingDialog(
context,
- Collections.singletonList(new StreamEntity(item)),
+ List.of(new StreamEntity(item)),
dialog -> dialog.show(
fragmentManager,
"QueueItemMenuUtil@append_playlist"
diff --git a/app/src/main/java/org/schabi/newpipe/RouterActivity.java b/app/src/main/java/org/schabi/newpipe/RouterActivity.java
index 1fe6ce7ec7a..936beecff80 100644
--- a/app/src/main/java/org/schabi/newpipe/RouterActivity.java
+++ b/app/src/main/java/org/schabi/newpipe/RouterActivity.java
@@ -30,6 +30,7 @@
import androidx.appcompat.content.res.AppCompatResources;
import androidx.core.app.NotificationCompat;
import androidx.core.app.ServiceCompat;
+import androidx.core.math.MathUtils;
import androidx.fragment.app.FragmentManager;
import androidx.preference.PreferenceManager;
@@ -60,7 +61,7 @@
import org.schabi.newpipe.extractor.stream.StreamInfo;
import org.schabi.newpipe.ktx.ExceptionUtils;
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.PlayerHolder;
import org.schabi.newpipe.player.playqueue.ChannelPlayQueue;
@@ -81,7 +82,6 @@
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Arrays;
-import java.util.Collections;
import java.util.List;
import icepick.Icepick;
@@ -452,7 +452,7 @@ private void showDialog(final List choices) {
}
}
- selectedRadioPosition = Math.min(Math.max(-1, selectedRadioPosition), choices.size() - 1);
+ selectedRadioPosition = MathUtils.clamp(selectedRadioPosition, -1, choices.size() - 1);
if (selectedRadioPosition != -1) {
((RadioButton) radioGroup.getChildAt(selectedRadioPosition)).setChecked(true);
}
@@ -630,8 +630,8 @@ private boolean canHandleChoiceLikeShowInfo(final String selectedChoiceKey) {
}
// ...the player is not running or in normal Video-mode/type
- final MainPlayer.PlayerType playerType = PlayerHolder.getInstance().getType();
- return playerType == null || playerType == MainPlayer.PlayerType.VIDEO;
+ final PlayerType playerType = PlayerHolder.getInstance().getType();
+ return playerType == null || playerType == PlayerType.MAIN;
}
private void openAddToPlaylistDialog() {
@@ -649,7 +649,7 @@ private void openAddToPlaylistDialog() {
.subscribe(
info -> PlaylistDialog.createCorrespondingDialog(
getThemeWrapperContext(),
- Collections.singletonList(new StreamEntity(info)),
+ List.of(new StreamEntity(info)),
playlistDialog -> {
playlistDialog.setOnDismissListener(dialog -> finish());
diff --git a/app/src/main/java/org/schabi/newpipe/about/LicenseFragment.kt b/app/src/main/java/org/schabi/newpipe/about/LicenseFragment.kt
index c816d78be5d..f19ecd74a02 100644
--- a/app/src/main/java/org/schabi/newpipe/about/LicenseFragment.kt
+++ b/app/src/main/java/org/schabi/newpipe/about/LicenseFragment.kt
@@ -8,7 +8,6 @@ import androidx.core.os.bundleOf
import androidx.fragment.app.Fragment
import io.reactivex.rxjava3.disposables.CompositeDisposable
import org.schabi.newpipe.R
-import org.schabi.newpipe.about.LicenseFragmentHelper.showLicense
import org.schabi.newpipe.databinding.FragmentLicensesBinding
import org.schabi.newpipe.databinding.ItemSoftwareComponentBinding
diff --git a/app/src/main/java/org/schabi/newpipe/about/LicenseFragmentHelper.kt b/app/src/main/java/org/schabi/newpipe/about/LicenseFragmentHelper.kt
index c1dd38389c5..6e3aa4be878 100644
--- a/app/src/main/java/org/schabi/newpipe/about/LicenseFragmentHelper.kt
+++ b/app/src/main/java/org/schabi/newpipe/about/LicenseFragmentHelper.kt
@@ -12,126 +12,92 @@ import org.schabi.newpipe.R
import org.schabi.newpipe.util.Localization
import org.schabi.newpipe.util.ThemeHelper
import org.schabi.newpipe.util.external_communication.ShareUtils
-import java.io.BufferedReader
import java.io.IOException
-import java.io.InputStreamReader
-import java.nio.charset.StandardCharsets
-object LicenseFragmentHelper {
- /**
- * @param context the context to use
- * @param license the license
- * @return String which contains a HTML formatted license page
- * styled according to the context's theme
- */
- private fun getFormattedLicense(context: Context, license: License): String {
- val licenseContent = StringBuilder()
- val webViewData: String
- try {
- BufferedReader(
- 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
- webViewData = "$licenseContent".replace(
- "
",
- "
"
- )
- }
- } catch (e: IOException) {
- throw IllegalArgumentException(
- "Could not get license file: " + license.filename, e
- )
- }
- return webViewData
+/**
+ * @param context the context to use
+ * @param license the license
+ * @return String which contains a HTML formatted license page
+ * styled according to the context's theme
+ */
+private fun getFormattedLicense(context: Context, license: License): String {
+ try {
+ return context.assets.open(license.filename).bufferedReader().use { it.readText() }
+ // split the HTML file and insert the stylesheet into the HEAD of the file
+ .replace("
", "
")
+ } catch (e: IOException) {
+ throw IllegalArgumentException("Could not get license file: ${license.filename}", e)
}
+}
- /**
- * @param context the Android context
- * @return String which is a CSS stylesheet according to the context's theme
- */
- private fun getLicenseStylesheet(context: Context): String {
- val isLightTheme = ThemeHelper.isLightThemeSelected(context)
- return (
- "body{padding:12px 15px;margin:0;" + "background:#" + getHexRGBColor(
- context,
- if (isLightTheme) R.color.light_license_background_color
- else R.color.dark_license_background_color
- ) + ";" + "color:#" + getHexRGBColor(
- context,
- if (isLightTheme) R.color.light_license_text_color
- else R.color.dark_license_text_color
- ) + "}" + "a[href]{color:#" + getHexRGBColor(
- context,
- if (isLightTheme) R.color.light_youtube_primary_color
- else R.color.dark_youtube_primary_color
- ) + "}" + "pre{white-space:pre-wrap}"
- )
- }
+/**
+ * @param context the Android context
+ * @return String which is a CSS stylesheet according to the context's theme
+ */
+private fun getLicenseStylesheet(context: Context): String {
+ val isLightTheme = ThemeHelper.isLightThemeSelected(context)
+ val licenseBackgroundColor = getHexRGBColor(
+ context, if (isLightTheme) R.color.light_license_background_color else R.color.dark_license_background_color
+ )
+ val licenseTextColor = getHexRGBColor(
+ context, if (isLightTheme) R.color.light_license_text_color else R.color.dark_license_text_color
+ )
+ val youtubePrimaryColor = getHexRGBColor(
+ context, if (isLightTheme) R.color.light_youtube_primary_color else R.color.dark_youtube_primary_color
+ )
+ return "body{padding:12px 15px;margin:0;background:#$licenseBackgroundColor;color:#$licenseTextColor}" +
+ "a[href]{color:#$youtubePrimaryColor}pre{white-space:pre-wrap}"
+}
- /**
- * Cast R.color to a hexadecimal color value.
- *
- * @param context the context to use
- * @param color the color number from R.color
- * @return a six characters long String with hexadecimal RGB values
- */
- private fun getHexRGBColor(context: Context, color: Int): String {
- return context.getString(color).substring(3)
- }
+/**
+ * Cast R.color to a hexadecimal color value.
+ *
+ * @param context the context to use
+ * @param color the color number from R.color
+ * @return a six characters long String with hexadecimal RGB values
+ */
+private fun getHexRGBColor(context: Context, color: Int): String {
+ return context.getString(color).substring(3)
+}
- fun showLicense(context: Context?, license: License): Disposable {
- return showLicense(context, license) { alertDialog ->
- alertDialog.setPositiveButton(R.string.ok) { dialog, _ ->
- dialog.dismiss()
- }
+fun showLicense(context: Context?, component: SoftwareComponent): Disposable {
+ return showLicense(context, component.license) {
+ setPositiveButton(R.string.dismiss) { dialog, _ ->
+ dialog.dismiss()
}
- }
-
- fun showLicense(context: Context?, component: SoftwareComponent): Disposable {
- return showLicense(context, component.license) { alertDialog ->
- alertDialog.setPositiveButton(R.string.dismiss) { dialog, _ ->
- dialog.dismiss()
- }
- alertDialog.setNeutralButton(R.string.open_website_license) { _, _ ->
- ShareUtils.openUrlInBrowser(context!!, component.link)
- }
+ setNeutralButton(R.string.open_website_license) { _, _ ->
+ ShareUtils.openUrlInBrowser(context!!, component.link)
}
}
+}
- private fun showLicense(
- context: Context?,
- license: License,
- block: (AlertDialog.Builder) -> Unit
- ): Disposable {
- return if (context == null) {
- Disposable.empty()
- } else {
- Observable.fromCallable { getFormattedLicense(context, license) }
- .subscribeOn(Schedulers.io())
- .observeOn(AndroidSchedulers.mainThread())
- .subscribe { formattedLicense ->
- val webViewData = Base64.encodeToString(
- formattedLicense.toByteArray(StandardCharsets.UTF_8), Base64.NO_PADDING
- )
- val webView = WebView(context)
- webView.loadData(webViewData, "text/html; charset=UTF-8", "base64")
+fun showLicense(context: Context?, license: License) = showLicense(context, license) {
+ setPositiveButton(R.string.ok) { dialog, _ -> dialog.dismiss() }
+}
- AlertDialog.Builder(context).apply {
- setTitle(license.name)
- setView(webView)
- Localization.assureCorrectAppLanguage(context)
- block(this)
- show()
- }
- }
- }
+private fun showLicense(
+ context: Context?,
+ license: License,
+ block: AlertDialog.Builder.() -> AlertDialog.Builder
+): Disposable {
+ return if (context == null) {
+ Disposable.empty()
+ } else {
+ Observable.fromCallable { getFormattedLicense(context, license) }
+ .subscribeOn(Schedulers.io())
+ .observeOn(AndroidSchedulers.mainThread())
+ .subscribe { formattedLicense ->
+ val webViewData =
+ Base64.encodeToString(formattedLicense.toByteArray(), Base64.NO_PADDING)
+ val webView = WebView(context)
+ webView.loadData(webViewData, "text/html; charset=UTF-8", "base64")
+
+ Localization.assureCorrectAppLanguage(context)
+ AlertDialog.Builder(context)
+ .setTitle(license.name)
+ .setView(webView)
+ .block()
+ .show()
+ }
}
}
diff --git a/app/src/main/java/org/schabi/newpipe/database/BasicDAO.java b/app/src/main/java/org/schabi/newpipe/database/BasicDAO.java
index 1b8540808c0..255f5ba8deb 100644
--- a/app/src/main/java/org/schabi/newpipe/database/BasicDAO.java
+++ b/app/src/main/java/org/schabi/newpipe/database/BasicDAO.java
@@ -3,7 +3,6 @@
import androidx.room.Dao;
import androidx.room.Delete;
import androidx.room.Insert;
-import androidx.room.OnConflictStrategy;
import androidx.room.Update;
import java.util.Collection;
@@ -14,13 +13,10 @@
@Dao
public interface BasicDAO {
/* Inserts */
- @Insert(onConflict = OnConflictStrategy.ABORT)
+ @Insert
long insert(Entity entity);
- @Insert(onConflict = OnConflictStrategy.ABORT)
- List insertAll(Entity... entities);
-
- @Insert(onConflict = OnConflictStrategy.ABORT)
+ @Insert
List insertAll(Collection entities);
/* Searches */
@@ -32,9 +28,6 @@ public interface BasicDAO {
@Delete
void delete(Entity entity);
- @Delete
- int delete(Collection entities);
-
int deleteAll();
/* Updates */
diff --git a/app/src/main/java/org/schabi/newpipe/database/feed/dao/FeedDAO.kt b/app/src/main/java/org/schabi/newpipe/database/feed/dao/FeedDAO.kt
index d573788a65e..b2b3d18a667 100644
--- a/app/src/main/java/org/schabi/newpipe/database/feed/dao/FeedDAO.kt
+++ b/app/src/main/java/org/schabi/newpipe/database/feed/dao/FeedDAO.kt
@@ -9,6 +9,7 @@ import androidx.room.Update
import io.reactivex.rxjava3.core.Flowable
import io.reactivex.rxjava3.core.Maybe
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.stream.StreamWithState
import org.schabi.newpipe.database.stream.model.StreamStateEntity
@@ -21,92 +22,16 @@ abstract class FeedDAO {
@Query("DELETE FROM feed")
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>
-
- @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>
-
- /**
- * @see StreamStateEntity.isFinished()
- * @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(
- """
- 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
-
- WHERE (
- sh.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 * 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>
-
/**
+ * @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.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(
"""
@@ -122,24 +47,37 @@ abstract class FeedDAO {
INNER JOIN feed f
ON s.uid = f.stream_id
- INNER JOIN feed_group_subscription_join fgs
+ LEFT JOIN feed_group_subscription_join fgs
ON fgs.subscription_id = f.subscription_id
- WHERE fgs.group_id = :groupId
+ WHERE (
+ :groupId = ${FeedGroupEntity.GROUP_ALL_ID}
+ OR fgs.group_id = :groupId
+ )
AND (
- sh.stream_id IS NULL
+ :includePlayed
+ OR sh.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 * 3 / 4
OR s.stream_type = '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
LIMIT 500
"""
)
- abstract fun getLiveOrNotPlayedStreamsForGroup(groupId: Long): Maybe>
+ abstract fun getStreams(
+ groupId: Long,
+ includePlayed: Boolean,
+ uploadDateBefore: OffsetDateTime?
+ ): Maybe>
@Query(
"""
diff --git a/app/src/main/java/org/schabi/newpipe/database/playlist/PlaylistLocalItem.java b/app/src/main/java/org/schabi/newpipe/database/playlist/PlaylistLocalItem.java
index 43dbd89ea46..695f9ec5a69 100644
--- a/app/src/main/java/org/schabi/newpipe/database/playlist/PlaylistLocalItem.java
+++ b/app/src/main/java/org/schabi/newpipe/database/playlist/PlaylistLocalItem.java
@@ -3,10 +3,10 @@
import org.schabi.newpipe.database.LocalItem;
import org.schabi.newpipe.database.playlist.model.PlaylistRemoteEntity;
-import java.util.ArrayList;
-import java.util.Collections;
import java.util.Comparator;
import java.util.List;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
public interface PlaylistLocalItem extends LocalItem {
String getOrderingName();
@@ -14,14 +14,9 @@ public interface PlaylistLocalItem extends LocalItem {
static List merge(
final List localPlaylists,
final List remotePlaylists) {
- final List items = new ArrayList<>(
- localPlaylists.size() + remotePlaylists.size());
- items.addAll(localPlaylists);
- items.addAll(remotePlaylists);
-
- Collections.sort(items, Comparator.comparing(PlaylistLocalItem::getOrderingName,
- Comparator.nullsLast(String.CASE_INSENSITIVE_ORDER)));
-
- return items;
+ return Stream.concat(localPlaylists.stream(), remotePlaylists.stream())
+ .sorted(Comparator.comparing(PlaylistLocalItem::getOrderingName,
+ Comparator.nullsLast(String.CASE_INSENSITIVE_ORDER)))
+ .collect(Collectors.toList());
}
}
diff --git a/app/src/main/java/org/schabi/newpipe/download/DownloadDialog.java b/app/src/main/java/org/schabi/newpipe/download/DownloadDialog.java
index e4adddc2a44..0e64e8b4846 100644
--- a/app/src/main/java/org/schabi/newpipe/download/DownloadDialog.java
+++ b/app/src/main/java/org/schabi/newpipe/download/DownloadDialog.java
@@ -1,5 +1,9 @@
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.content.ComponentName;
import android.content.Context;
@@ -82,10 +86,6 @@
import us.shandian.giga.service.DownloadManagerService.DownloadManagerBinder;
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
implements RadioGroup.OnCheckedChangeListener, AdapterView.OnItemSelectedListener {
private static final String TAG = "DialogFragment";
@@ -205,8 +205,8 @@ public void onCreate(@Nullable final Bundle savedInstanceState) {
setStyle(STYLE_NO_TITLE, ThemeHelper.getDialogTheme(context));
Icepick.restoreInstanceState(this, savedInstanceState);
- final SparseArray> secondaryStreams
- = new SparseArray<>(4);
+ final SparseArray> secondaryStreams =
+ new SparseArray<>(4);
final List videoStreams = wrappedVideoStreams.getStreamsList();
for (int i = 0; i < videoStreams.size(); i++) {
diff --git a/app/src/main/java/org/schabi/newpipe/error/ErrorActivity.java b/app/src/main/java/org/schabi/newpipe/error/ErrorActivity.java
index bd843029687..e1dd929d4d4 100644
--- a/app/src/main/java/org/schabi/newpipe/error/ErrorActivity.java
+++ b/app/src/main/java/org/schabi/newpipe/error/ErrorActivity.java
@@ -31,6 +31,7 @@
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.Arrays;
+import java.util.stream.Collectors;
/*
* 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_SUBJECT = "Exception in ";
- public static final String ERROR_GITHUB_ISSUE_URL
- = "https://github.com/TeamNewPipe/NewPipe/issues";
+ public static final String ERROR_GITHUB_ISSUE_URL =
+ "https://github.com/TeamNewPipe/NewPipe/issues";
- public static final DateTimeFormatter CURRENT_TIMESTAMP_FORMATTER
- = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm");
+ public static final DateTimeFormatter CURRENT_TIMESTAMP_FORMATTER =
+ DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm");
private ErrorInfo errorInfo;
@@ -182,14 +183,9 @@ private void openPrivacyPolicyDialog(final Context context, final String action)
}
private String formErrorText(final String[] el) {
- final StringBuilder text = new StringBuilder();
- if (el != null) {
- for (final String e : el) {
- text.append("-------------------------------------\n").append(e);
- }
- }
- text.append("-------------------------------------");
- return text.toString();
+ final String separator = "-------------------------------------";
+ return Arrays.stream(el)
+ .collect(Collectors.joining(separator + "\n", separator + "\n", separator));
}
/**
diff --git a/app/src/main/java/org/schabi/newpipe/error/ErrorInfo.kt b/app/src/main/java/org/schabi/newpipe/error/ErrorInfo.kt
index f9f9f003aa0..d87fa333060 100644
--- a/app/src/main/java/org/schabi/newpipe/error/ErrorInfo.kt
+++ b/app/src/main/java/org/schabi/newpipe/error/ErrorInfo.kt
@@ -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.ktx.isNetworkRelated
import org.schabi.newpipe.util.ServiceHelper
-import java.io.PrintWriter
-import java.io.StringWriter
@Parcelize
class ErrorInfo(
@@ -80,19 +78,10 @@ class ErrorInfo(
companion object {
const val SERVICE_NONE = "none"
- private fun getStackTrace(throwable: Throwable): String {
- StringWriter().use { stringWriter ->
- PrintWriter(stringWriter, true).use { printWriter ->
- throwable.printStackTrace(printWriter)
- return stringWriter.buffer.toString()
- }
- }
- }
-
- fun throwableToStringList(throwable: Throwable) = arrayOf(getStackTrace(throwable))
+ fun throwableToStringList(throwable: Throwable) = arrayOf(throwable.stackTraceToString())
- fun throwableListToStringList(throwable: List) =
- Array(throwable.size) { i -> getStackTrace(throwable[i]) }
+ fun throwableListToStringList(throwableList: List) =
+ throwableList.map { it.stackTraceToString() }.toTypedArray()
private fun getInfoServiceName(info: Info?) =
if (info == null) SERVICE_NONE else ServiceHelper.getNameOfServiceById(info.serviceId)
diff --git a/app/src/main/java/org/schabi/newpipe/error/ErrorUtil.kt b/app/src/main/java/org/schabi/newpipe/error/ErrorUtil.kt
index e4dd2e16d03..86e2e1028a4 100644
--- a/app/src/main/java/org/schabi/newpipe/error/ErrorUtil.kt
+++ b/app/src/main/java/org/schabi/newpipe/error/ErrorUtil.kt
@@ -114,13 +114,7 @@ class ErrorUtil {
context,
context.getString(R.string.error_report_channel_id)
)
- .setSmallIcon(
- // 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
- )
+ .setSmallIcon(R.drawable.ic_bug_report)
.setContentTitle(context.getString(R.string.error_report_notification_title))
.setContentText(context.getString(errorInfo.messageStringId))
.setAutoCancel(true)
diff --git a/app/src/main/java/org/schabi/newpipe/error/ReCaptchaActivity.java b/app/src/main/java/org/schabi/newpipe/error/ReCaptchaActivity.java
index 555dd709baa..e2780d215cb 100644
--- a/app/src/main/java/org/schabi/newpipe/error/ReCaptchaActivity.java
+++ b/app/src/main/java/org/schabi/newpipe/error/ReCaptchaActivity.java
@@ -3,14 +3,15 @@
import android.annotation.SuppressLint;
import android.content.Intent;
import android.content.SharedPreferences;
-import android.os.Build;
import android.os.Bundle;
import android.util.Log;
import android.view.Menu;
import android.view.MenuItem;
import android.webkit.CookieManager;
+import android.webkit.WebResourceRequest;
import android.webkit.WebSettings;
import android.webkit.WebView;
+import android.webkit.WebViewClient;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
@@ -18,7 +19,6 @@
import androidx.appcompat.app.AppCompatActivity;
import androidx.core.app.NavUtils;
import androidx.preference.PreferenceManager;
-import androidx.webkit.WebViewClientCompat;
import org.schabi.newpipe.databinding.ActivityRecaptchaBinding;
import org.schabi.newpipe.DownloaderImpl;
@@ -86,14 +86,15 @@ protected void onCreate(final Bundle savedInstanceState) {
webSettings.setJavaScriptEnabled(true);
webSettings.setUserAgentString(DownloaderImpl.USER_AGENT);
- recaptchaBinding.reCaptchaWebView.setWebViewClient(new WebViewClientCompat() {
+ recaptchaBinding.reCaptchaWebView.setWebViewClient(new WebViewClient() {
@Override
- public boolean shouldOverrideUrlLoading(final WebView view, final String url) {
+ public boolean shouldOverrideUrlLoading(final WebView view,
+ final WebResourceRequest request) {
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;
}
@@ -107,12 +108,7 @@ public void onPageFinished(final WebView view, final String url) {
// cleaning cache, history and cookies from webView
recaptchaBinding.reCaptchaWebView.clearCache(true);
recaptchaBinding.reCaptchaWebView.clearHistory();
- final CookieManager cookieManager = CookieManager.getInstance();
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
- cookieManager.removeAllCookies(value -> { });
- } else {
- cookieManager.removeAllCookie();
- }
+ CookieManager.getInstance().removeAllCookies(null);
recaptchaBinding.reCaptchaWebView.loadUrl(url);
}
diff --git a/app/src/main/java/org/schabi/newpipe/fragments/detail/DescriptionFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/detail/DescriptionFragment.java
index d57ddb02df6..bf7f8fa5d97 100644
--- a/app/src/main/java/org/schabi/newpipe/fragments/detail/DescriptionFragment.java
+++ b/app/src/main/java/org/schabi/newpipe/fragments/detail/DescriptionFragment.java
@@ -1,5 +1,9 @@
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.view.LayoutInflater;
import android.view.View;
@@ -26,17 +30,9 @@
import org.schabi.newpipe.util.external_communication.ShareUtils;
import org.schabi.newpipe.util.external_communication.TextLinkifier;
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.List;
-
import icepick.State;
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 {
@State
@@ -185,8 +181,8 @@ private void addMetadataItem(final LayoutInflater inflater,
return;
}
- final ItemMetadataBinding itemBinding
- = ItemMetadataBinding.inflate(inflater, layout, false);
+ final ItemMetadataBinding itemBinding =
+ ItemMetadataBinding.inflate(inflater, layout, false);
itemBinding.metadataTypeView.setText(type);
itemBinding.metadataTypeView.setOnLongClickListener(v -> {
@@ -206,19 +202,16 @@ private void addMetadataItem(final LayoutInflater inflater,
private void addTagsMetadataItem(final LayoutInflater inflater, final LinearLayout layout) {
if (streamInfo.getTags() != null && !streamInfo.getTags().isEmpty()) {
- final ItemMetadataTagsBinding itemBinding
- = ItemMetadataTagsBinding.inflate(inflater, layout, false);
+ final var itemBinding = ItemMetadataTagsBinding.inflate(inflater, layout, false);
- final List tags = new ArrayList<>(streamInfo.getTags());
- Collections.sort(tags);
- for (final String tag : tags) {
+ streamInfo.getTags().stream().sorted(String.CASE_INSENSITIVE_ORDER).forEach(tag -> {
final Chip chip = (Chip) inflater.inflate(R.layout.chip,
itemBinding.metadataTagsChips, false);
chip.setText(tag);
chip.setOnClickListener(this::onTagClick);
chip.setOnLongClickListener(this::onTagLongClick);
itemBinding.metadataTagsChips.addView(chip);
- }
+ });
layout.addView(itemBinding.getRoot());
}
diff --git a/app/src/main/java/org/schabi/newpipe/fragments/detail/VideoDetailFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/detail/VideoDetailFragment.java
index 5e19f558d74..09e0857910b 100644
--- a/app/src/main/java/org/schabi/newpipe/fragments/detail/VideoDetailFragment.java
+++ b/app/src/main/java/org/schabi/newpipe/fragments/detail/VideoDetailFragment.java
@@ -1,5 +1,16 @@
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.app.Activity;
import android.content.BroadcastReceiver;
@@ -10,7 +21,6 @@
import android.content.pm.ActivityInfo;
import android.database.ContentObserver;
import android.graphics.Color;
-import android.graphics.Point;
import android.graphics.Rect;
import android.graphics.drawable.Drawable;
import android.net.Uri;
@@ -77,9 +87,9 @@
import org.schabi.newpipe.ktx.AnimationType;
import org.schabi.newpipe.local.dialog.PlaylistDialog;
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.PlayerService;
+import org.schabi.newpipe.player.PlayerType;
import org.schabi.newpipe.player.event.OnKeyDownListener;
import org.schabi.newpipe.player.event.PlayerServiceExtendedEventListener;
import org.schabi.newpipe.player.helper.PlayerHelper;
@@ -87,6 +97,8 @@
import org.schabi.newpipe.player.playqueue.PlayQueue;
import org.schabi.newpipe.player.playqueue.PlayQueueItem;
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.DeviceUtils;
import org.schabi.newpipe.util.ExtractorHelper;
@@ -101,11 +113,11 @@
import org.schabi.newpipe.util.external_communication.ShareUtils;
import java.util.ArrayList;
-import java.util.Collections;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Objects;
+import java.util.Optional;
import java.util.concurrent.TimeUnit;
import icepick.State;
@@ -114,17 +126,6 @@
import io.reactivex.rxjava3.disposables.Disposable;
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
extends BaseStateFragment
implements BackPressable,
@@ -179,6 +180,8 @@ public final class VideoDetailFragment
@State
int bottomSheetState = BottomSheetBehavior.STATE_EXPANDED;
@State
+ int lastStableBottomSheetState = BottomSheetBehavior.STATE_EXPANDED;
+ @State
protected boolean autoPlayEnabled = true;
@Nullable
@@ -190,6 +193,7 @@ public final class VideoDetailFragment
private Disposable positionSubscriber = null;
private BottomSheetBehavior bottomSheetBehavior;
+ private BottomSheetBehavior.BottomSheetCallback bottomSheetCallback;
private BroadcastReceiver broadcastReceiver;
/*//////////////////////////////////////////////////////////////////////////
@@ -202,7 +206,7 @@ public final class VideoDetailFragment
private ContentObserver settingsContentObserver;
@Nullable
- private MainPlayer playerService;
+ private PlayerService playerService;
private Player player;
private final PlayerHolder playerHolder = PlayerHolder.getInstance();
@@ -211,7 +215,7 @@ public final class VideoDetailFragment
//////////////////////////////////////////////////////////////////////////*/
@Override
public void onServiceConnected(final Player connectedPlayer,
- final MainPlayer connectedPlayerService,
+ final PlayerService connectedPlayerService,
final boolean playAfterConnect) {
player = connectedPlayer;
playerService = connectedPlayerService;
@@ -219,6 +223,7 @@ public void onServiceConnected(final Player connectedPlayer,
// It will do nothing if the player is not in fullscreen mode
hideSystemUiIfNeeded();
+ final Optional playerUi = player.UIs().get(MainPlayerUi.class);
if (!player.videoPlayerSelected() && !playAfterConnect) {
return;
}
@@ -227,22 +232,19 @@ public void onServiceConnected(final Player connectedPlayer,
// If the video is playing but orientation changed
// let's make the video in fullscreen again
checkLandscape();
- } else if (player.isFullscreen() && !player.isVerticalVideo()
+ } else if (playerUi.map(ui -> ui.isFullscreen() && !ui.isVerticalVideo()).orElse(false)
// Tablet UI has orientation-independent fullscreen
&& !DeviceUtils.isTablet(activity)) {
// Device is in portrait orientation after rotation but UI is in fullscreen.
// Return back to non-fullscreen state
- player.toggleFullscreen();
- }
-
- if (playerIsNotStopped() && player.videoPlayerSelected()) {
- addVideoPlayerView();
+ playerUi.ifPresent(MainPlayerUi::toggleFullscreen);
}
+ //noinspection SimplifyOptionalCallChains
if (playAfterConnect
|| (currentInfo != null
&& isAutoplayEnabled()
- && player.getParentActivity() == null)) {
+ && !playerUi.isPresent())) {
autoPlayEnabled = true; // forcefully start playing
openVideoPlayerAutoFullscreen();
}
@@ -269,7 +271,7 @@ public static VideoDetailFragment getInstance(final int serviceId,
public static VideoDetailFragment getInstanceInCollapsedState() {
final VideoDetailFragment instance = new VideoDetailFragment();
- instance.bottomSheetState = BottomSheetBehavior.STATE_COLLAPSED;
+ instance.updateBottomSheetState(BottomSheetBehavior.STATE_COLLAPSED);
return instance;
}
@@ -329,6 +331,9 @@ public void onPause() {
@Override
public void onResume() {
super.onResume();
+ if (DEBUG) {
+ Log.d(TAG, "onResume() called");
+ }
activity.sendBroadcast(new Intent(ACTION_VIDEO_FRAGMENT_RESUMED));
@@ -383,7 +388,7 @@ public void onDestroy() {
disposables.clear();
positionSubscriber = null;
currentWorker = null;
- bottomSheetBehavior.setBottomSheetCallback(null);
+ bottomSheetBehavior.removeBottomSheetCallback(bottomSheetCallback);
if (activity.isFinishing()) {
playQueue = null;
@@ -449,7 +454,7 @@ public void onClick(final View v) {
disposables.add(
PlaylistDialog.createCorrespondingDialog(
getContext(),
- Collections.singletonList(new StreamEntity(currentInfo)),
+ List.of(new StreamEntity(currentInfo)),
dialog -> dialog.show(getFM(), TAG)
)
);
@@ -500,12 +505,18 @@ public void onClick(final View v) {
}
break;
case R.id.detail_thumbnail_root_layout:
- autoPlayEnabled = true; // forcefully start playing
- // FIXME Workaround #7427
- if (isPlayerAvailable()) {
- player.setRecovery();
+ // 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
+ // FIXME Workaround #7427
+ if (isPlayerAvailable()) {
+ player.setRecovery();
+ }
+ openVideoPlayerAutoFullscreen();
}
- openVideoPlayerAutoFullscreen();
break;
case R.id.detail_title_root_layout:
toggleTitleAndSecondaryControls();
@@ -518,7 +529,7 @@ public void onClick(final View v) {
case R.id.overlay_play_pause_button:
if (playerIsNotStopped()) {
player.playPause();
- player.hideControls(0, 0);
+ player.UIs().get(VideoPlayerUi.class).ifPresent(ui -> ui.hideControls(0, 0));
showSystemUi();
} else {
autoPlayEnabled = true; // forcefully start playing
@@ -583,12 +594,12 @@ private void toggleTitleAndSecondaryControls() {
if (binding.detailSecondaryControlPanel.getVisibility() == View.GONE) {
binding.detailVideoTitleView.setMaxLines(10);
animateRotation(binding.detailToggleSecondaryControlsView,
- Player.DEFAULT_CONTROLS_DURATION, 180);
+ VideoPlayerUi.DEFAULT_CONTROLS_DURATION, 180);
binding.detailSecondaryControlPanel.setVisibility(View.VISIBLE);
} else {
binding.detailVideoTitleView.setMaxLines(1);
animateRotation(binding.detailToggleSecondaryControlsView,
- Player.DEFAULT_CONTROLS_DURATION, 0);
+ VideoPlayerUi.DEFAULT_CONTROLS_DURATION, 0);
binding.detailSecondaryControlPanel.setVisibility(View.GONE);
}
// view pager height has changed, update the tab layout
@@ -714,7 +725,7 @@ private View.OnTouchListener getOnControlsTouchListener() {
}
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() {
@Override
public void onSuccess() {
@@ -746,7 +757,9 @@ public void onError(final Exception e) {
@Override
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
@@ -756,7 +769,7 @@ public boolean onBackPressed() {
}
// If we are in fullscreen mode just exit from it via first back press
- if (isPlayerAvailable() && player.isFullscreen()) {
+ if (isFullscreen()) {
if (!DeviceUtils.isTablet(activity)) {
player.pause();
}
@@ -1006,8 +1019,7 @@ private void updateTabs(@NonNull final StreamInfo info) {
getChildFragmentManager().beginTransaction()
.replace(R.id.relatedItemsLayout, RelatedItemsFragment.getInstance(info))
.commitAllowingStateLoss();
- binding.relatedItemsLayout.setVisibility(
- isPlayerAvailable() && player.isFullscreen() ? View.GONE : View.VISIBLE);
+ binding.relatedItemsLayout.setVisibility(isFullscreen() ? View.GONE : View.VISIBLE);
}
}
@@ -1047,15 +1059,13 @@ public void updateTabLayoutVisibility() {
// call `post()` to be sure `viewPager.getHitRect()`
// is up to date and not being currently recomputed
binding.tabLayout.post(() -> {
- if (getContext() != null) {
+ final var activity = getActivity();
+ if (activity != null) {
final Rect pagerHitRect = new Rect();
binding.viewPager.getHitRect(pagerHitRect);
- final Point displaySize = new Point();
- Objects.requireNonNull(ContextCompat.getSystemService(getContext(),
- WindowManager.class)).getDefaultDisplay().getSize(displaySize);
-
- final int viewPagerVisibleHeight = displaySize.y - pagerHitRect.top;
+ final int height = DeviceUtils.getWindowHeight(activity.getWindowManager());
+ final int viewPagerVisibleHeight = height - pagerHitRect.top;
// see TabLayout.DEFAULT_HEIGHT, which is equal to 48dp
final float tabLayoutHeight = TypedValue.applyDimension(
TypedValue.COMPLEX_UNIT_DIP, 48, getResources().getDisplayMetrics());
@@ -1087,8 +1097,12 @@ public void scrollToTop() {
private void toggleFullscreenIfInFullscreenMode() {
// If a user watched video inside fullscreen mode and than chose another player
// return to non-fullscreen mode
- if (isPlayerAvailable() && player.isFullscreen()) {
- player.toggleFullscreen();
+ if (isPlayerAvailable()) {
+ player.UIs().get(MainPlayerUi.class).ifPresent(playerUi -> {
+ if (playerUi.isFullscreen()) {
+ playerUi.toggleFullscreen();
+ }
+ });
}
}
@@ -1164,7 +1178,7 @@ public void openVideoPlayer(final boolean directlyFullscreenIfApplicable) {
// doesn't tell which state it was settling to, and thus the bottom sheet settles to
// STATE_COLLAPSED. This can be solved by manually setting the state that will be
// restored (i.e. bottomSheetState) to STATE_EXPANDED.
- bottomSheetState = BottomSheetBehavior.STATE_EXPANDED;
+ updateBottomSheetState(BottomSheetBehavior.STATE_EXPANDED);
// toggle landscape in order to open directly in fullscreen
onScreenRotationButtonClicked();
}
@@ -1214,16 +1228,10 @@ private void openMainPlayer() {
}
final PlayQueue queue = setupPlayQueueForIntent(false);
-
- // 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();
+ tryAddVideoPlayerView();
final Intent playerIntent = NavigationHelper.getPlayerIntent(requireContext(),
- MainPlayer.class, queue, true, autoPlayEnabled);
+ PlayerService.class, queue, true, autoPlayEnabled);
ContextCompat.startForegroundService(activity, playerIntent);
}
@@ -1235,8 +1243,8 @@ private void openMainPlayer() {
* be reused in a few milliseconds and the flickering would be annoying.
*/
private void hideMainPlayerOnLoadingNewStream() {
- if (!isPlayerServiceAvailable()
- || playerService.getView() == null
+ //noinspection SimplifyOptionalCallChains
+ if (!isPlayerServiceAvailable() || !getRoot().isPresent()
|| !player.videoPlayerSelected()) {
return;
}
@@ -1244,7 +1252,7 @@ private void hideMainPlayerOnLoadingNewStream() {
removeVideoPlayerView();
if (isAutoplayEnabled()) {
playerService.stopForImmediateReusing();
- playerService.getView().setVisibility(View.GONE);
+ getRoot().ifPresent(view -> view.setVisibility(View.GONE));
} else {
playerHolder.stopService();
}
@@ -1301,27 +1309,41 @@ private boolean isAutoplayEnabled() {
&& PlayerHelper.isAutoplayAllowedByUser(requireContext());
}
- private void addVideoPlayerView() {
- if (!isPlayerAvailable() || getView() == null) {
- return;
+ 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();
}
- // Check if viewHolder already contains a child
- if (player.getRootView().getParent() != binding.playerPlaceholder) {
- playerService.removeViewFromParent();
- }
- 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) {
+ return;
+ }
- // Prevent from re-adding a view multiple times
- if (player.getRootView().getParent() == null) {
- binding.playerPlaceholder.addView(player.getRootView());
- }
+ // setup the surface view height, so that it fits the video correctly
+ setHeightThumbnail();
+
+ player.UIs().get(MainPlayerUi.class).ifPresent(playerUi -> {
+ // sometimes binding would be null here, even though getView() != null above u.u
+ if (binding != null) {
+ // prevent from re-adding a view multiple times
+ playerUi.removeViewFromParent();
+ binding.playerPlaceholder.addView(playerUi.getBinding().getRoot());
+ playerUi.setupVideoSurfaceIfNeeded();
+ }
+ });
+ });
}
private void removeVideoPlayerView() {
makeDefaultHeightForVideoPlaceholder();
- playerService.removeViewFromParent();
+ if (player != null) {
+ player.UIs().get(VideoPlayerUi.class).ifPresent(VideoPlayerUi::removeViewFromParent);
+ }
}
private void makeDefaultHeightForVideoPlaceholder() {
@@ -1362,7 +1384,7 @@ private void setHeightThumbnail() {
final boolean isPortrait = metrics.heightPixels > metrics.widthPixels;
requireView().getViewTreeObserver().removeOnPreDrawListener(preDrawListener);
- if (isPlayerAvailable() && player.isFullscreen()) {
+ if (isFullscreen()) {
final int height = (DeviceUtils.isInMultiWindow(activity)
? requireView()
: activity.getWindow().getDecorView()).getHeight();
@@ -1387,8 +1409,9 @@ private void setHeightThumbnail(final int newHeight, final DisplayMetrics metric
binding.detailThumbnailImageView.setMinimumHeight(newHeight);
if (isPlayerAvailable()) {
final int maxHeight = (int) (metrics.heightPixels * MAX_PLAYER_HEIGHT);
- player.getSurfaceView()
- .setHeights(newHeight, player.isFullscreen() ? newHeight : maxHeight);
+ player.UIs().get(VideoPlayerUi.class).ifPresent(ui ->
+ ui.getBinding().surfaceView.setHeights(newHeight,
+ ui.isFullscreen() ? newHeight : maxHeight));
}
}
@@ -1517,7 +1540,7 @@ public void showLoading() {
if (binding.relatedItemsLayout != null) {
if (showRelatedItems) {
binding.relatedItemsLayout.setVisibility(
- isPlayerAvailable() && player.isFullscreen() ? View.GONE : View.INVISIBLE);
+ isFullscreen() ? View.GONE : View.INVISIBLE);
} else {
binding.relatedItemsLayout.setVisibility(View.GONE);
}
@@ -1551,7 +1574,8 @@ public void handleResult(@NonNull final StreamInfo info) {
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.detailUploaderThumbnailView.setImageDrawable(buddyDrawable);
@@ -1778,6 +1802,11 @@ private void showPlaybackProgress(final long progress, final long duration) {
// Player event listener
//////////////////////////////////////////////////////////////////////////*/
+ @Override
+ public void onViewCreated() {
+ tryAddVideoPlayerView();
+ }
+
@Override
public void onQueueUpdate(final PlayQueue queue) {
playQueue = queue;
@@ -1898,15 +1927,10 @@ public void onServiceStopped() {
@Override
public void onFullscreenStateChanged(final boolean fullscreen) {
setupBrightness();
+ //noinspection SimplifyOptionalCallChains
if (!isPlayerAndPlayerServiceAvailable()
- || playerService.getView() == null
- || player.getParentActivity() == null) {
- return;
- }
-
- final View view = playerService.getView();
- final ViewGroup parent = (ViewGroup) view.getParent();
- if (parent == null) {
+ || !player.UIs().get(MainPlayerUi.class).isPresent()
+ || getRoot().map(View::getParent).orElse(null) == null) {
return;
}
@@ -1922,13 +1946,7 @@ public void onFullscreenStateChanged(final boolean fullscreen) {
}
scrollToTop();
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
- addVideoPlayerView();
- } else {
- // KitKat needs a delay before addVideoPlayerView call or it reports wrong height in
- // activity.getWindow().getDecorView().getHeight()
- new Handler().post(this::addVideoPlayerView);
- }
+ tryAddVideoPlayerView();
}
@Override
@@ -1940,7 +1958,7 @@ public void onScreenRotationButtonClicked() {
final boolean isLandscape = DeviceUtils.isLandscape(requireContext());
if (DeviceUtils.isTablet(activity)
&& (!globalScreenOrientationLocked(activity) || isLandscape)) {
- player.toggleFullscreen();
+ player.UIs().get(MainPlayerUi.class).ifPresent(MainPlayerUi::toggleFullscreen);
return;
}
@@ -1991,10 +2009,8 @@ private void showSystemUi() {
}
activity.getWindow().getDecorView().setSystemUiVisibility(0);
activity.getWindow().clearFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN);
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
- activity.getWindow().setStatusBarColor(ThemeHelper.resolveColorFromAttr(
- requireContext(), android.R.attr.colorPrimary));
- }
+ activity.getWindow().setStatusBarColor(ThemeHelper.resolveColorFromAttr(
+ requireContext(), android.R.attr.colorPrimary));
}
private void hideSystemUi() {
@@ -2025,8 +2041,7 @@ private void hideSystemUi() {
}
activity.getWindow().getDecorView().setSystemUiVisibility(visibility);
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP
- && (isInMultiWindow || (isPlayerAvailable() && player.isFullscreen()))) {
+ if (isInMultiWindow || isFullscreen()) {
activity.getWindow().setStatusBarColor(Color.TRANSPARENT);
activity.getWindow().setNavigationBarColor(Color.TRANSPARENT);
}
@@ -2034,14 +2049,19 @@ private void hideSystemUi() {
}
// Listener implementation
+ @Override
public void hideSystemUiIfNeeded() {
- if (isPlayerAvailable()
- && player.isFullscreen()
+ if (isFullscreen()
&& bottomSheetBehavior.getState() == BottomSheetBehavior.STATE_EXPANDED) {
hideSystemUi();
}
}
+ private boolean isFullscreen() {
+ return isPlayerAvailable() && player.UIs().get(VideoPlayerUi.class)
+ .map(VideoPlayerUi::isFullscreen).orElse(false);
+ }
+
private boolean playerIsNotStopped() {
return isPlayerAvailable() && !player.isStopped();
}
@@ -2064,10 +2084,7 @@ private void setupBrightness() {
}
final WindowManager.LayoutParams lp = activity.getWindow().getAttributes();
- if (!isPlayerAvailable()
- || !player.videoPlayerSelected()
- || !player.isFullscreen()
- || bottomSheetState != BottomSheetBehavior.STATE_EXPANDED) {
+ if (!isFullscreen() || bottomSheetState != BottomSheetBehavior.STATE_EXPANDED) {
// Apply system brightness when the player is not in fullscreen
restoreDefaultBrightness();
} else {
@@ -2091,7 +2108,7 @@ private void checkLandscape() {
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
if (globalScreenOrientationLocked(activity) && !player.isPlaying()) {
player.play();
@@ -2170,12 +2187,8 @@ private void showExternalPlaybackDialog() {
} else {
final int selectedVideoStreamIndexForExternalPlayers =
ListHelper.getDefaultResolutionIndex(activity, videoStreamsForExternalPlayers);
- final CharSequence[] resolutions =
- new CharSequence[videoStreamsForExternalPlayers.size()];
-
- for (int i = 0; i < videoStreamsForExternalPlayers.size(); i++) {
- resolutions[i] = videoStreamsForExternalPlayers.get(i).getResolution();
- }
+ final CharSequence[] resolutions = videoStreamsForExternalPlayers.stream()
+ .map(VideoStream::getResolution).toArray(CharSequence[]::new);
builder.setSingleChoiceItems(resolutions, selectedVideoStreamIndexForExternalPlayers,
null);
@@ -2279,7 +2292,9 @@ private void setupBottomPlayer() {
final FrameLayout bottomSheetLayout = activity.findViewById(R.id.fragment_player_holder);
bottomSheetBehavior = BottomSheetBehavior.from(bottomSheetLayout);
- bottomSheetBehavior.setState(bottomSheetState);
+ bottomSheetBehavior.setState(lastStableBottomSheetState);
+ updateBottomSheetState(lastStableBottomSheetState);
+
final int peekHeight = getResources().getDimensionPixelSize(R.dimen.mini_player_height);
if (bottomSheetState != BottomSheetBehavior.STATE_HIDDEN) {
manageSpaceAtTheBottom(false);
@@ -2292,10 +2307,10 @@ private void setupBottomPlayer() {
}
}
- bottomSheetBehavior.setBottomSheetCallback(new BottomSheetBehavior.BottomSheetCallback() {
+ bottomSheetCallback = new BottomSheetBehavior.BottomSheetCallback() {
@Override
public void onStateChanged(@NonNull final View bottomSheet, final int newState) {
- bottomSheetState = newState;
+ updateBottomSheetState(newState);
switch (newState) {
case BottomSheetBehavior.STATE_HIDDEN:
@@ -2318,10 +2333,10 @@ public void onStateChanged(@NonNull final View bottomSheet, final int newState)
if (DeviceUtils.isLandscape(requireContext())
&& isPlayerAvailable()
&& player.isPlaying()
- && !player.isFullscreen()
- && !DeviceUtils.isTablet(activity)
- && player.videoPlayerSelected()) {
- player.toggleFullscreen();
+ && !isFullscreen()
+ && !DeviceUtils.isTablet(activity)) {
+ player.UIs().get(MainPlayerUi.class)
+ .ifPresent(MainPlayerUi::toggleFullscreen);
}
setOverlayLook(binding.appBarLayout, behavior, 1);
break;
@@ -2334,19 +2349,26 @@ && isPlayerAvailable()
// Re-enable clicks
setOverlayElementsClickable(true);
if (isPlayerAvailable()) {
- player.closeItemsList();
+ player.UIs().get(MainPlayerUi.class)
+ .ifPresent(MainPlayerUi::closeItemsList);
}
setOverlayLook(binding.appBarLayout, behavior, 0);
break;
case BottomSheetBehavior.STATE_DRAGGING:
case BottomSheetBehavior.STATE_SETTLING:
- if (isPlayerAvailable() && player.isFullscreen()) {
+ if (isFullscreen()) {
showSystemUi();
}
- if (isPlayerAvailable() && player.isControlsVisible()) {
- player.hideControls(0, 0);
+ if (isPlayerAvailable()) {
+ player.UIs().get(MainPlayerUi.class).ifPresent(ui -> {
+ if (ui.isControlsVisible()) {
+ ui.hideControls(0, 0);
+ }
+ });
}
break;
+ case BottomSheetBehavior.STATE_HALF_EXPANDED:
+ break;
}
}
@@ -2354,7 +2376,9 @@ && isPlayerAvailable()
public void onSlide(@NonNull final View bottomSheet, final float slideOffset) {
setOverlayLook(binding.appBarLayout, behavior, slideOffset);
}
- });
+ };
+
+ bottomSheetBehavior.addBottomSheetCallback(bottomSheetCallback);
// User opened a new page and the player will hide itself
activity.getSupportFragmentManager().addOnBackStackChangedListener(() -> {
@@ -2369,8 +2393,8 @@ private void updateOverlayData(@Nullable final String overlayTitle,
@Nullable final String thumbnailUrl) {
binding.overlayTitleTextView.setText(isEmpty(overlayTitle) ? "" : overlayTitle);
binding.overlayChannelTextView.setText(isEmpty(uploader) ? "" : uploader);
- binding.overlayThumbnail.setImageResource(R.drawable.dummy_thumbnail_dark);
- PicassoHelper.loadThumbnail(thumbnailUrl).tag(PICASSO_VIDEO_DETAILS_TAG)
+ binding.overlayThumbnail.setImageDrawable(null);
+ PicassoHelper.loadDetailsThumbnail(thumbnailUrl).tag(PICASSO_VIDEO_DETAILS_TAG)
.into(binding.overlayThumbnail);
}
@@ -2418,4 +2442,21 @@ boolean isPlayerServiceAvailable() {
boolean isPlayerAndPlayerServiceAvailable() {
return (player != null && playerService != null);
}
+
+ public Optional 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;
+ }
+ }
}
diff --git a/app/src/main/java/org/schabi/newpipe/fragments/detail/VideoDetailPlayerCrasher.java b/app/src/main/java/org/schabi/newpipe/fragments/detail/VideoDetailPlayerCrasher.java
index 55336a42f0e..c816723ff7c 100644
--- a/app/src/main/java/org/schabi/newpipe/fragments/detail/VideoDetailPlayerCrasher.java
+++ b/app/src/main/java/org/schabi/newpipe/fragments/detail/VideoDetailPlayerCrasher.java
@@ -6,6 +6,7 @@
import android.content.Context;
import android.util.Log;
+import android.util.Pair;
import android.view.ContextThemeWrapper;
import android.view.LayoutInflater;
import android.view.ViewGroup;
@@ -28,9 +29,7 @@
import org.schabi.newpipe.util.ThemeHelper;
import java.io.IOException;
-import java.util.Collections;
-import java.util.LinkedHashMap;
-import java.util.Map;
+import java.util.List;
import java.util.function.Supplier;
/**
@@ -43,50 +42,34 @@ public final class VideoDetailPlayerCrasher {
// https://stackoverflow.com/a/54744028
private static final String TAG = "VideoDetPlayerCrasher";
- private static final Map> AVAILABLE_EXCEPTION_TYPES =
- getExceptionTypes();
+ private static final String DEFAULT_MSG = "Dummy";
+
+ private static final List>>
+ AVAILABLE_EXCEPTION_TYPES = List.of(
+ new Pair<>("Source", () -> ExoPlaybackException.createForSource(
+ new IOException(DEFAULT_MSG),
+ ERROR_CODE_BEHIND_LIVE_WINDOW
+ )),
+ new Pair<>("Renderer", () -> ExoPlaybackException.createForRenderer(
+ new Exception(DEFAULT_MSG),
+ "Dummy renderer",
+ 0,
+ null,
+ C.FORMAT_HANDLED,
+ /*isRecoverable=*/false,
+ ERROR_CODE_DECODING_FAILED
+ )),
+ new Pair<>("Unexpected", () -> ExoPlaybackException.createForUnexpected(
+ new RuntimeException(DEFAULT_MSG),
+ ERROR_CODE_UNSPECIFIED
+ )),
+ new Pair<>("Remote", () -> ExoPlaybackException.createForRemote(DEFAULT_MSG))
+ );
private VideoDetailPlayerCrasher() {
// No impls
}
- private static Map> getExceptionTypes() {
- final String defaultMsg = "Dummy";
- final Map> exceptionTypes = new LinkedHashMap<>();
- exceptionTypes.put(
- "Source",
- () -> ExoPlaybackException.createForSource(
- new IOException(defaultMsg),
- ERROR_CODE_BEHIND_LIVE_WINDOW
- )
- );
- exceptionTypes.put(
- "Renderer",
- () -> ExoPlaybackException.createForRenderer(
- new Exception(defaultMsg),
- "Dummy renderer",
- 0,
- null,
- C.FORMAT_HANDLED,
- /*isRecoverable=*/false,
- ERROR_CODE_DECODING_FAILED
- )
- );
- exceptionTypes.put(
- "Unexpected",
- () -> ExoPlaybackException.createForUnexpected(
- new RuntimeException(defaultMsg),
- ERROR_CODE_UNSPECIFIED
- )
- );
- exceptionTypes.put(
- "Remote",
- () -> ExoPlaybackException.createForRemote(defaultMsg)
- );
-
- return Collections.unmodifiableMap(exceptionTypes);
- }
-
private static Context getThemeWrapperContext(final Context context) {
return new ContextThemeWrapper(
context,
@@ -121,10 +104,9 @@ public static void onCrashThePlayer(
.setNegativeButton(R.string.cancel, null)
.create();
- for (final Map.Entry> entry
- : AVAILABLE_EXCEPTION_TYPES.entrySet()) {
+ for (final Pair> entry : AVAILABLE_EXCEPTION_TYPES) {
final RadioButton radioButton = ListRadioIconItemBinding.inflate(inflater).getRoot();
- radioButton.setText(entry.getKey());
+ radioButton.setText(entry.first);
radioButton.setChecked(false);
radioButton.setLayoutParams(
new RadioGroup.LayoutParams(
@@ -133,7 +115,7 @@ public static void onCrashThePlayer(
)
);
radioButton.setOnClickListener(v -> {
- tryCrashPlayerWith(player, entry.getValue().get());
+ tryCrashPlayerWith(player, entry.second.get());
alertDialog.cancel();
});
binding.list.addView(radioButton);
diff --git a/app/src/main/java/org/schabi/newpipe/fragments/list/BaseListFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/list/BaseListFragment.java
index 27e5a8571e4..9e7cb757ccc 100644
--- a/app/src/main/java/org/schabi/newpipe/fragments/list/BaseListFragment.java
+++ b/app/src/main/java/org/schabi/newpipe/fragments/list/BaseListFragment.java
@@ -23,14 +23,11 @@
import org.schabi.newpipe.R;
import org.schabi.newpipe.error.ErrorUtil;
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.fragments.BaseStateFragment;
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.dialog.InfoItemDialog;
import org.schabi.newpipe.util.NavigationHelper;
import org.schabi.newpipe.util.OnClickGesture;
import org.schabi.newpipe.util.StateSaver;
@@ -264,45 +261,28 @@ public void held(final StreamInfoItem selectedItem) {
}
});
- infoListAdapter.setOnChannelSelectedListener(new OnClickGesture<>() {
- @Override
- public void selected(final ChannelInfoItem selectedItem) {
- try {
- onItemSelected(selectedItem);
- NavigationHelper.openChannelFragment(getFM(),
- selectedItem.getServiceId(),
- selectedItem.getUrl(),
- selectedItem.getName());
- } catch (final Exception e) {
- ErrorUtil.showUiErrorSnackbar(
- BaseListFragment.this, "Opening channel fragment", e);
- }
- }
- });
-
- infoListAdapter.setOnPlaylistSelectedListener(new OnClickGesture<>() {
- @Override
- public void selected(final PlaylistInfoItem selectedItem) {
- try {
- onItemSelected(selectedItem);
- NavigationHelper.openPlaylistFragment(getFM(),
- selectedItem.getServiceId(),
- selectedItem.getUrl(),
- selectedItem.getName());
- } catch (final Exception e) {
- ErrorUtil.showUiErrorSnackbar(BaseListFragment.this,
- "Opening playlist fragment", e);
- }
+ infoListAdapter.setOnChannelSelectedListener(selectedItem -> {
+ try {
+ onItemSelected(selectedItem);
+ NavigationHelper.openChannelFragment(getFM(), selectedItem.getServiceId(),
+ selectedItem.getUrl(), selectedItem.getName());
+ } catch (final Exception e) {
+ ErrorUtil.showUiErrorSnackbar(this, "Opening channel fragment", e);
}
});
- infoListAdapter.setOnCommentsSelectedListener(new OnClickGesture<>() {
- @Override
- public void selected(final CommentsInfoItem selectedItem) {
+ infoListAdapter.setOnPlaylistSelectedListener(selectedItem -> {
+ try {
onItemSelected(selectedItem);
+ NavigationHelper.openPlaylistFragment(getFM(), selectedItem.getServiceId(),
+ selectedItem.getUrl(), selectedItem.getName());
+ } catch (final Exception e) {
+ ErrorUtil.showUiErrorSnackbar(this, "Opening playlist fragment", e);
}
});
+ infoListAdapter.setOnCommentsSelectedListener(this::onItemSelected);
+
// Ensure that there is always a scroll listener (e.g. when rotating the device)
useNormalItemListScrollListener();
}
diff --git a/app/src/main/java/org/schabi/newpipe/fragments/list/channel/ChannelFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/list/channel/ChannelFragment.java
index fa8f5fdbd96..8ed9389c39d 100644
--- a/app/src/main/java/org/schabi/newpipe/fragments/list/channel/ChannelFragment.java
+++ b/app/src/main/java/org/schabi/newpipe/fragments/list/channel/ChannelFragment.java
@@ -43,7 +43,7 @@
import org.schabi.newpipe.ktx.AnimationType;
import org.schabi.newpipe.local.subscription.SubscriptionManager;
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.PlayQueue;
import org.schabi.newpipe.util.ExtractorHelper;
@@ -578,17 +578,13 @@ private void showContentNotSupportedIfNeeded() {
}
private PlayQueue getPlayQueue() {
- return getPlayQueue(0);
- }
-
- private PlayQueue getPlayQueue(final int index) {
final List streamItems = infoListAdapter.getItemsList().stream()
.filter(StreamInfoItem.class::isInstance)
.map(StreamInfoItem.class::cast)
.collect(Collectors.toList());
return new ChannelPlayQueue(currentInfo.getServiceId(), currentInfo.getUrl(),
- currentInfo.getNextPage(), streamItems, index);
+ currentInfo.getNextPage(), streamItems, 0);
}
/*//////////////////////////////////////////////////////////////////////////
diff --git a/app/src/main/java/org/schabi/newpipe/fragments/list/playlist/PlaylistFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/list/playlist/PlaylistFragment.java
index ed63c6fd7b5..e3caeb522b0 100644
--- a/app/src/main/java/org/schabi/newpipe/fragments/list/playlist/PlaylistFragment.java
+++ b/app/src/main/java/org/schabi/newpipe/fragments/list/playlist/PlaylistFragment.java
@@ -43,7 +43,7 @@
import org.schabi.newpipe.info_list.dialog.StreamDialogDefaultEntry;
import org.schabi.newpipe.local.dialog.PlaylistDialog;
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.PlaylistPlayQueue;
import org.schabi.newpipe.util.ExtractorHelper;
diff --git a/app/src/main/java/org/schabi/newpipe/fragments/list/search/SearchFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/list/search/SearchFragment.java
index 055c277330f..5175e009685 100644
--- a/app/src/main/java/org/schabi/newpipe/fragments/list/search/SearchFragment.java
+++ b/app/src/main/java/org/schabi/newpipe/fragments/list/search/SearchFragment.java
@@ -200,7 +200,7 @@ public void onAttach(@NonNull final Context context) {
showLocalSuggestions = NewPipeSettings.showLocalSearchSuggestions(activity, prefs);
showRemoteSuggestions = NewPipeSettings.showRemoteSearchSuggestions(activity, prefs);
- suggestionListAdapter = new SuggestionListAdapter(activity);
+ suggestionListAdapter = new SuggestionListAdapter();
historyRecordManager = new HistoryRecordManager(context);
}
@@ -340,6 +340,8 @@ protected void initViews(final View rootView, final Bundle savedInstanceState) {
super.initViews(rootView, savedInstanceState);
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() {
@Override
public int getMovementFlags(@NonNull final RecyclerView recyclerView,
@@ -497,9 +499,6 @@ private void showSearchOnStart() {
+ lastSearchedString);
}
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())) {
searchToolbarContainer.setTranslationX(100);
@@ -533,7 +532,7 @@ private void initSearchListeners() {
searchBinding.correctSuggestion.setVisibility(View.GONE);
searchEditText.setText("");
- suggestionListAdapter.setItems(new ArrayList<>());
+ suggestionListAdapter.submitList(null);
showKeyboardSearch();
});
@@ -922,7 +921,7 @@ private void changeContentFilter(final MenuItem item, final List theCont
filterItemCheckedId = item.getItemId();
item.setChecked(true);
- contentFilter = new String[]{theContentFilter.get(0)};
+ contentFilter = theContentFilter.toArray(new String[0]);
if (!TextUtils.isEmpty(searchString)) {
search(searchString, contentFilter, sortFilter);
@@ -947,8 +946,8 @@ public void handleSuggestions(@NonNull final List suggestions) {
if (DEBUG) {
Log.d(TAG, "handleSuggestions() called with: suggestions = [" + suggestions + "]");
}
- searchBinding.suggestionsList.smoothScrollToPosition(0);
- searchBinding.suggestionsList.post(() -> suggestionListAdapter.setItems(suggestions));
+ suggestionListAdapter.submitList(suggestions,
+ () -> searchBinding.suggestionsList.scrollToPosition(0));
if (suggestionsPanelVisible && isErrorPanelVisible()) {
hideLoading();
@@ -983,8 +982,7 @@ public void handleResult(@NonNull final SearchInfo result) {
isCorrectedSearch = result.isCorrectedSearch();
// List cannot be bundled without creating some containers
- metaInfo = new MetaInfo[result.getMetaInfo().size()];
- metaInfo = result.getMetaInfo().toArray(metaInfo);
+ metaInfo = result.getMetaInfo().toArray(new MetaInfo[0]);
showMetaInfoInTextView(result.getMetaInfo(), searchBinding.searchMetaInfoTextView,
searchBinding.searchMetaInfoSeparator, disposables);
@@ -1070,14 +1068,14 @@ public int getSuggestionMovementFlags(@NonNull final RecyclerView.ViewHolder vie
return 0;
}
- final SuggestionItem item = suggestionListAdapter.getItem(position);
+ final SuggestionItem item = suggestionListAdapter.getCurrentList().get(position);
return item.fromHistory ? makeMovementFlags(0,
ItemTouchHelper.LEFT | ItemTouchHelper.RIGHT) : 0;
}
public void onSuggestionItemSwiped(@NonNull final RecyclerView.ViewHolder viewHolder) {
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)
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
diff --git a/app/src/main/java/org/schabi/newpipe/fragments/list/search/SuggestionListAdapter.java b/app/src/main/java/org/schabi/newpipe/fragments/list/search/SuggestionListAdapter.java
index fb983b01e26..856ba22f19c 100644
--- a/app/src/main/java/org/schabi/newpipe/fragments/list/search/SuggestionListAdapter.java
+++ b/app/src/main/java/org/schabi/newpipe/fragments/list/search/SuggestionListAdapter.java
@@ -1,34 +1,22 @@
package org.schabi.newpipe.fragments.list.search;
-import android.content.Context;
import android.view.LayoutInflater;
-import android.view.View;
import android.view.ViewGroup;
-import android.widget.ImageView;
-import android.widget.TextView;
import androidx.annotation.NonNull;
+import androidx.recyclerview.widget.DiffUtil;
+import androidx.recyclerview.widget.ListAdapter;
import androidx.recyclerview.widget.RecyclerView;
import org.schabi.newpipe.R;
-
-import java.util.ArrayList;
-import java.util.List;
+import org.schabi.newpipe.databinding.ItemSearchSuggestionBinding;
public class SuggestionListAdapter
- extends RecyclerView.Adapter {
- private final ArrayList items = new ArrayList<>();
- private final Context context;
+ extends ListAdapter {
private OnSuggestionItemSelected listener;
- public SuggestionListAdapter(final Context context) {
- this.context = context;
- }
-
- public void setItems(final List items) {
- this.items.clear();
- this.items.addAll(items);
- notifyDataSetChanged();
+ public SuggestionListAdapter() {
+ super(new SuggestionItemCallback());
}
public void setListener(final OnSuggestionItemSelected listener) {
@@ -39,45 +27,32 @@ public void setListener(final OnSuggestionItemSelected listener) {
@Override
public SuggestionItemHolder onCreateViewHolder(@NonNull final ViewGroup parent,
final int viewType) {
- return new SuggestionItemHolder(LayoutInflater.from(context)
- .inflate(R.layout.item_search_suggestion, parent, false));
+ return new SuggestionItemHolder(ItemSearchSuggestionBinding
+ .inflate(LayoutInflater.from(parent.getContext()), parent, false));
}
@Override
public void onBindViewHolder(final SuggestionItemHolder holder, final int position) {
final SuggestionItem currentItem = getItem(position);
holder.updateFrom(currentItem);
- holder.queryView.setOnClickListener(v -> {
+ holder.itemBinding.suggestionSearch.setOnClickListener(v -> {
if (listener != null) {
listener.onSuggestionItemSelected(currentItem);
}
});
- holder.queryView.setOnLongClickListener(v -> {
+ holder.itemBinding.suggestionSearch.setOnLongClickListener(v -> {
if (listener != null) {
listener.onSuggestionItemLongClick(currentItem);
}
return true;
});
- holder.insertView.setOnClickListener(v -> {
+ holder.itemBinding.suggestionInsert.setOnClickListener(v -> {
if (listener != null) {
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 {
void onSuggestionItemSelected(SuggestionItem item);
@@ -87,30 +62,32 @@ public interface OnSuggestionItemSelected {
}
public static final class SuggestionItemHolder extends RecyclerView.ViewHolder {
- private final TextView itemSuggestionQuery;
- private final ImageView suggestionIcon;
- private final View queryView;
- private final View insertView;
+ private final ItemSearchSuggestionBinding itemBinding;
- // Cache some ids, as they can potentially be constantly updated/recycled
- private final int historyResId;
- private final int searchResId;
-
- private SuggestionItemHolder(final View rootView) {
- super(rootView);
- suggestionIcon = rootView.findViewById(R.id.item_suggestion_icon);
- itemSuggestionQuery = rootView.findViewById(R.id.item_suggestion_query);
+ private SuggestionItemHolder(final ItemSearchSuggestionBinding binding) {
+ super(binding.getRoot());
+ this.itemBinding = binding;
+ }
- queryView = rootView.findViewById(R.id.suggestion_search);
- insertView = rootView.findViewById(R.id.suggestion_insert);
+ private void updateFrom(final SuggestionItem item) {
+ itemBinding.itemSuggestionIcon.setImageResource(item.fromHistory ? R.drawable.ic_history
+ : R.drawable.ic_search);
+ itemBinding.itemSuggestionQuery.setText(item.query);
+ }
+ }
- historyResId = R.drawable.ic_history;
- searchResId = R.drawable.ic_search;
+ private static class SuggestionItemCallback extends DiffUtil.ItemCallback {
+ @Override
+ public boolean areItemsTheSame(@NonNull final SuggestionItem oldItem,
+ @NonNull final SuggestionItem newItem) {
+ return oldItem.fromHistory == newItem.fromHistory
+ && oldItem.query.equals(newItem.query);
}
- private void updateFrom(final SuggestionItem item) {
- suggestionIcon.setImageResource(item.fromHistory ? historyResId : searchResId);
- itemSuggestionQuery.setText(item.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
}
}
}
diff --git a/app/src/main/java/org/schabi/newpipe/info_list/InfoItemBuilder.java b/app/src/main/java/org/schabi/newpipe/info_list/InfoItemBuilder.java
index d78bf10769d..68f19ee9714 100644
--- a/app/src/main/java/org/schabi/newpipe/info_list/InfoItemBuilder.java
+++ b/app/src/main/java/org/schabi/newpipe/info_list/InfoItemBuilder.java
@@ -67,8 +67,8 @@ public View buildView(@NonNull final ViewGroup parent, @NonNull final InfoItem i
public View buildView(@NonNull final ViewGroup parent, @NonNull final InfoItem infoItem,
final HistoryRecordManager historyRecordManager,
final boolean useMiniVariant) {
- final InfoItemHolder holder
- = holderFromInfoType(parent, infoItem.getInfoType(), useMiniVariant);
+ final InfoItemHolder holder =
+ holderFromInfoType(parent, infoItem.getInfoType(), useMiniVariant);
holder.updateFromItem(infoItem, historyRecordManager);
return holder.itemView;
}
diff --git a/app/src/main/java/org/schabi/newpipe/info_list/dialog/InfoItemDialog.java b/app/src/main/java/org/schabi/newpipe/info_list/dialog/InfoItemDialog.java
index 5afaea0384a..61a88bb8f22 100644
--- a/app/src/main/java/org/schabi/newpipe/info_list/dialog/InfoItemDialog.java
+++ b/app/src/main/java/org/schabi/newpipe/info_list/dialog/InfoItemDialog.java
@@ -321,6 +321,7 @@ public Builder addDefaultBeginningEntries() {
*/
public Builder addDefaultEndEntries() {
addAllEntries(
+ StreamDialogDefaultEntry.DOWNLOAD,
StreamDialogDefaultEntry.APPEND_PLAYLIST,
StreamDialogDefaultEntry.SHARE,
StreamDialogDefaultEntry.OPEN_IN_BROWSER
diff --git a/app/src/main/java/org/schabi/newpipe/info_list/dialog/StreamDialogDefaultEntry.java b/app/src/main/java/org/schabi/newpipe/info_list/dialog/StreamDialogDefaultEntry.java
index 7e87318ee39..1265e976726 100644
--- a/app/src/main/java/org/schabi/newpipe/info_list/dialog/StreamDialogDefaultEntry.java
+++ b/app/src/main/java/org/schabi/newpipe/info_list/dialog/StreamDialogDefaultEntry.java
@@ -2,6 +2,7 @@
import static org.schabi.newpipe.util.NavigationHelper.openChannelFragment;
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 android.net.Uri;
@@ -11,6 +12,7 @@
import org.schabi.newpipe.R;
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.PlaylistDialog;
import org.schabi.newpipe.local.history.HistoryRecordManager;
@@ -18,7 +20,7 @@
import org.schabi.newpipe.util.external_communication.KoreUtils;
import org.schabi.newpipe.util.external_communication.ShareUtils;
-import java.util.Collections;
+import java.util.List;
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
@@ -87,7 +89,7 @@ public enum StreamDialogDefaultEntry {
APPEND_PLAYLIST(R.string.add_to_playlist, (fragment, item) ->
PlaylistDialog.createCorrespondingDialog(
fragment.getContext(),
- Collections.singletonList(new StreamEntity(item)),
+ List.of(new StreamEntity(item)),
dialog -> dialog.show(
fragment.getParentFragmentManager(),
"StreamDialogEntry@"
@@ -110,6 +112,15 @@ public enum StreamDialogDefaultEntry {
ShareUtils.shareText(fragment.requireContext(), item.getName(), item.getUrl(),
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) ->
ShareUtils.openUrlInBrowser(fragment.requireContext(), item.getUrl())),
diff --git a/app/src/main/java/org/schabi/newpipe/info_list/holder/ChannelMiniInfoItemHolder.java b/app/src/main/java/org/schabi/newpipe/info_list/holder/ChannelMiniInfoItemHolder.java
index aa4f4c9f023..89398a1e52a 100644
--- a/app/src/main/java/org/schabi/newpipe/info_list/holder/ChannelMiniInfoItemHolder.java
+++ b/app/src/main/java/org/schabi/newpipe/info_list/holder/ChannelMiniInfoItemHolder.java
@@ -42,7 +42,7 @@ public void updateFromItem(final InfoItem infoItem,
itemTitleView.setText(item.getName());
itemAdditionalDetailView.setText(getDetailLine(item));
- PicassoHelper.loadThumbnail(item.getThumbnailUrl()).into(itemThumbnailView);
+ PicassoHelper.loadAvatar(item.getThumbnailUrl()).into(itemThumbnailView);
itemView.setOnClickListener(view -> {
if (itemBuilder.getOnChannelSelectedListener() != null) {
diff --git a/app/src/main/java/org/schabi/newpipe/info_list/holder/CommentsMiniInfoItemHolder.java b/app/src/main/java/org/schabi/newpipe/info_list/holder/CommentsMiniInfoItemHolder.java
index 6e4773c09d4..b900750a8f3 100644
--- a/app/src/main/java/org/schabi/newpipe/info_list/holder/CommentsMiniInfoItemHolder.java
+++ b/app/src/main/java/org/schabi/newpipe/info_list/holder/CommentsMiniInfoItemHolder.java
@@ -23,9 +23,9 @@
import org.schabi.newpipe.util.DeviceUtils;
import org.schabi.newpipe.util.Localization;
import org.schabi.newpipe.util.NavigationHelper;
-import org.schabi.newpipe.util.external_communication.TimestampExtractor;
-import org.schabi.newpipe.util.external_communication.ShareUtils;
import org.schabi.newpipe.util.PicassoHelper;
+import org.schabi.newpipe.util.external_communication.ShareUtils;
+import org.schabi.newpipe.util.external_communication.TimestampExtractor;
import java.util.regex.Matcher;
@@ -204,8 +204,9 @@ private void ellipsize() {
boolean hasEllipsis = false;
if (itemContentView.getLineCount() > COMMENT_DEFAULT_LINES) {
- final int endOfLastLine
- = itemContentView.getLayout().getLineEnd(COMMENT_DEFAULT_LINES - 1);
+ final int endOfLastLine = itemContentView
+ .getLayout()
+ .getLineEnd(COMMENT_DEFAULT_LINES - 1);
int end = itemContentView.getText().toString().lastIndexOf(' ', endOfLastLine - 2);
if (end == -1) {
end = Math.max(endOfLastLine - 2, 0);
diff --git a/app/src/main/java/org/schabi/newpipe/info_list/holder/StreamMiniInfoItemHolder.java b/app/src/main/java/org/schabi/newpipe/info_list/holder/StreamMiniInfoItemHolder.java
index 54d31ca5735..8d17017d217 100644
--- a/app/src/main/java/org/schabi/newpipe/info_list/holder/StreamMiniInfoItemHolder.java
+++ b/app/src/main/java/org/schabi/newpipe/info_list/holder/StreamMiniInfoItemHolder.java
@@ -14,8 +14,8 @@
import org.schabi.newpipe.info_list.InfoItemBuilder;
import org.schabi.newpipe.ktx.ViewUtils;
import org.schabi.newpipe.local.history.HistoryRecordManager;
-import org.schabi.newpipe.util.PicassoHelper;
import org.schabi.newpipe.util.Localization;
+import org.schabi.newpipe.util.PicassoHelper;
import org.schabi.newpipe.util.StreamTypeUtil;
import org.schabi.newpipe.views.AnimatedProgressBar;
@@ -111,8 +111,9 @@ public void updateState(final InfoItem infoItem,
final HistoryRecordManager historyRecordManager) {
final StreamInfoItem item = (StreamInfoItem) infoItem;
- final StreamStateEntity state
- = historyRecordManager.loadStreamState(infoItem).blockingGet()[0];
+ final StreamStateEntity state = historyRecordManager
+ .loadStreamState(infoItem)
+ .blockingGet()[0];
if (state != null && item.getDuration() > 0
&& !StreamTypeUtil.isLiveStream(item.getStreamType())) {
itemProgressView.setMax((int) item.getDuration());
diff --git a/app/src/main/java/org/schabi/newpipe/ktx/View.kt b/app/src/main/java/org/schabi/newpipe/ktx/View.kt
index ace1dbf7ef2..bf0dcb2012c 100644
--- a/app/src/main/java/org/schabi/newpipe/ktx/View.kt
+++ b/app/src/main/java/org/schabi/newpipe/ktx/View.kt
@@ -21,10 +21,6 @@ import org.schabi.newpipe.MainActivity
private const val TAG = "ViewUtils"
-inline var View.backgroundTintListCompat: ColorStateList?
- get() = ViewCompat.getBackgroundTintList(this)
- set(value) = ViewCompat.setBackgroundTintList(this, value)
-
/**
* Animate the view.
*
@@ -96,62 +92,43 @@ fun View.animateBackgroundColor(duration: Long, @ColorInt colorStart: Int, @Colo
if (MainActivity.DEBUG) {
Log.d(
TAG,
- "animateBackgroundColor() called with: " +
- "view = [" + this + "], duration = [" + duration + "], " +
- "colorStart = [" + colorStart + "], colorEnd = [" + colorEnd + "]"
+ "animateBackgroundColor() called with: view = [$this], duration = [$duration], " +
+ "colorStart = [$colorStart], colorEnd = [$colorEnd]"
)
}
- val empty = arrayOf(IntArray(0))
val viewPropertyAnimator = ValueAnimator.ofObject(ArgbEvaluator(), colorStart, colorEnd)
viewPropertyAnimator.interpolator = FastOutSlowInInterpolator()
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(
- onCancel = { backgroundTintListCompat = ColorStateList(empty, intArrayOf(colorEnd)) },
- onEnd = { backgroundTintListCompat = ColorStateList(empty, intArrayOf(colorEnd)) }
- )
+ viewPropertyAnimator.addUpdateListener { listenerAction(it.animatedValue as Int) }
+ viewPropertyAnimator.addListener(onCancel = { listenerAction(colorEnd) }, onEnd = { listenerAction(colorEnd) })
viewPropertyAnimator.start()
}
fun View.animateHeight(duration: Long, targetHeight: Int): ValueAnimator {
if (MainActivity.DEBUG) {
- Log.d(
- TAG,
- "animateHeight: duration = [" + duration + "], " +
- "from " + height + " to → " + targetHeight + " in: " + this
- )
+ Log.d(TAG, "animateHeight: duration = [$duration], from $height to → $targetHeight in: $this")
}
val animator = ValueAnimator.ofFloat(height.toFloat(), targetHeight.toFloat())
animator.interpolator = FastOutSlowInInterpolator()
animator.duration = duration
- animator.addUpdateListener { animation: ValueAnimator ->
- val value = animation.animatedValue as Float
- layoutParams.height = value.toInt()
+
+ fun listenerAction(value: Int) {
+ layoutParams.height = value
requestLayout()
}
- animator.addListener(
- onCancel = {
- layoutParams.height = targetHeight
- requestLayout()
- },
- onEnd = {
- layoutParams.height = targetHeight
- requestLayout()
- }
- )
+ animator.addUpdateListener { listenerAction((it.animatedValue as Float).toInt()) }
+ animator.addListener(onCancel = { listenerAction(targetHeight) }, onEnd = { listenerAction(targetHeight) })
animator.start()
return animator
}
fun View.animateRotation(duration: Long, targetRotation: Int) {
if (MainActivity.DEBUG) {
- Log.d(
- TAG,
- "animateRotation: duration = [" + duration + "], " +
- "from " + rotation + " to → " + targetRotation + " in: " + this
- )
+ Log.d(TAG, "animateRotation: duration = [$duration], from $rotation to → $targetRotation in: $this")
}
animate().setListener(null).cancel()
animate()
@@ -172,20 +149,13 @@ private fun View.animateAlpha(enterOrExit: Boolean, duration: Long, delay: Long,
if (enterOrExit) {
animate().setInterpolator(FastOutSlowInInterpolator()).alpha(1f)
.setDuration(duration).setStartDelay(delay)
- .setListener(object : AnimatorListenerAdapter() {
- override fun onAnimationEnd(animation: Animator) {
- execOnEnd?.run()
- }
- }).start()
+ .setListener(ExecOnEndListener(execOnEnd))
+ .start()
} else {
animate().setInterpolator(FastOutSlowInInterpolator()).alpha(0f)
.setDuration(duration).setStartDelay(delay)
- .setListener(object : AnimatorListenerAdapter() {
- override fun onAnimationEnd(animation: Animator) {
- isGone = true
- execOnEnd?.run()
- }
- }).start()
+ .setListener(HideAndExecOnEndListener(this, execOnEnd))
+ .start()
}
}
@@ -197,11 +167,8 @@ private fun View.animateScaleAndAlpha(enterOrExit: Boolean, duration: Long, dela
.setInterpolator(FastOutSlowInInterpolator())
.alpha(1f).scaleX(1f).scaleY(1f)
.setDuration(duration).setStartDelay(delay)
- .setListener(object : AnimatorListenerAdapter() {
- override fun onAnimationEnd(animation: Animator) {
- execOnEnd?.run()
- }
- }).start()
+ .setListener(ExecOnEndListener(execOnEnd))
+ .start()
} else {
scaleX = 1f
scaleY = 1f
@@ -209,12 +176,8 @@ private fun View.animateScaleAndAlpha(enterOrExit: Boolean, duration: Long, dela
.setInterpolator(FastOutSlowInInterpolator())
.alpha(0f).scaleX(.8f).scaleY(.8f)
.setDuration(duration).setStartDelay(delay)
- .setListener(object : AnimatorListenerAdapter() {
- override fun onAnimationEnd(animation: Animator) {
- isGone = true
- execOnEnd?.run()
- }
- }).start()
+ .setListener(HideAndExecOnEndListener(this, execOnEnd))
+ .start()
}
}
@@ -227,11 +190,8 @@ private fun View.animateLightScaleAndAlpha(enterOrExit: Boolean, duration: Long,
.setInterpolator(FastOutSlowInInterpolator())
.alpha(1f).scaleX(1f).scaleY(1f)
.setDuration(duration).setStartDelay(delay)
- .setListener(object : AnimatorListenerAdapter() {
- override fun onAnimationEnd(animation: Animator) {
- execOnEnd?.run()
- }
- }).start()
+ .setListener(ExecOnEndListener(execOnEnd))
+ .start()
} else {
alpha = 1f
scaleX = 1f
@@ -240,12 +200,8 @@ private fun View.animateLightScaleAndAlpha(enterOrExit: Boolean, duration: Long,
.setInterpolator(FastOutSlowInInterpolator())
.alpha(0f).scaleX(.95f).scaleY(.95f)
.setDuration(duration).setStartDelay(delay)
- .setListener(object : AnimatorListenerAdapter() {
- override fun onAnimationEnd(animation: Animator) {
- isGone = true
- execOnEnd?.run()
- }
- }).start()
+ .setListener(HideAndExecOnEndListener(this, execOnEnd))
+ .start()
}
}
@@ -256,22 +212,15 @@ private fun View.animateSlideAndAlpha(enterOrExit: Boolean, duration: Long, dela
animate()
.setInterpolator(FastOutSlowInInterpolator()).alpha(1f).translationY(0f)
.setDuration(duration).setStartDelay(delay)
- .setListener(object : AnimatorListenerAdapter() {
- override fun onAnimationEnd(animation: Animator) {
- execOnEnd?.run()
- }
- }).start()
+ .setListener(ExecOnEndListener(execOnEnd))
+ .start()
} else {
animate()
.setInterpolator(FastOutSlowInInterpolator())
.alpha(0f).translationY(-height.toFloat())
.setDuration(duration).setStartDelay(delay)
- .setListener(object : AnimatorListenerAdapter() {
- override fun onAnimationEnd(animation: Animator) {
- isGone = true
- execOnEnd?.run()
- }
- }).start()
+ .setListener(HideAndExecOnEndListener(this, execOnEnd))
+ .start()
}
}
@@ -282,21 +231,14 @@ private fun View.animateLightSlideAndAlpha(enterOrExit: Boolean, duration: Long,
animate()
.setInterpolator(FastOutSlowInInterpolator()).alpha(1f).translationY(0f)
.setDuration(duration).setStartDelay(delay)
- .setListener(object : AnimatorListenerAdapter() {
- override fun onAnimationEnd(animation: Animator) {
- execOnEnd?.run()
- }
- }).start()
+ .setListener(ExecOnEndListener(execOnEnd))
+ .start()
} else {
animate().setInterpolator(FastOutSlowInInterpolator())
.alpha(0f).translationY(-height / 2.0f)
.setDuration(duration).setStartDelay(delay)
- .setListener(object : AnimatorListenerAdapter() {
- override fun onAnimationEnd(animation: Animator) {
- isGone = true
- execOnEnd?.run()
- }
- }).start()
+ .setListener(HideAndExecOnEndListener(this, execOnEnd))
+ .start()
}
}
@@ -318,11 +260,7 @@ fun View.slideUp(
.setStartDelay(delay)
.setDuration(duration)
.setInterpolator(FastOutSlowInInterpolator())
- .setListener(object : AnimatorListenerAdapter() {
- override fun onAnimationEnd(animation: Animator) {
- execOnEnd?.run()
- }
- })
+ .setListener(ExecOnEndListener(execOnEnd))
.start()
}
@@ -336,6 +274,20 @@ fun View.animateHideRecyclerViewAllowingScrolling() {
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 {
ALPHA, SCALE_AND_ALPHA, LIGHT_SCALE_AND_ALPHA, SLIDE_AND_ALPHA, LIGHT_SLIDE_AND_ALPHA
}
diff --git a/app/src/main/java/org/schabi/newpipe/local/bookmark/BookmarkFragment.java b/app/src/main/java/org/schabi/newpipe/local/bookmark/BookmarkFragment.java
index f272a8831f4..be74145424c 100644
--- a/app/src/main/java/org/schabi/newpipe/local/bookmark/BookmarkFragment.java
+++ b/app/src/main/java/org/schabi/newpipe/local/bookmark/BookmarkFragment.java
@@ -98,7 +98,7 @@ protected void initViews(final View rootView, final Bundle savedInstanceState) {
protected void initListeners() {
super.initListeners();
- itemListAdapter.setSelectedListener(new OnClickGesture() {
+ itemListAdapter.setSelectedListener(new OnClickGesture<>() {
@Override
public void selected(final LocalItem selectedItem) {
final FragmentManager fragmentManager = getFM();
@@ -256,8 +256,8 @@ private void showRemoteDeleteDialog(final PlaylistRemoteEntity item) {
}
private void showLocalDialog(final PlaylistMetadataEntry selectedItem) {
- final DialogEditTextBinding dialogBinding
- = DialogEditTextBinding.inflate(getLayoutInflater());
+ final DialogEditTextBinding dialogBinding =
+ DialogEditTextBinding.inflate(getLayoutInflater());
dialogBinding.dialogEditText.setHint(R.string.name);
dialogBinding.dialogEditText.setInputType(InputType.TYPE_CLASS_TEXT);
dialogBinding.dialogEditText.setText(selectedItem.name);
diff --git a/app/src/main/java/org/schabi/newpipe/local/dialog/PlaylistAppendDialog.java b/app/src/main/java/org/schabi/newpipe/local/dialog/PlaylistAppendDialog.java
index a874cdd621c..3d5d16c39c1 100644
--- a/app/src/main/java/org/schabi/newpipe/local/dialog/PlaylistAppendDialog.java
+++ b/app/src/main/java/org/schabi/newpipe/local/dialog/PlaylistAppendDialog.java
@@ -13,12 +13,10 @@
import org.schabi.newpipe.NewPipeDatabase;
import org.schabi.newpipe.R;
-import org.schabi.newpipe.database.LocalItem;
import org.schabi.newpipe.database.playlist.PlaylistMetadataEntry;
import org.schabi.newpipe.database.stream.model.StreamEntity;
import org.schabi.newpipe.local.LocalItemListAdapter;
import org.schabi.newpipe.local.playlist.LocalPlaylistManager;
-import org.schabi.newpipe.util.OnClickGesture;
import java.util.List;
@@ -63,18 +61,10 @@ public void onViewCreated(@NonNull final View view, @Nullable final Bundle saved
new LocalPlaylistManager(NewPipeDatabase.getInstance(requireContext()));
playlistAdapter = new LocalItemListAdapter(getActivity());
- playlistAdapter.setSelectedListener(new OnClickGesture() {
- @Override
- public void selected(final LocalItem selectedItem) {
- if (!(selectedItem instanceof PlaylistMetadataEntry)
- || getStreamEntities() == null) {
- return;
- }
- onPlaylistSelected(
- playlistManager,
- (PlaylistMetadataEntry) selectedItem,
- getStreamEntities()
- );
+ playlistAdapter.setSelectedListener(selectedItem -> {
+ final List entities = getStreamEntities();
+ if (selectedItem instanceof PlaylistMetadataEntry && entities != null) {
+ onPlaylistSelected(playlistManager, (PlaylistMetadataEntry) selectedItem, entities);
}
});
@@ -138,14 +128,11 @@ private void onPlaylistsReceived(@NonNull final List play
private void onPlaylistSelected(@NonNull final LocalPlaylistManager manager,
@NonNull final PlaylistMetadataEntry playlist,
@NonNull final List streams) {
- if (getStreamEntities() == null) {
- return;
- }
-
final Toast successToast = Toast.makeText(getContext(),
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
.changePlaylistThumbnail(playlist.uid, streams.get(0).getThumbnailUrl())
.observeOn(AndroidSchedulers.mainThread())
diff --git a/app/src/main/java/org/schabi/newpipe/local/dialog/PlaylistCreationDialog.java b/app/src/main/java/org/schabi/newpipe/local/dialog/PlaylistCreationDialog.java
index 0c09f3f0dc3..0d5cfac234f 100644
--- a/app/src/main/java/org/schabi/newpipe/local/dialog/PlaylistCreationDialog.java
+++ b/app/src/main/java/org/schabi/newpipe/local/dialog/PlaylistCreationDialog.java
@@ -45,8 +45,8 @@ public Dialog onCreateDialog(@Nullable final Bundle savedInstanceState) {
return super.onCreateDialog(savedInstanceState);
}
- final DialogEditTextBinding dialogBinding
- = DialogEditTextBinding.inflate(getLayoutInflater());
+ final DialogEditTextBinding dialogBinding =
+ DialogEditTextBinding.inflate(getLayoutInflater());
dialogBinding.getRoot().getContext().setTheme(ThemeHelper.getDialogTheme(requireContext()));
dialogBinding.dialogEditText.setHint(R.string.name);
dialogBinding.dialogEditText.setInputType(InputType.TYPE_CLASS_TEXT);
diff --git a/app/src/main/java/org/schabi/newpipe/local/dialog/PlaylistDialog.java b/app/src/main/java/org/schabi/newpipe/local/dialog/PlaylistDialog.java
index f568ef81a03..612c3818187 100644
--- a/app/src/main/java/org/schabi/newpipe/local/dialog/PlaylistDialog.java
+++ b/app/src/main/java/org/schabi/newpipe/local/dialog/PlaylistDialog.java
@@ -9,15 +9,20 @@
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.fragment.app.DialogFragment;
+import androidx.fragment.app.FragmentManager;
import org.schabi.newpipe.NewPipeDatabase;
import org.schabi.newpipe.database.stream.model.StreamEntity;
import org.schabi.newpipe.local.playlist.LocalPlaylistManager;
+import org.schabi.newpipe.player.Player;
import org.schabi.newpipe.util.StateSaver;
import java.util.List;
+import java.util.Objects;
import java.util.Queue;
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.disposables.Disposable;
@@ -131,13 +136,13 @@ protected void setStreamEntities(final List streamEntities) {
* @param context context used for accessing the database
* @param streamEntities used for crating the dialog
* @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(
final Context context,
final List streamEntities,
- final Consumer onExec
- ) {
+ final Consumer onExec) {
+
return new LocalPlaylistManager(NewPipeDatabase.getInstance(context))
.hasPlaylists()
.observeOn(AndroidSchedulers.mainThread())
@@ -147,4 +152,30 @@ public static Disposable createCorrespondingDialog(
: 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 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"));
+ }
}
diff --git a/app/src/main/java/org/schabi/newpipe/local/feed/FeedDatabaseManager.kt b/app/src/main/java/org/schabi/newpipe/local/feed/FeedDatabaseManager.kt
index 7a8723ceb2f..07edb04997d 100644
--- a/app/src/main/java/org/schabi/newpipe/local/feed/FeedDatabaseManager.kt
+++ b/app/src/main/java/org/schabi/newpipe/local/feed/FeedDatabaseManager.kt
@@ -41,19 +41,15 @@ class FeedDatabaseManager(context: Context) {
fun database() = database
fun getStreams(
- groupId: Long = FeedGroupEntity.GROUP_ALL_ID,
- getPlayedStreams: Boolean = true
+ groupId: Long,
+ includePlayedStreams: Boolean,
+ includeFutureStreams: Boolean
): Maybe> {
- return when (groupId) {
- FeedGroupEntity.GROUP_ALL_ID -> {
- if (getPlayedStreams) feedTable.getAllStreams()
- else feedTable.getLiveOrNotPlayedStreams()
- }
- else -> {
- if (getPlayedStreams) feedTable.getAllStreamsForGroup(groupId)
- else feedTable.getLiveOrNotPlayedStreamsForGroup(groupId)
- }
- }
+ return feedTable.getStreams(
+ groupId,
+ includePlayedStreams,
+ if (includeFutureStreams) null else OffsetDateTime.now()
+ )
}
fun outdatedSubscriptions(outdatedThreshold: OffsetDateTime) = feedTable.getAllOutdated(outdatedThreshold)
diff --git a/app/src/main/java/org/schabi/newpipe/local/feed/FeedFragment.kt b/app/src/main/java/org/schabi/newpipe/local/feed/FeedFragment.kt
index b291aa03568..c9f926f061a 100644
--- a/app/src/main/java/org/schabi/newpipe/local/feed/FeedFragment.kt
+++ b/app/src/main/java/org/schabi/newpipe/local/feed/FeedFragment.kt
@@ -41,6 +41,7 @@ import androidx.appcompat.app.AlertDialog
import androidx.appcompat.content.res.AppCompatResources
import androidx.core.content.edit
import androidx.core.os.bundleOf
+import androidx.core.view.MenuItemCompat
import androidx.core.view.isVisible
import androidx.lifecycle.ViewModelProvider
import androidx.preference.PreferenceManager
@@ -98,6 +99,7 @@ class FeedFragment : BaseStateFragment() {
private lateinit var groupAdapter: GroupieAdapter
@State @JvmField var showPlayedItems: Boolean = true
+ @State @JvmField var showFutureItems: Boolean = true
private var onSettingsChangeListener: SharedPreferences.OnSharedPreferenceChangeListener? = null
private var updateListViewModeOnResume = false
@@ -134,9 +136,10 @@ class FeedFragment : BaseStateFragment() {
_feedBinding = FragmentFeedBinding.bind(rootView)
super.onViewCreated(rootView, savedInstanceState)
- val factory = FeedViewModel.Factory(requireContext(), groupId)
- viewModel = ViewModelProvider(this, factory).get(FeedViewModel::class.java)
+ val factory = FeedViewModel.getFactory(requireContext(), groupId)
+ viewModel = ViewModelProvider(this, factory)[FeedViewModel::class.java]
showPlayedItems = viewModel.getShowPlayedItemsFromPreferences()
+ showFutureItems = viewModel.getShowFutureItemsFromPreferences()
viewModel.stateLiveData.observe(viewLifecycleOwner) { it?.let(::handleResult) }
groupAdapter = GroupieAdapter().apply {
@@ -212,6 +215,7 @@ class FeedFragment : BaseStateFragment() {
inflater.inflate(R.menu.menu_feed_fragment, menu)
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 {
@@ -241,6 +245,11 @@ class FeedFragment : BaseStateFragment() {
updateTogglePlayedItemsButton(item)
viewModel.togglePlayedItems(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)
@@ -278,6 +287,32 @@ class FeedFragment : BaseStateFragment() {
requireContext(),
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
+ )
+ )
}
// //////////////////////////////////////////////////////////////////////////
diff --git a/app/src/main/java/org/schabi/newpipe/local/feed/FeedViewModel.kt b/app/src/main/java/org/schabi/newpipe/local/feed/FeedViewModel.kt
index e21963c1651..76d5e9d632b 100644
--- a/app/src/main/java/org/schabi/newpipe/local/feed/FeedViewModel.kt
+++ b/app/src/main/java/org/schabi/newpipe/local/feed/FeedViewModel.kt
@@ -1,17 +1,20 @@
package org.schabi.newpipe.local.feed
+import android.app.Application
import android.content.Context
import androidx.core.content.edit
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
-import androidx.lifecycle.ViewModelProvider
+import androidx.lifecycle.viewmodel.initializer
+import androidx.lifecycle.viewmodel.viewModelFactory
import androidx.preference.PreferenceManager
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
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.schedulers.Schedulers
+import org.schabi.newpipe.App
import org.schabi.newpipe.R
import org.schabi.newpipe.database.feed.model.FeedGroupEntity
import org.schabi.newpipe.database.stream.StreamWithState
@@ -26,17 +29,23 @@ import java.time.OffsetDateTime
import java.util.concurrent.TimeUnit
class FeedViewModel(
- private val applicationContext: Context,
+ private val application: Application,
groupId: Long = FeedGroupEntity.GROUP_ALL_ID,
- initialShowPlayedItems: Boolean = true
+ initialShowPlayedItems: Boolean = true,
+ initialShowFutureItems: Boolean = true
) : ViewModel() {
- private var feedDatabaseManager: FeedDatabaseManager = FeedDatabaseManager(applicationContext)
+ private val feedDatabaseManager = FeedDatabaseManager(application)
private val toggleShowPlayedItems = BehaviorProcessor.create()
private val toggleShowPlayedItemsFlowable = toggleShowPlayedItems
.startWithItem(initialShowPlayedItems)
.distinctUntilChanged()
+ private val toggleShowFutureItems = BehaviorProcessor.create()
+ private val toggleShowFutureItemsFlowable = toggleShowFutureItems
+ .startWithItem(initialShowFutureItems)
+ .distinctUntilChanged()
+
private val mutableStateLiveData = MutableLiveData()
val stateLiveData: LiveData = mutableStateLiveData
@@ -44,21 +53,22 @@ class FeedViewModel(
.combineLatest(
FeedEventManager.events(),
toggleShowPlayedItemsFlowable,
+ toggleShowFutureItemsFlowable,
feedDatabaseManager.notLoadedCount(groupId),
feedDatabaseManager.oldestSubscriptionUpdate(groupId),
- Function4 { t1: FeedEventManager.Event, t2: Boolean,
- t3: Long, t4: List ->
- return@Function4 CombineResultEventHolder(t1, t2, t3, t4.firstOrNull())
+ Function5 { t1: FeedEventManager.Event, t2: Boolean, t3: Boolean,
+ t4: Long, t5: List ->
+ return@Function5 CombineResultEventHolder(t1, t2, t3, t4, t5.firstOrNull())
}
)
.throttleLatest(DEFAULT_THROTTLE_TIMEOUT, TimeUnit.MILLISECONDS)
.subscribeOn(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)
feedDatabaseManager
- .getStreams(groupId, showPlayedItems)
+ .getStreams(groupId, showPlayedItems, showFutureItems)
.blockingGet(arrayListOf())
else
arrayListOf()
@@ -89,8 +99,9 @@ class FeedViewModel(
private data class CombineResultEventHolder(
val t1: FeedEventManager.Event,
val t2: Boolean,
- val t3: Long,
- val t4: OffsetDateTime?
+ val t3: Boolean,
+ val t4: Long,
+ val t5: OffsetDateTime?
)
private data class CombineResultDataHolder(
@@ -105,31 +116,42 @@ class FeedViewModel(
}
fun saveShowPlayedItemsToPreferences(showPlayedItems: Boolean) =
- PreferenceManager.getDefaultSharedPreferences(applicationContext).edit {
- this.putBoolean(applicationContext.getString(R.string.feed_show_played_items_key), showPlayedItems)
+ PreferenceManager.getDefaultSharedPreferences(application).edit {
+ this.putBoolean(application.getString(R.string.feed_show_played_items_key), showPlayedItems)
+ this.apply()
+ }
+
+ 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 getShowPlayedItemsFromPreferences() = getShowPlayedItemsFromPreferences(applicationContext)
+ fun getShowFutureItemsFromPreferences() = getShowFutureItemsFromPreferences(application)
companion object {
private fun getShowPlayedItemsFromPreferences(context: Context) =
PreferenceManager.getDefaultSharedPreferences(context)
.getBoolean(context.getString(R.string.feed_show_played_items_key), true)
- }
-
- class Factory(
- private val context: Context,
- private val groupId: Long = FeedGroupEntity.GROUP_ALL_ID
- ) : ViewModelProvider.Factory {
- @Suppress("UNCHECKED_CAST")
- override fun create(modelClass: Class): T {
- return FeedViewModel(
- context.applicationContext,
- groupId,
- // Read initial value from preferences
- getShowPlayedItemsFromPreferences(context.applicationContext)
- ) as T
+ private fun getShowFutureItemsFromPreferences(context: Context) =
+ PreferenceManager.getDefaultSharedPreferences(context)
+ .getBoolean(context.getString(R.string.feed_show_future_items_key), true)
+ fun getFactory(context: Context, groupId: Long) = viewModelFactory {
+ initializer {
+ FeedViewModel(
+ App.getApp(),
+ groupId,
+ // Read initial value from preferences
+ getShowPlayedItemsFromPreferences(context.applicationContext),
+ getShowFutureItemsFromPreferences(context.applicationContext)
+ )
+ }
}
}
}
diff --git a/app/src/main/java/org/schabi/newpipe/local/feed/notifications/NotificationHelper.kt b/app/src/main/java/org/schabi/newpipe/local/feed/notifications/NotificationHelper.kt
index 3a08b3e4aa5..351975486fb 100644
--- a/app/src/main/java/org/schabi/newpipe/local/feed/notifications/NotificationHelper.kt
+++ b/app/src/main/java/org/schabi/newpipe/local/feed/notifications/NotificationHelper.kt
@@ -4,6 +4,8 @@ import android.app.NotificationManager
import android.app.PendingIntent
import android.content.Context
import android.content.Intent
+import android.graphics.Bitmap
+import android.graphics.drawable.Drawable
import android.net.Uri
import android.os.Build
import android.provider.Settings
@@ -11,6 +13,8 @@ import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat
import androidx.core.content.ContextCompat
import androidx.preference.PreferenceManager
+import com.squareup.picasso.Picasso
+import com.squareup.picasso.Target
import org.schabi.newpipe.R
import org.schabi.newpipe.extractor.stream.StreamInfoItem
import org.schabi.newpipe.local.feed.service.FeedUpdateInfo
@@ -27,6 +31,8 @@ class NotificationHelper(val context: Context) {
Context.NOTIFICATION_SERVICE
) as NotificationManager
+ private val iconLoadingTargets = ArrayList()
+
/**
* Show a notification about new streams from a single channel.
* Opening the notification will open the corresponding channel page.
@@ -77,10 +83,29 @@ class NotificationHelper(val context: Context) {
)
)
- PicassoHelper.loadNotificationIcon(data.avatarUrl) { bitmap ->
- bitmap?.let { builder.setLargeIcon(it) } // set only if != null
- manager.notify(data.pseudoId, builder.build())
+ // a Target is like a listener for image loading events
+ 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())
+ 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 {
diff --git a/app/src/main/java/org/schabi/newpipe/local/history/HistoryRecordManager.java b/app/src/main/java/org/schabi/newpipe/local/history/HistoryRecordManager.java
index 19f7afce534..b8d2eae2d97 100644
--- a/app/src/main/java/org/schabi/newpipe/local/history/HistoryRecordManager.java
+++ b/app/src/main/java/org/schabi/newpipe/local/history/HistoryRecordManager.java
@@ -28,7 +28,6 @@
import org.schabi.newpipe.R;
import org.schabi.newpipe.database.AppDatabase;
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.StreamHistoryDAO;
import org.schabi.newpipe.database.history.model.SearchHistoryEntry;
@@ -51,7 +50,6 @@
import java.time.OffsetDateTime;
import java.time.ZoneOffset;
import java.util.ArrayList;
-import java.util.Collection;
import java.util.List;
import io.reactivex.rxjava3.core.Completable;
@@ -89,7 +87,6 @@ public HistoryRecordManager(final Context context) {
* 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%.
*
- * @see FeedDAO#getLiveOrNotPlayedStreams
* @see FeedViewModel#togglePlayedItems
* @param info the item to mark as watched
* @return a Maybe containing the ID of the item if successful
@@ -176,10 +173,6 @@ public Single deleteCompleteStreamStateHistory() {
.subscribeOn(Schedulers.io());
}
- public Flowable> getStreamHistory() {
- return streamHistoryTable.getHistory().subscribeOn(Schedulers.io());
- }
-
public Flowable> getStreamHistorySortedById() {
return streamHistoryTable.getHistorySortedById().subscribeOn(Schedulers.io());
}
@@ -188,24 +181,6 @@ public Flowable> getStreamStatistics() {
return streamHistoryTable.getStatistics().subscribeOn(Schedulers.io());
}
- public Single> insertStreamHistory(final Collection entries) {
- final List 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 deleteStreamHistory(final Collection entries) {
- final List 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() {
return sharedPreferences.getBoolean(streamHistoryKey, false);
}
@@ -259,13 +234,6 @@ private boolean isSearchHistoryEnabled() {
// Stream State History
///////////////////////////////////////////////////////
- public Maybe getStreamHistory(final StreamInfo info) {
- return Maybe.fromCallable(() -> {
- final long streamId = streamTable.upsert(new StreamEntity(info));
- return streamHistoryTable.getLatestEntry(streamId);
- }).subscribeOn(Schedulers.io());
- }
-
public Maybe loadStreamState(final PlayQueueItem queueItem) {
return queueItem.getStream()
.map(info -> streamTable.upsert(new StreamEntity(info)))
@@ -311,28 +279,6 @@ public Single loadStreamState(final InfoItem info) {
}).subscribeOn(Schedulers.io());
}
- public Single> loadStreamStateBatch(final List infos) {
- return Single.fromCallable(() -> {
- final List result = new ArrayList<>(infos.size());
- for (final InfoItem info : infos) {
- final List entities = streamTable
- .getStream(info.getServiceId(), info.getUrl()).blockingFirst();
- if (entities.isEmpty()) {
- result.add(null);
- continue;
- }
- final List 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> loadLocalStreamStateBatch(
final List extends LocalItem> items) {
return Single.fromCallable(() -> {
diff --git a/app/src/main/java/org/schabi/newpipe/local/history/StatisticsPlaylistFragment.java b/app/src/main/java/org/schabi/newpipe/local/history/StatisticsPlaylistFragment.java
index 01df342920b..a20a80ae985 100644
--- a/app/src/main/java/org/schabi/newpipe/local/history/StatisticsPlaylistFragment.java
+++ b/app/src/main/java/org/schabi/newpipe/local/history/StatisticsPlaylistFragment.java
@@ -135,7 +135,7 @@ protected ViewBinding getListHeader() {
protected void initListeners() {
super.initListeners();
- itemListAdapter.setSelectedListener(new OnClickGesture() {
+ itemListAdapter.setSelectedListener(new OnClickGesture<>() {
@Override
public void selected(final LocalItem selectedItem) {
if (selectedItem instanceof StreamStatisticsEntry) {
diff --git a/app/src/main/java/org/schabi/newpipe/local/playlist/LocalPlaylistFragment.java b/app/src/main/java/org/schabi/newpipe/local/playlist/LocalPlaylistFragment.java
index 6023d4b10af..11d54f1efd8 100644
--- a/app/src/main/java/org/schabi/newpipe/local/playlist/LocalPlaylistFragment.java
+++ b/app/src/main/java/org/schabi/newpipe/local/playlist/LocalPlaylistFragment.java
@@ -1,5 +1,6 @@
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.util.ThemeHelper.shouldUseGridLayout;
@@ -41,15 +42,16 @@
import org.schabi.newpipe.error.UserAction;
import org.schabi.newpipe.extractor.stream.StreamInfoItem;
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.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.SinglePlayQueue;
import org.schabi.newpipe.util.Localization;
import org.schabi.newpipe.util.NavigationHelper;
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.Collections;
@@ -57,10 +59,12 @@
import java.util.List;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.stream.Collectors;
import icepick.State;
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
import io.reactivex.rxjava3.core.Flowable;
+import io.reactivex.rxjava3.core.Single;
import io.reactivex.rxjava3.disposables.CompositeDisposable;
import io.reactivex.rxjava3.disposables.Disposable;
import io.reactivex.rxjava3.schedulers.Schedulers;
@@ -163,7 +167,7 @@ protected void initListeners() {
itemTouchHelper = new ItemTouchHelper(getItemTouchCallback());
itemTouchHelper.attachToRecyclerView(itemsList);
- itemListAdapter.setSelectedListener(new OnClickGesture() {
+ itemListAdapter.setSelectedListener(new OnClickGesture<>() {
@Override
public void selected(final LocalItem selectedItem) {
if (selectedItem instanceof PlaylistStreamEntry) {
@@ -345,7 +349,11 @@ public void onComplete() {
@Override
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) {
new AlertDialog.Builder(requireContext())
.setMessage(R.string.remove_watched_popup_warning)
@@ -360,14 +368,26 @@ public boolean onOptionsItemSelected(final MenuItem item) {
.create()
.show();
}
- } else if (item.getItemId() == R.id.menu_item_rename_playlist) {
- createRenameDialog();
} else {
return super.onOptionsItemSelected(item);
}
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) {
if (isRemovingWatched) {
return;
@@ -382,8 +402,8 @@ public void removeWatchedStreams(final boolean removePartiallyWatched) {
final Iterator playlistIter = playlist.iterator();
// History data
- final HistoryRecordManager recordManager
- = new HistoryRecordManager(getContext());
+ final HistoryRecordManager recordManager =
+ new HistoryRecordManager(getContext());
final Iterator historyIter = recordManager
.getStreamHistorySortedById().blockingFirst().iterator();
@@ -524,8 +544,8 @@ private void createRenameDialog() {
return;
}
- final DialogEditTextBinding dialogBinding
- = DialogEditTextBinding.inflate(getLayoutInflater());
+ final DialogEditTextBinding dialogBinding =
+ DialogEditTextBinding.inflate(getLayoutInflater());
dialogBinding.dialogEditText.setHint(R.string.name);
dialogBinding.dialogEditText.setInputType(InputType.TYPE_CLASS_TEXT);
dialogBinding.dialogEditText.setSelection(dialogBinding.dialogEditText.getText().length());
@@ -593,7 +613,7 @@ private void updateThumbnailUrl() {
newThumbnailUrl = ((PlaylistStreamEntry) itemListAdapter.getItemsList().get(0))
.getStreamEntity().getThumbnailUrl();
} else {
- newThumbnailUrl = "drawable://" + R.drawable.dummy_thumbnail_playlist;
+ newThumbnailUrl = "drawable://" + R.drawable.placeholder_thumbnail_playlist;
}
changeThumbnailUrl(newThumbnailUrl);
diff --git a/app/src/main/java/org/schabi/newpipe/local/subscription/SubscriptionFragment.kt b/app/src/main/java/org/schabi/newpipe/local/subscription/SubscriptionFragment.kt
index 4295424e695..20f8a01c132 100644
--- a/app/src/main/java/org/schabi/newpipe/local/subscription/SubscriptionFragment.kt
+++ b/app/src/main/java/org/schabi/newpipe/local/subscription/SubscriptionFragment.kt
@@ -346,7 +346,7 @@ class SubscriptionFragment : BaseStateFragment() {
override fun doInitialLoadLogic() = Unit
override fun startLoading(forceLoad: Boolean) = Unit
- private val listenerFeedGroups = object : OnClickGesture>() {
+ private val listenerFeedGroups = object : OnClickGesture> {
override fun selected(selectedItem: Item<*>?) {
when (selectedItem) {
is FeedGroupCardItem -> NavigationHelper.openFeedFragment(fm, selectedItem.groupId, selectedItem.name)
@@ -361,7 +361,7 @@ class SubscriptionFragment : BaseStateFragment() {
}
}
- private val listenerChannelItem = object : OnClickGesture() {
+ private val listenerChannelItem = object : OnClickGesture {
override fun selected(selectedItem: ChannelInfoItem) = NavigationHelper.openChannelFragment(
fm,
selectedItem.serviceId, selectedItem.url, selectedItem.name
diff --git a/app/src/main/java/org/schabi/newpipe/local/subscription/dialog/FeedGroupDialog.kt b/app/src/main/java/org/schabi/newpipe/local/subscription/dialog/FeedGroupDialog.kt
index e9632896195..379b4c0d778 100644
--- a/app/src/main/java/org/schabi/newpipe/local/subscription/dialog/FeedGroupDialog.kt
+++ b/app/src/main/java/org/schabi/newpipe/local/subscription/dialog/FeedGroupDialog.kt
@@ -8,12 +8,10 @@ import android.view.View
import android.view.ViewGroup
import android.view.inputmethod.InputMethodManager
import android.widget.Toast
-import androidx.appcompat.content.res.AppCompatResources
import androidx.core.content.getSystemService
import androidx.core.os.bundleOf
import androidx.core.view.isGone
import androidx.core.view.isVisible
-import androidx.core.widget.ImageViewCompat
import androidx.core.widget.doOnTextChanged
import androidx.fragment.app.DialogFragment
import androidx.lifecycle.Observer
@@ -124,14 +122,6 @@ class FeedGroupDialog : DialogFragment(), BackPressable {
_feedGroupCreateBinding = DialogFeedGroupCreateBinding.bind(view)
_searchLayoutBinding = feedGroupCreateBinding.subscriptionsHeaderSearchContainer
- if (android.os.Build.VERSION.SDK_INT < android.os.Build.VERSION_CODES.LOLLIPOP) {
- // KitKat doesn't apply container's theme to 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(
this,
FeedGroupDialogViewModel.Factory(
diff --git a/app/src/main/java/org/schabi/newpipe/local/subscription/dialog/FeedGroupDialogViewModel.kt b/app/src/main/java/org/schabi/newpipe/local/subscription/dialog/FeedGroupDialogViewModel.kt
index 54ba1c6dc53..dfdb2b47af9 100644
--- a/app/src/main/java/org/schabi/newpipe/local/subscription/dialog/FeedGroupDialogViewModel.kt
+++ b/app/src/main/java/org/schabi/newpipe/local/subscription/dialog/FeedGroupDialogViewModel.kt
@@ -122,7 +122,7 @@ class FeedGroupDialogViewModel(
private val initialShowOnlyUngrouped: Boolean = false
) : ViewModelProvider.Factory {
@Suppress("UNCHECKED_CAST")
- override fun create(modelClass: Class): T {
+ override fun create(modelClass: Class): T {
return FeedGroupDialogViewModel(
context.applicationContext,
groupId, initialQuery, initialShowOnlyUngrouped
diff --git a/app/src/main/java/org/schabi/newpipe/local/subscription/item/ChannelItem.kt b/app/src/main/java/org/schabi/newpipe/local/subscription/item/ChannelItem.kt
index a8c05838f48..bee2e910a8d 100644
--- a/app/src/main/java/org/schabi/newpipe/local/subscription/item/ChannelItem.kt
+++ b/app/src/main/java/org/schabi/newpipe/local/subscription/item/ChannelItem.kt
@@ -39,7 +39,7 @@ class ChannelItem(
itemChannelDescriptionView.text = infoItem.description
}
- PicassoHelper.loadThumbnail(infoItem.thumbnailUrl).into(itemThumbnailView)
+ PicassoHelper.loadAvatar(infoItem.thumbnailUrl).into(itemThumbnailView)
gesturesListener?.run {
viewHolder.root.setOnClickListener { selected(infoItem) }
diff --git a/app/src/main/java/org/schabi/newpipe/local/subscription/services/SubscriptionsExportService.java b/app/src/main/java/org/schabi/newpipe/local/subscription/services/SubscriptionsExportService.java
index 06310359706..d56d16f3cc5 100644
--- a/app/src/main/java/org/schabi/newpipe/local/subscription/services/SubscriptionsExportService.java
+++ b/app/src/main/java/org/schabi/newpipe/local/subscription/services/SubscriptionsExportService.java
@@ -19,6 +19,8 @@
package org.schabi.newpipe.local.subscription.services;
+import static org.schabi.newpipe.MainActivity.DEBUG;
+
import android.content.Intent;
import android.net.Uri;
import android.util.Log;
@@ -43,8 +45,6 @@
import io.reactivex.rxjava3.functions.Function;
import io.reactivex.rxjava3.schedulers.Schedulers;
-import static org.schabi.newpipe.MainActivity.DEBUG;
-
public class SubscriptionsExportService extends BaseImportExportService {
public static final String KEY_FILE_PATH = "key_file_path";
@@ -109,8 +109,8 @@ private void startExport() {
subscriptionManager.subscriptionTable().getAll().take(1)
.map(subscriptionEntities -> {
- final List result
- = new ArrayList<>(subscriptionEntities.size());
+ final List result =
+ new ArrayList<>(subscriptionEntities.size());
for (final SubscriptionEntity entity : subscriptionEntities) {
result.add(new SubscriptionItem(entity.getServiceId(), entity.getUrl(),
entity.getName()));
diff --git a/app/src/main/java/org/schabi/newpipe/player/MainPlayer.java b/app/src/main/java/org/schabi/newpipe/player/MainPlayer.java
deleted file mode 100644
index a9b9f4c8762..00000000000
--- a/app/src/main/java/org/schabi/newpipe/player/MainPlayer.java
+++ /dev/null
@@ -1,259 +0,0 @@
-/*
- * Copyright 2017 Mauricio Colli
- * 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 .
- */
-
-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;
- }
- }
-}
diff --git a/app/src/main/java/org/schabi/newpipe/player/PlayQueueActivity.java b/app/src/main/java/org/schabi/newpipe/player/PlayQueueActivity.java
index 676d634584a..c18a7f4874d 100644
--- a/app/src/main/java/org/schabi/newpipe/player/PlayQueueActivity.java
+++ b/app/src/main/java/org/schabi/newpipe/player/PlayQueueActivity.java
@@ -29,6 +29,7 @@
import org.schabi.newpipe.databinding.ActivityPlayerQueueControlBinding;
import org.schabi.newpipe.extractor.stream.StreamInfo;
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.helper.PlaybackParameterDialog;
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;
- protected Player player;
+ private Player player;
private boolean serviceBound;
private ServiceConnection serviceConnection;
@@ -126,13 +127,13 @@ public boolean onOptionsItemSelected(final MenuItem item) {
NavigationHelper.openSettings(this);
return true;
case R.id.action_append_playlist:
- player.onAddToPlaylistClicked(getSupportFragmentManager());
+ PlaylistDialog.showForPlayQueue(player, getSupportFragmentManager());
return true;
case R.id.action_playback_speed:
openPlaybackParameterDialog();
return true;
case R.id.action_mute:
- player.onMuteUnmuteButtonClicked();
+ player.toggleMute();
return true;
case R.id.action_system_audio:
startActivity(new Intent(Settings.ACTION_SOUND_SETTINGS));
@@ -168,7 +169,7 @@ protected void onDestroy() {
////////////////////////////////////////////////////////////////////////////
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);
if (!success) {
unbindService(serviceConnection);
@@ -184,10 +185,7 @@ private void unbind() {
player.removeActivityListener(this);
}
- if (player != null && player.getPlayQueueAdapter() != null) {
- player.getPlayQueueAdapter().unsetSelectedListener();
- }
- queueControlBinding.playQueue.setAdapter(null);
+ onQueueUpdate(null);
if (itemTouchHelper != null) {
itemTouchHelper.attachToRecyclerView(null);
}
@@ -208,17 +206,15 @@ public void onServiceDisconnected(final ComponentName name) {
public void onServiceConnected(final ComponentName name, final IBinder service) {
Log.d(TAG, "Player service is connected");
- if (service instanceof PlayerServiceBinder) {
- player = ((PlayerServiceBinder) service).getPlayerInstance();
- } else if (service instanceof MainPlayer.LocalBinder) {
- player = ((MainPlayer.LocalBinder) service).getPlayer();
+ if (service instanceof PlayerService.LocalBinder) {
+ player = ((PlayerService.LocalBinder) service).getPlayer();
}
- if (player == null || player.getPlayQueue() == null
- || player.getPlayQueueAdapter() == null || player.exoPlayerIsNull()) {
+ if (player == null || player.getPlayQueue() == null || player.exoPlayerIsNull()) {
unbind();
finish();
} else {
+ onQueueUpdate(player.getPlayQueue());
buildComponents();
if (player != null) {
player.setActivityListener(PlayQueueActivity.this);
@@ -241,7 +237,6 @@ private void buildComponents() {
private void buildQueue() {
queueControlBinding.playQueue.setLayoutManager(new LinearLayoutManager(this));
- queueControlBinding.playQueue.setAdapter(player.getPlayQueueAdapter());
queueControlBinding.playQueue.setClickable(true);
queueControlBinding.playQueue.setLongClickable(true);
queueControlBinding.playQueue.clearOnScrollListeners();
@@ -249,8 +244,6 @@ private void buildQueue() {
itemTouchHelper = new ItemTouchHelper(getItemTouchCallback());
itemTouchHelper.attachToRecyclerView(queueControlBinding.playQueue);
-
- player.getPlayQueueAdapter().setSelectedListener(getOnSelectedListener());
}
private void buildMetadata() {
@@ -370,7 +363,7 @@ public void onClick(final View view) {
}
if (view.getId() == queueControlBinding.controlRepeat.getId()) {
- player.onRepeatClicked();
+ player.cycleNextRepeatMode();
} else if (view.getId() == queueControlBinding.controlBackward.getId()) {
player.playPrevious();
} else if (view.getId() == queueControlBinding.controlFastRewind.getId()) {
@@ -382,7 +375,7 @@ public void onClick(final View view) {
} else if (view.getId() == queueControlBinding.controlForward.getId()) {
player.playNext();
} else if (view.getId() == queueControlBinding.controlShuffle.getId()) {
- player.onShuffleClicked();
+ player.toggleShuffleModeEnabled();
} else if (view.getId() == queueControlBinding.metadata.getId()) {
scrollToSelected();
} else if (view.getId() == queueControlBinding.liveSync.getId()) {
@@ -445,7 +438,14 @@ public void onStopTrackingTouch(final SeekBar seekBar) {
////////////////////////////////////////////////////////////////////////////
@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
@@ -454,7 +454,6 @@ public void onPlaybackUpdate(final int state, final int repeatMode, final boolea
onStateChanged(state);
onPlayModeChanged(repeatMode, shuffled);
onPlaybackParameterChanged(parameters);
- onMaybePlaybackAdapterChanged();
onMaybeMuteChanged();
}
@@ -582,17 +581,6 @@ private void onPlaybackParameterChanged(@Nullable final PlaybackParameters param
}
}
- 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() {
if (menu != null && player != null) {
final MenuItem item = menu.findItem(R.id.action_mute);
diff --git a/app/src/main/java/org/schabi/newpipe/player/Player.java b/app/src/main/java/org/schabi/newpipe/player/Player.java
index b3194afe6ce..99d36f66eea 100644
--- a/app/src/main/java/org/schabi/newpipe/player/Player.java
+++ b/app/src/main/java/org/schabi/newpipe/player/Player.java
@@ -24,196 +24,106 @@
import static com.google.android.exoplayer2.Player.DISCONTINUITY_REASON_SKIP;
import static com.google.android.exoplayer2.Player.DiscontinuityReason;
import static com.google.android.exoplayer2.Player.Listener;
-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_ONE;
import static com.google.android.exoplayer2.Player.RepeatMode;
-import static org.schabi.newpipe.QueueItemMenuUtil.openPopupMenu;
import static org.schabi.newpipe.extractor.ServiceList.YouTube;
import static org.schabi.newpipe.extractor.utils.Utils.isNullOrEmpty;
-import static org.schabi.newpipe.ktx.ViewUtils.animate;
-import static org.schabi.newpipe.ktx.ViewUtils.animateRotation;
-import static org.schabi.newpipe.player.MainPlayer.ACTION_CLOSE;
-import static org.schabi.newpipe.player.MainPlayer.ACTION_FAST_FORWARD;
-import static org.schabi.newpipe.player.MainPlayer.ACTION_FAST_REWIND;
-import static org.schabi.newpipe.player.MainPlayer.ACTION_PLAY_NEXT;
-import static org.schabi.newpipe.player.MainPlayer.ACTION_PLAY_PAUSE;
-import static org.schabi.newpipe.player.MainPlayer.ACTION_PLAY_PREVIOUS;
-import static org.schabi.newpipe.player.MainPlayer.ACTION_RECREATE_NOTIFICATION;
-import static org.schabi.newpipe.player.MainPlayer.ACTION_REPEAT;
-import static org.schabi.newpipe.player.MainPlayer.ACTION_SHUFFLE;
-import static org.schabi.newpipe.player.helper.PlayerHelper.MinimizeMode.MINIMIZE_ON_EXIT_MODE_BACKGROUND;
-import static org.schabi.newpipe.player.helper.PlayerHelper.MinimizeMode.MINIMIZE_ON_EXIT_MODE_NONE;
-import static org.schabi.newpipe.player.helper.PlayerHelper.MinimizeMode.MINIMIZE_ON_EXIT_MODE_POPUP;
-import static org.schabi.newpipe.player.helper.PlayerHelper.buildCloseOverlayLayoutParams;
-import static org.schabi.newpipe.player.helper.PlayerHelper.formatSpeed;
-import static org.schabi.newpipe.player.helper.PlayerHelper.getMinimizeOnExitAction;
-import static org.schabi.newpipe.player.helper.PlayerHelper.getMinimumVideoHeight;
-import static org.schabi.newpipe.player.helper.PlayerHelper.getTimeString;
-import static org.schabi.newpipe.player.helper.PlayerHelper.globalScreenOrientationLocked;
import static org.schabi.newpipe.player.helper.PlayerHelper.isPlaybackResumeEnabled;
import static org.schabi.newpipe.player.helper.PlayerHelper.nextRepeatMode;
-import static org.schabi.newpipe.player.helper.PlayerHelper.nextResizeModeAndSaveToPrefs;
import static org.schabi.newpipe.player.helper.PlayerHelper.retrievePlaybackParametersFromPrefs;
-import static org.schabi.newpipe.player.helper.PlayerHelper.retrievePlayerTypeFromIntent;
-import static org.schabi.newpipe.player.helper.PlayerHelper.retrievePopupLayoutParamsFromPrefs;
import static org.schabi.newpipe.player.helper.PlayerHelper.retrieveSeekDurationFromPreferences;
import static org.schabi.newpipe.player.helper.PlayerHelper.savePlaybackParametersToPrefs;
+import static org.schabi.newpipe.player.notification.NotificationConstants.ACTION_CLOSE;
+import static org.schabi.newpipe.player.notification.NotificationConstants.ACTION_FAST_FORWARD;
+import static org.schabi.newpipe.player.notification.NotificationConstants.ACTION_FAST_REWIND;
+import static org.schabi.newpipe.player.notification.NotificationConstants.ACTION_PLAY_NEXT;
+import static org.schabi.newpipe.player.notification.NotificationConstants.ACTION_PLAY_PAUSE;
+import static org.schabi.newpipe.player.notification.NotificationConstants.ACTION_PLAY_PREVIOUS;
+import static org.schabi.newpipe.player.notification.NotificationConstants.ACTION_RECREATE_NOTIFICATION;
+import static org.schabi.newpipe.player.notification.NotificationConstants.ACTION_REPEAT;
+import static org.schabi.newpipe.player.notification.NotificationConstants.ACTION_SHUFFLE;
import static org.schabi.newpipe.util.ListHelper.getPopupResolutionIndex;
import static org.schabi.newpipe.util.ListHelper.getResolutionIndex;
import static org.schabi.newpipe.util.Localization.assureCorrectAppLanguage;
import static java.util.concurrent.TimeUnit.MILLISECONDS;
-import android.animation.Animator;
-import android.animation.AnimatorListenerAdapter;
-import android.annotation.SuppressLint;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.SharedPreferences;
-import android.content.res.Resources;
-import android.database.ContentObserver;
import android.graphics.Bitmap;
-import android.graphics.BitmapFactory;
-import android.graphics.Color;
-import android.graphics.PorterDuff;
-import android.graphics.PorterDuffColorFilter;
import android.graphics.drawable.Drawable;
import android.media.AudioManager;
-import android.net.Uri;
-import android.os.Build;
-import android.os.Handler;
-import android.provider.Settings;
-import android.util.DisplayMetrics;
import android.util.Log;
-import android.util.TypedValue;
-import android.view.GestureDetector;
-import android.view.Gravity;
-import android.view.KeyEvent;
import android.view.LayoutInflater;
-import android.view.Menu;
-import android.view.MenuItem;
-import android.view.MotionEvent;
-import android.view.Surface;
-import android.view.View;
-import android.view.ViewGroup;
-import android.view.WindowManager;
-import android.view.animation.AnticipateInterpolator;
-import android.widget.FrameLayout;
-import android.widget.ImageButton;
-import android.widget.ImageView;
-import android.widget.LinearLayout;
-import android.widget.ProgressBar;
-import android.widget.RelativeLayout;
-import android.widget.SeekBar;
-import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
-import androidx.appcompat.app.AppCompatActivity;
-import androidx.appcompat.content.res.AppCompatResources;
-import androidx.appcompat.view.ContextThemeWrapper;
-import androidx.appcompat.widget.AppCompatImageButton;
-import androidx.appcompat.widget.PopupMenu;
-import androidx.collection.ArraySet;
-import androidx.core.content.ContextCompat;
-import androidx.core.graphics.Insets;
-import androidx.core.view.ViewCompat;
-import androidx.core.view.WindowInsetsCompat;
-import androidx.fragment.app.FragmentManager;
+import androidx.core.math.MathUtils;
import androidx.preference.PreferenceManager;
-import androidx.recyclerview.widget.ItemTouchHelper;
-import androidx.recyclerview.widget.RecyclerView;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.DefaultRenderersFactory;
import com.google.android.exoplayer2.ExoPlayer;
-import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.PlaybackException;
import com.google.android.exoplayer2.PlaybackParameters;
import com.google.android.exoplayer2.Player.PositionInfo;
import com.google.android.exoplayer2.RenderersFactory;
import com.google.android.exoplayer2.Timeline;
-import com.google.android.exoplayer2.TracksInfo;
+import com.google.android.exoplayer2.Tracks;
import com.google.android.exoplayer2.source.MediaSource;
-import com.google.android.exoplayer2.text.Cue;
+import com.google.android.exoplayer2.text.CueGroup;
import com.google.android.exoplayer2.trackselection.DefaultTrackSelector;
import com.google.android.exoplayer2.trackselection.MappingTrackSelector;
-import com.google.android.exoplayer2.ui.AspectRatioFrameLayout;
-import com.google.android.exoplayer2.ui.CaptionStyleCompat;
-import com.google.android.exoplayer2.ui.SubtitleView;
import com.google.android.exoplayer2.upstream.DefaultBandwidthMeter;
import com.google.android.exoplayer2.util.Util;
import com.google.android.exoplayer2.video.VideoSize;
-import com.google.android.material.floatingactionbutton.FloatingActionButton;
import com.squareup.picasso.Picasso;
import com.squareup.picasso.Target;
import org.schabi.newpipe.MainActivity;
import org.schabi.newpipe.R;
-import org.schabi.newpipe.database.stream.model.StreamEntity;
import org.schabi.newpipe.databinding.PlayerBinding;
-import org.schabi.newpipe.databinding.PlayerPopupCloseOverlayBinding;
import org.schabi.newpipe.error.ErrorInfo;
import org.schabi.newpipe.error.ErrorUtil;
import org.schabi.newpipe.error.UserAction;
-import org.schabi.newpipe.extractor.Info;
-import org.schabi.newpipe.extractor.MediaFormat;
import org.schabi.newpipe.extractor.stream.StreamInfo;
-import org.schabi.newpipe.extractor.stream.StreamSegment;
import org.schabi.newpipe.extractor.stream.StreamType;
import org.schabi.newpipe.extractor.stream.VideoStream;
-import org.schabi.newpipe.fragments.OnScrollBelowItemsListener;
import org.schabi.newpipe.fragments.detail.VideoDetailFragment;
-import org.schabi.newpipe.info_list.StreamSegmentAdapter;
-import org.schabi.newpipe.ktx.AnimationType;
-import org.schabi.newpipe.local.dialog.PlaylistDialog;
import org.schabi.newpipe.local.history.HistoryRecordManager;
-import org.schabi.newpipe.player.MainPlayer.PlayerType;
-import org.schabi.newpipe.player.event.DisplayPortion;
import org.schabi.newpipe.player.event.PlayerEventListener;
-import org.schabi.newpipe.player.event.PlayerGestureListener;
import org.schabi.newpipe.player.event.PlayerServiceEventListener;
import org.schabi.newpipe.player.helper.AudioReactor;
import org.schabi.newpipe.player.helper.LoadController;
-import org.schabi.newpipe.player.helper.MediaSessionManager;
import org.schabi.newpipe.player.helper.PlayerDataSource;
import org.schabi.newpipe.player.helper.PlayerHelper;
-import org.schabi.newpipe.player.listeners.view.PlaybackSpeedClickListener;
-import org.schabi.newpipe.player.listeners.view.QualityClickListener;
import org.schabi.newpipe.player.mediaitem.MediaItemTag;
+import org.schabi.newpipe.player.mediasession.MediaSessionPlayerUi;
+import org.schabi.newpipe.player.notification.NotificationPlayerUi;
import org.schabi.newpipe.player.playback.MediaSourceManager;
import org.schabi.newpipe.player.playback.PlaybackListener;
-import org.schabi.newpipe.player.playback.PlayerMediaSession;
-import org.schabi.newpipe.player.playback.SurfaceHolderCallback;
import org.schabi.newpipe.player.playqueue.PlayQueue;
-import org.schabi.newpipe.player.playqueue.PlayQueueAdapter;
import org.schabi.newpipe.player.playqueue.PlayQueueItem;
-import org.schabi.newpipe.player.playqueue.PlayQueueItemBuilder;
-import org.schabi.newpipe.player.playqueue.PlayQueueItemHolder;
-import org.schabi.newpipe.player.playqueue.PlayQueueItemTouchCallback;
import org.schabi.newpipe.player.resolver.AudioPlaybackResolver;
import org.schabi.newpipe.player.resolver.VideoPlaybackResolver;
import org.schabi.newpipe.player.resolver.VideoPlaybackResolver.SourceType;
-import org.schabi.newpipe.player.seekbarpreview.SeekbarPreviewThumbnailHelper;
-import org.schabi.newpipe.player.seekbarpreview.SeekbarPreviewThumbnailHolder;
+import org.schabi.newpipe.player.ui.MainPlayerUi;
+import org.schabi.newpipe.player.ui.PlayerUi;
+import org.schabi.newpipe.player.ui.PlayerUiList;
+import org.schabi.newpipe.player.ui.PopupPlayerUi;
+import org.schabi.newpipe.player.ui.VideoPlayerUi;
import org.schabi.newpipe.util.DeviceUtils;
import org.schabi.newpipe.util.ListHelper;
import org.schabi.newpipe.util.NavigationHelper;
import org.schabi.newpipe.util.PicassoHelper;
import org.schabi.newpipe.util.SerializedCache;
import org.schabi.newpipe.util.StreamTypeUtil;
-import org.schabi.newpipe.util.external_communication.KoreUtils;
-import org.schabi.newpipe.util.external_communication.ShareUtils;
-import org.schabi.newpipe.views.ExpandableSurfaceView;
-import org.schabi.newpipe.views.player.PlayerFastSeekOverlay;
-import java.util.Collections;
import java.util.List;
-import java.util.Objects;
import java.util.Optional;
-import java.util.stream.Collectors;
import java.util.stream.IntStream;
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
@@ -222,14 +132,7 @@
import io.reactivex.rxjava3.disposables.Disposable;
import io.reactivex.rxjava3.disposables.SerialDisposable;
-public final class Player implements
- PlaybackListener,
- Listener,
- SeekBar.OnSeekBarChangeListener,
- View.OnClickListener,
- PopupMenu.OnMenuItemClickListener,
- PopupMenu.OnDismissListener,
- View.OnLongClickListener {
+public final class Player implements PlaybackListener, Listener {
public static final boolean DEBUG = MainActivity.DEBUG;
public static final String TAG = Player.class.getSimpleName();
@@ -265,18 +168,13 @@ public final class Player implements
public static final int PLAY_PREV_ACTIVATION_LIMIT_MILLIS = 5000; // 5 seconds
public static final int PROGRESS_LOOP_INTERVAL_MILLIS = 1000; // 1 second
- public static final int DEFAULT_CONTROLS_DURATION = 300; // 300 millis
- public static final int DEFAULT_CONTROLS_HIDE_TIME = 2000; // 2 Seconds
- public static final int DPAD_CONTROLS_HIDE_TIME = 7000; // 7 Seconds
- public static final int SEEK_OVERLAY_DURATION = 450; // 450 millis
/*//////////////////////////////////////////////////////////////////////////
// Other constants
//////////////////////////////////////////////////////////////////////////*/
- private static final float[] PLAYBACK_SPEEDS = {0.5f, 0.75f, 1.0f, 1.25f, 1.5f, 1.75f, 2.0f};
-
- private static final int RENDERER_UNAVAILABLE = -1;
+ public static final int RENDERER_UNAVAILABLE = -1;
+ private static final String PICASSO_PLAYER_THUMBNAIL_TAG = "PICASSO_PLAYER_THUMBNAIL_TAG";
/*//////////////////////////////////////////////////////////////////////////
// Playback
@@ -284,8 +182,6 @@ public final class Player implements
// play queue might be null e.g. while player is starting
@Nullable private PlayQueue playQueue;
- private PlayQueueAdapter playQueueAdapter;
- private StreamSegmentAdapter segmentAdapter;
@Nullable private MediaSourceManager playQueueManager;
@@ -299,8 +195,6 @@ public final class Player implements
private ExoPlayer simpleExoPlayer;
private AudioReactor audioReactor;
- private MediaSessionManager mediaSessionManager;
- @Nullable private SurfaceHolderCallback surfaceHolderCallback;
@NonNull private final DefaultTrackSelector trackSelector;
@NonNull private final LoadController loadController;
@@ -309,13 +203,13 @@ public final class Player implements
@NonNull private final VideoPlaybackResolver videoResolver;
@NonNull private final AudioPlaybackResolver audioResolver;
- private final MainPlayer service; //TODO try to remove and replace everything with context
+ private final PlayerService service; //TODO try to remove and replace everything with context
/*//////////////////////////////////////////////////////////////////////////
// Player states
//////////////////////////////////////////////////////////////////////////*/
- private PlayerType playerType = PlayerType.VIDEO;
+ private PlayerType playerType = PlayerType.MAIN;
private int currentState = STATE_PREFLIGHT;
// audio only mode does not mean that player type is background, but that the player was
@@ -323,85 +217,27 @@ public final class Player implements
private boolean isAudioOnly = false;
private boolean isPrepared = false;
private boolean wasPlaying = false;
- private boolean isFullscreen = false;
- private boolean isVerticalVideo = false;
- private boolean fragmentIsVisible = false;
-
- private List availableStreams;
- private int selectedStreamIndex;
-
- /*//////////////////////////////////////////////////////////////////////////
- // Views
- //////////////////////////////////////////////////////////////////////////*/
-
- private PlayerBinding binding;
-
- private final Handler controlsVisibilityHandler = new Handler();
-
- // fullscreen player
- private boolean isQueueVisible = false;
- private boolean areSegmentsVisible = false;
- private ItemTouchHelper itemTouchHelper;
-
- /*//////////////////////////////////////////////////////////////////////////
- // Popup menus ("popup" means that they pop up, not that they belong to the popup player)
- //////////////////////////////////////////////////////////////////////////*/
-
- private static final int POPUP_MENU_ID_QUALITY = 69;
- private static final int POPUP_MENU_ID_PLAYBACK_SPEED = 79;
- private static final int POPUP_MENU_ID_CAPTION = 89;
-
- private boolean isSomePopupMenuVisible = false;
- private PopupMenu qualityPopupMenu;
- private PopupMenu playbackSpeedPopupMenu;
- private PopupMenu captionPopupMenu;
-
- /*//////////////////////////////////////////////////////////////////////////
- // Popup player
- //////////////////////////////////////////////////////////////////////////*/
-
- private PlayerPopupCloseOverlayBinding closeOverlayBinding;
-
- private boolean isPopupClosing = false;
-
- private float screenWidth;
- private float screenHeight;
-
- /*//////////////////////////////////////////////////////////////////////////
- // Popup player window manager
- //////////////////////////////////////////////////////////////////////////*/
-
- public static final int IDLE_WINDOW_FLAGS = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
- | WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM;
- public static final int ONGOING_PLAYBACK_WINDOW_FLAGS = IDLE_WINDOW_FLAGS
- | WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON;
-
- @Nullable private WindowManager.LayoutParams popupLayoutParams; // null if player is not popup
- @Nullable private final WindowManager windowManager;
/*//////////////////////////////////////////////////////////////////////////
- // Gestures
+ // UIs, listeners and disposables
//////////////////////////////////////////////////////////////////////////*/
- private static final float MAX_GESTURE_LENGTH = 0.75f;
-
- private int maxGestureLength; // scaled
- private GestureDetector gestureDetector;
- private PlayerGestureListener playerGestureListener;
-
- /*//////////////////////////////////////////////////////////////////////////
- // Listeners and disposables
- //////////////////////////////////////////////////////////////////////////*/
+ @SuppressWarnings({"MemberName", "java:S116"}) // keep the unusual member name
+ private final PlayerUiList UIs;
private BroadcastReceiver broadcastReceiver;
private IntentFilter intentFilter;
- private PlayerServiceEventListener fragmentListener;
- private PlayerEventListener activityListener;
- private ContentObserver settingsContentObserver;
+ @Nullable private PlayerServiceEventListener fragmentListener = null;
+ @Nullable private PlayerEventListener activityListener = null;
@NonNull private final SerialDisposable progressUpdateDisposable = new SerialDisposable();
@NonNull private final CompositeDisposable databaseUpdateDisposable = new CompositeDisposable();
+ // This is the only listener we need for thumbnail loading, since there is always at most only
+ // one thumbnail being loaded at a time. This field is also here to maintain a strong reference,
+ // which would otherwise be garbage collected since Picasso holds weak references to targets.
+ @NonNull private final Target currentThumbnailTarget;
+
/*//////////////////////////////////////////////////////////////////////////
// Utils
//////////////////////////////////////////////////////////////////////////*/
@@ -410,16 +246,13 @@ public final class Player implements
@NonNull private final SharedPreferences prefs;
@NonNull private final HistoryRecordManager recordManager;
- @NonNull private final SeekbarPreviewThumbnailHolder seekbarPreviewThumbnailHolder =
- new SeekbarPreviewThumbnailHolder();
-
/*//////////////////////////////////////////////////////////////////////////
// Constructor
//////////////////////////////////////////////////////////////////////////*/
//region Constructor
- public Player(@NonNull final MainPlayer service) {
+ public Player(@NonNull final PlayerService service) {
this.service = service;
context = service;
prefs = PreferenceManager.getDefaultSharedPreferences(context);
@@ -436,7 +269,16 @@ public Player(@NonNull final MainPlayer service) {
videoResolver = new VideoPlaybackResolver(context, dataSource, getQualityResolver());
audioResolver = new AudioPlaybackResolver(context, dataSource);
- windowManager = ContextCompat.getSystemService(context, WindowManager.class);
+ currentThumbnailTarget = getCurrentThumbnailTarget();
+
+ // The UIs added here should always be present. They will be initialized when the player
+ // reaches the initialization step. Make sure the media session ui is before the
+ // notification ui in the UIs list, since the notification depends on the media session in
+ // PlayerUi#initPlayer(), and UIs.call() guarantees UI order is preserved.
+ UIs = new PlayerUiList(
+ new MediaSessionPlayerUi(this),
+ new NotificationPlayerUi(this)
+ );
}
private VideoPlaybackResolver.QualityResolver getQualityResolver() {
@@ -461,234 +303,6 @@ public int getOverrideResolutionIndex(final List sortedVideos,
- /*//////////////////////////////////////////////////////////////////////////
- // Setup and initialization
- //////////////////////////////////////////////////////////////////////////*/
- //region Setup and initialization
-
- public void setupFromView(@NonNull final PlayerBinding playerBinding) {
- initViews(playerBinding);
- if (exoPlayerIsNull()) {
- initPlayer(true);
- }
- initListeners();
-
- setupPlayerSeekOverlay();
- }
-
- private void initViews(@NonNull final PlayerBinding playerBinding) {
- binding = playerBinding;
- setupSubtitleView();
-
- binding.resizeTextView
- .setText(PlayerHelper.resizeTypeOf(context, binding.surfaceView.getResizeMode()));
-
- binding.playbackSeekBar.getThumb()
- .setColorFilter(new PorterDuffColorFilter(Color.RED, PorterDuff.Mode.SRC_IN));
- binding.playbackSeekBar.getProgressDrawable()
- .setColorFilter(new PorterDuffColorFilter(Color.RED, PorterDuff.Mode.MULTIPLY));
-
- final ContextThemeWrapper themeWrapper = new ContextThemeWrapper(getContext(),
- R.style.DarkPopupMenu);
-
- qualityPopupMenu = new PopupMenu(themeWrapper, binding.qualityTextView);
- playbackSpeedPopupMenu = new PopupMenu(context, binding.playbackSpeed);
- captionPopupMenu = new PopupMenu(themeWrapper, binding.captionTextView);
-
- binding.progressBarLoadingPanel.getIndeterminateDrawable()
- .setColorFilter(new PorterDuffColorFilter(Color.WHITE, PorterDuff.Mode.MULTIPLY));
-
- binding.titleTextView.setSelected(true);
- binding.channelTextView.setSelected(true);
-
- // Prevent hiding of bottom sheet via swipe inside queue
- binding.itemsList.setNestedScrollingEnabled(false);
- }
-
- private void initPlayer(final boolean playOnReady) {
- if (DEBUG) {
- Log.d(TAG, "initPlayer() called with: playOnReady = [" + playOnReady + "]");
- }
-
- simpleExoPlayer = new ExoPlayer.Builder(context, renderFactory)
- .setTrackSelector(trackSelector)
- .setLoadControl(loadController)
- .build();
- simpleExoPlayer.addListener(this);
- simpleExoPlayer.setPlayWhenReady(playOnReady);
- simpleExoPlayer.setSeekParameters(PlayerHelper.getSeekParameters(context));
- simpleExoPlayer.setWakeMode(C.WAKE_MODE_NETWORK);
- simpleExoPlayer.setHandleAudioBecomingNoisy(true);
-
- audioReactor = new AudioReactor(context, simpleExoPlayer);
- mediaSessionManager = new MediaSessionManager(context, simpleExoPlayer,
- new PlayerMediaSession(this));
-
- registerBroadcastReceiver();
-
- // Setup video view
- setupVideoSurface();
-
- // enable media tunneling
- if (DEBUG && PreferenceManager.getDefaultSharedPreferences(context)
- .getBoolean(context.getString(R.string.disable_media_tunneling_key), false)) {
- Log.d(TAG, "[" + Util.DEVICE_DEBUG_INFO + "] "
- + "media tunneling disabled in debug preferences");
- } else if (DeviceUtils.shouldSupportMediaTunneling()) {
- trackSelector.setParameters(trackSelector.buildUponParameters()
- .setTunnelingEnabled(true));
- } else if (DEBUG) {
- Log.d(TAG, "[" + Util.DEVICE_DEBUG_INFO + "] does not support media tunneling");
- }
- }
-
- private void initListeners() {
- binding.qualityTextView.setOnClickListener(
- new QualityClickListener(this, qualityPopupMenu));
- binding.playbackSpeed.setOnClickListener(
- new PlaybackSpeedClickListener(this, playbackSpeedPopupMenu));
-
- binding.playbackSeekBar.setOnSeekBarChangeListener(this);
- binding.captionTextView.setOnClickListener(this);
- binding.resizeTextView.setOnClickListener(this);
- binding.playbackLiveSync.setOnClickListener(this);
-
- playerGestureListener = new PlayerGestureListener(this, service);
- gestureDetector = new GestureDetector(context, playerGestureListener);
- binding.getRoot().setOnTouchListener(playerGestureListener);
-
- binding.queueButton.setOnClickListener(v -> onQueueClicked());
- binding.segmentsButton.setOnClickListener(v -> onSegmentsClicked());
- binding.repeatButton.setOnClickListener(v -> onRepeatClicked());
- binding.shuffleButton.setOnClickListener(v -> onShuffleClicked());
- binding.addToPlaylistButton.setOnClickListener(v -> {
- if (getParentActivity() != null) {
- onAddToPlaylistClicked(getParentActivity().getSupportFragmentManager());
- }
- });
-
- binding.playPauseButton.setOnClickListener(this);
- binding.playPreviousButton.setOnClickListener(this);
- binding.playNextButton.setOnClickListener(this);
-
- binding.moreOptionsButton.setOnClickListener(this);
- binding.moreOptionsButton.setOnLongClickListener(this);
- binding.share.setOnClickListener(this);
- binding.share.setOnLongClickListener(this);
- binding.fullScreenButton.setOnClickListener(this);
- binding.screenRotationButton.setOnClickListener(this);
- binding.playWithKodi.setOnClickListener(this);
- binding.openInBrowser.setOnClickListener(this);
- binding.playerCloseButton.setOnClickListener(this);
- binding.switchMute.setOnClickListener(this);
-
- settingsContentObserver = new ContentObserver(new Handler()) {
- @Override
- public void onChange(final boolean selfChange) {
- setupScreenRotationButton();
- }
- };
- context.getContentResolver().registerContentObserver(
- Settings.System.getUriFor(Settings.System.ACCELEROMETER_ROTATION), false,
- settingsContentObserver);
- binding.getRoot().addOnLayoutChangeListener(this::onLayoutChange);
-
- ViewCompat.setOnApplyWindowInsetsListener(binding.itemsListPanel, (view, windowInsets) -> {
- final Insets cutout = windowInsets.getInsets(WindowInsetsCompat.Type.displayCutout());
- if (!cutout.equals(Insets.NONE)) {
- view.setPadding(cutout.left, cutout.top, cutout.right, cutout.bottom);
- }
- return windowInsets;
- });
-
- // PlaybackControlRoot already consumed window insets but we should pass them to
- // player_overlays and fast_seek_overlay too. Without it they will be off-centered.
- binding.playbackControlRoot.addOnLayoutChangeListener(
- (v, left, top, right, bottom, oldLeft, oldTop, oldRight, oldBottom) -> {
- binding.playerOverlays.setPadding(
- v.getPaddingLeft(),
- v.getPaddingTop(),
- v.getPaddingRight(),
- v.getPaddingBottom());
-
- // If we added padding to the fast seek overlay, too, it would not go under the
- // system ui. Instead we apply negative margins equal to the window insets of
- // the opposite side, so that the view covers all of the player (overflowing on
- // some sides) and its center coincides with the center of other controls.
- final RelativeLayout.LayoutParams fastSeekParams = (RelativeLayout.LayoutParams)
- binding.fastSeekOverlay.getLayoutParams();
- fastSeekParams.leftMargin = -v.getPaddingRight();
- fastSeekParams.topMargin = -v.getPaddingBottom();
- fastSeekParams.rightMargin = -v.getPaddingLeft();
- fastSeekParams.bottomMargin = -v.getPaddingTop();
- });
- }
-
- /**
- * Initializes the Fast-For/Backward overlay.
- */
- private void setupPlayerSeekOverlay() {
- binding.fastSeekOverlay
- .seekSecondsSupplier(() -> retrieveSeekDurationFromPreferences(this) / 1000)
- .performListener(new PlayerFastSeekOverlay.PerformListener() {
-
- @Override
- public void onDoubleTap() {
- animate(binding.fastSeekOverlay, true, SEEK_OVERLAY_DURATION);
- }
-
- @Override
- public void onDoubleTapEnd() {
- animate(binding.fastSeekOverlay, false, SEEK_OVERLAY_DURATION);
- }
-
- @NonNull
- @Override
- public FastSeekDirection getFastSeekDirection(
- @NonNull final DisplayPortion portion
- ) {
- if (exoPlayerIsNull()) {
- // Abort seeking
- playerGestureListener.endMultiDoubleTap();
- return FastSeekDirection.NONE;
- }
- if (portion == DisplayPortion.LEFT) {
- // Check if it's possible to rewind
- // Small puffer to eliminate infinite rewind seeking
- if (simpleExoPlayer.getCurrentPosition() < 500L) {
- return FastSeekDirection.NONE;
- }
- return FastSeekDirection.BACKWARD;
- } else if (portion == DisplayPortion.RIGHT) {
- // Check if it's possible to fast-forward
- if (currentState == STATE_COMPLETED
- || simpleExoPlayer.getCurrentPosition()
- >= simpleExoPlayer.getDuration()) {
- return FastSeekDirection.NONE;
- }
- return FastSeekDirection.FORWARD;
- }
- /* portion == DisplayPortion.MIDDLE */
- return FastSeekDirection.NONE;
- }
-
- @Override
- public void seek(final boolean forward) {
- playerGestureListener.keepInDoubleTapMode();
- if (forward) {
- fastForward();
- } else {
- fastRewind();
- }
- }
- });
- playerGestureListener.doubleTapControls(binding.fastSeekOverlay);
- }
-
- //endregion
-
-
-
/*//////////////////////////////////////////////////////////////////////////
// Playback initialization via intent
//////////////////////////////////////////////////////////////////////////*/
@@ -707,7 +321,8 @@ public void handleIntent(@NonNull final Intent intent) {
}
final PlayerType oldPlayerType = playerType;
- playerType = retrievePlayerTypeFromIntent(intent);
+ playerType = PlayerType.retrieveFromIntent(intent);
+ initUIsForCurrentPlayerType();
// We need to setup audioOnly before super(), see "sourceOf"
isAudioOnly = audioPlayerSelected();
@@ -728,9 +343,6 @@ public void handleIntent(@NonNull final Intent intent) {
return;
}
- // needed for tablets, check the function for a better explanation
- directlyOpenFullscreenIfNeeded();
-
final PlaybackParameters savedParameters = retrievePlaybackParametersFromPrefs(this);
final float playbackSpeed = savedParameters.speed;
final float playbackPitch = savedParameters.pitch;
@@ -828,46 +440,39 @@ && isPlaybackResumeEnabled(this)
reloadPlayQueueManager();
}
- setupElementsVisibility();
- setupElementsSize();
-
- if (audioPlayerSelected()) {
- service.removeViewFromParent();
- } else if (popupPlayerSelected()) {
- binding.getRoot().setVisibility(View.VISIBLE);
- initPopup();
- initPopupCloseOverlay();
- binding.playPauseButton.requestFocus();
- } else {
- binding.getRoot().setVisibility(View.VISIBLE);
- initVideoPlayer();
- closeItemsList();
- // Android TV: without it focus will frame the whole player
- binding.playPauseButton.requestFocus();
-
- // Note: This is for automatically playing (when "Resume playback" is off), see #6179
- if (getPlayWhenReady()) {
- play();
- } else {
- pause();
- }
- }
+ UIs.call(PlayerUi::setupAfterIntent);
NavigationHelper.sendPlayerStartedEvent(context);
}
- /**
- * Open fullscreen on tablets where the option to have the main player start automatically in
- * fullscreen mode is on. Rotating the device to landscape is already done in {@link
- * VideoDetailFragment#openVideoPlayer(boolean)} when the thumbnail is clicked, and that's
- * enough for phones, but not for tablets since the mini player can be also shown in landscape.
- */
- private void directlyOpenFullscreenIfNeeded() {
- if (fragmentListener != null
- && PlayerHelper.isStartMainPlayerFullscreenEnabled(service)
- && DeviceUtils.isTablet(service)
- && videoPlayerSelected()
- && PlayerHelper.globalScreenOrientationLocked(service)) {
- fragmentListener.onScreenRotationButtonClicked();
+ private void initUIsForCurrentPlayerType() {
+ if ((UIs.get(MainPlayerUi.class).isPresent() && playerType == PlayerType.MAIN)
+ || (UIs.get(PopupPlayerUi.class).isPresent() && playerType == PlayerType.POPUP)) {
+ // correct UI already in place
+ return;
+ }
+
+ // try to reuse binding if possible
+ final PlayerBinding binding = UIs.get(VideoPlayerUi.class).map(VideoPlayerUi::getBinding)
+ .orElseGet(() -> {
+ if (playerType == PlayerType.AUDIO) {
+ return null;
+ } else {
+ return PlayerBinding.inflate(LayoutInflater.from(context));
+ }
+ });
+
+ switch (playerType) {
+ case MAIN:
+ UIs.destroyAll(PopupPlayerUi.class);
+ UIs.addAndPrepare(new MainPlayerUi(this, binding));
+ break;
+ case POPUP:
+ UIs.destroyAll(MainPlayerUi.class);
+ UIs.addAndPrepare(new PopupPlayerUi(this, binding));
+ break;
+ case AUDIO:
+ UIs.destroyAll(VideoPlayerUi.class);
+ break;
}
}
@@ -881,23 +486,53 @@ private void initPlayback(@NonNull final PlayQueue queue,
destroyPlayer();
initPlayer(playOnReady);
setRepeatMode(repeatMode);
- // #6825 - Ensure that the shuffle-button is in the correct state on the UI
- setShuffleButton(binding.shuffleButton, simpleExoPlayer.getShuffleModeEnabled());
setPlaybackParameters(playbackSpeed, playbackPitch, playbackSkipSilence);
playQueue = queue;
playQueue.init();
reloadPlayQueueManager();
- if (playQueueAdapter != null) {
- playQueueAdapter.dispose();
- }
- playQueueAdapter = new PlayQueueAdapter(context, playQueue);
- segmentAdapter = new StreamSegmentAdapter(getStreamSegmentListener());
+ UIs.call(PlayerUi::initPlayback);
simpleExoPlayer.setVolume(isMuted ? 0 : 1);
notifyQueueUpdateToListeners();
}
+
+ private void initPlayer(final boolean playOnReady) {
+ if (DEBUG) {
+ Log.d(TAG, "initPlayer() called with: playOnReady = [" + playOnReady + "]");
+ }
+
+ simpleExoPlayer = new ExoPlayer.Builder(context, renderFactory)
+ .setTrackSelector(trackSelector)
+ .setLoadControl(loadController)
+ .setUsePlatformDiagnostics(false)
+ .build();
+ simpleExoPlayer.addListener(this);
+ simpleExoPlayer.setPlayWhenReady(playOnReady);
+ simpleExoPlayer.setSeekParameters(PlayerHelper.getSeekParameters(context));
+ simpleExoPlayer.setWakeMode(C.WAKE_MODE_NETWORK);
+ simpleExoPlayer.setHandleAudioBecomingNoisy(true);
+
+ audioReactor = new AudioReactor(context, simpleExoPlayer);
+
+ registerBroadcastReceiver();
+
+ // Setup UIs
+ UIs.call(PlayerUi::initPlayer);
+
+ // enable media tunneling
+ if (DEBUG && PreferenceManager.getDefaultSharedPreferences(context)
+ .getBoolean(context.getString(R.string.disable_media_tunneling_key), false)) {
+ Log.d(TAG, "[" + Util.DEVICE_DEBUG_INFO + "] "
+ + "media tunneling disabled in debug preferences");
+ } else if (DeviceUtils.shouldSupportMediaTunneling()) {
+ trackSelector.setParameters(trackSelector.buildUponParameters()
+ .setTunnelingEnabled(true));
+ } else if (DEBUG) {
+ Log.d(TAG, "[" + Util.DEVICE_DEBUG_INFO + "] does not support media tunneling");
+ }
+ }
//endregion
@@ -911,8 +546,7 @@ private void destroyPlayer() {
if (DEBUG) {
Log.d(TAG, "destroyPlayer() called");
}
-
- cleanupVideoSurface();
+ UIs.call(PlayerUi::destroyPlayer);
if (!exoPlayerIsNull()) {
simpleExoPlayer.removeListener(this);
@@ -931,32 +565,25 @@ private void destroyPlayer() {
if (playQueueManager != null) {
playQueueManager.dispose();
}
- if (mediaSessionManager != null) {
- mediaSessionManager.dispose();
- }
-
- if (playQueueAdapter != null) {
- playQueueAdapter.unsetSelectedListener();
- playQueueAdapter.dispose();
- }
}
public void destroy() {
if (DEBUG) {
Log.d(TAG, "destroy() called");
}
+
+ saveStreamProgressState();
+ setRecovery();
+ stopActivityBinding();
+
destroyPlayer();
unregisterBroadcastReceiver();
databaseUpdateDisposable.clear();
progressUpdateDisposable.set(null);
- PicassoHelper.cancelTag(PicassoHelper.PLAYER_THUMBNAIL_TAG); // cancel thumbnail loading
+ cancelLoadingCurrentThumbnail();
- if (binding != null) {
- binding.endScreen.setImageBitmap(null);
- }
-
- context.getContentResolver().unregisterContentObserver(settingsContentObserver);
+ UIs.destroyAll(Object.class); // destroy every UI: obviously every UI extends Object
}
public void setRecovery() {
@@ -969,11 +596,11 @@ public void setRecovery() {
final long duration = simpleExoPlayer.getDuration();
// No checks due to https://github.com/TeamNewPipe/NewPipe/pull/7195#issuecomment-962624380
- setRecovery(queuePos, Math.max(0, Math.min(windowPos, duration)));
+ setRecovery(queuePos, MathUtils.clamp(windowPos, 0, duration));
}
private void setRecovery(final int queuePos, final long windowPos) {
- if (playQueue.size() <= queuePos) {
+ if (playQueue == null || playQueue.size() <= queuePos) {
return;
}
@@ -983,7 +610,7 @@ private void setRecovery(final int queuePos, final long windowPos) {
playQueue.setRecovery(queuePos, windowPos);
}
- private void reloadPlayQueueManager() {
+ public void reloadPlayQueueManager() {
if (playQueueManager != null) {
playQueueManager.dispose();
}
@@ -1002,185 +629,11 @@ public void onPlaybackShutdown() {
service.stopService();
}
- public void smoothStopPlayer() {
+ public void smoothStopForImmediateReusing() {
// Pausing would make transition from one stream to a new stream not smooth, so only stop
simpleExoPlayer.stop();
- }
- //endregion
-
-
-
- /*//////////////////////////////////////////////////////////////////////////
- // Player type specific setup
- //////////////////////////////////////////////////////////////////////////*/
- //region Player type specific setup
-
- private void initVideoPlayer() {
- // restore last resize mode
- setResizeMode(PlayerHelper.retrieveResizeModeFromPrefs(this));
- binding.getRoot().setLayoutParams(new FrameLayout.LayoutParams(
- FrameLayout.LayoutParams.MATCH_PARENT, FrameLayout.LayoutParams.MATCH_PARENT));
- }
-
- @SuppressLint("RtlHardcoded")
- private void initPopup() {
- if (DEBUG) {
- Log.d(TAG, "initPopup() called");
- }
-
- // Popup is already added to windowManager
- if (popupHasParent()) {
- return;
- }
-
- updateScreenSize();
-
- popupLayoutParams = retrievePopupLayoutParamsFromPrefs(this);
- binding.surfaceView.setHeights(popupLayoutParams.height, popupLayoutParams.height);
-
- checkPopupPositionBounds();
-
- binding.loadingPanel.setMinimumWidth(popupLayoutParams.width);
- binding.loadingPanel.setMinimumHeight(popupLayoutParams.height);
-
- service.removeViewFromParent();
- Objects.requireNonNull(windowManager).addView(binding.getRoot(), popupLayoutParams);
-
- // Popup doesn't have aspectRatio selector, using FIT automatically
- setResizeMode(AspectRatioFrameLayout.RESIZE_MODE_FIT);
- }
-
- @SuppressLint("RtlHardcoded")
- private void initPopupCloseOverlay() {
- if (DEBUG) {
- Log.d(TAG, "initPopupCloseOverlay() called");
- }
-
- // closeOverlayView is already added to windowManager
- if (closeOverlayBinding != null) {
- return;
- }
-
- closeOverlayBinding = PlayerPopupCloseOverlayBinding.inflate(LayoutInflater.from(context));
-
- final WindowManager.LayoutParams closeOverlayLayoutParams = buildCloseOverlayLayoutParams();
- closeOverlayBinding.closeButton.setVisibility(View.GONE);
- Objects.requireNonNull(windowManager).addView(
- closeOverlayBinding.getRoot(), closeOverlayLayoutParams);
- }
- //endregion
-
-
-
- /*//////////////////////////////////////////////////////////////////////////
- // Elements visibility and size: popup and main players have different look
- //////////////////////////////////////////////////////////////////////////*/
- //region Elements visibility and size: popup and main players have different look
-
- /**
- * This method ensures that popup and main players have different look.
- * We use one layout for both players and need to decide what to show and what to hide.
- * Additional measuring should be done inside {@link #setupElementsSize}.
- */
- private void setupElementsVisibility() {
- if (popupPlayerSelected()) {
- binding.fullScreenButton.setVisibility(View.VISIBLE);
- binding.screenRotationButton.setVisibility(View.GONE);
- binding.resizeTextView.setVisibility(View.GONE);
- binding.getRoot().findViewById(R.id.metadataView).setVisibility(View.GONE);
- binding.queueButton.setVisibility(View.GONE);
- binding.segmentsButton.setVisibility(View.GONE);
- binding.moreOptionsButton.setVisibility(View.GONE);
- binding.topControls.setOrientation(LinearLayout.HORIZONTAL);
- binding.primaryControls.getLayoutParams().width
- = LinearLayout.LayoutParams.WRAP_CONTENT;
- binding.secondaryControls.setAlpha(1.0f);
- binding.secondaryControls.setVisibility(View.VISIBLE);
- binding.secondaryControls.setTranslationY(0);
- binding.share.setVisibility(View.GONE);
- binding.playWithKodi.setVisibility(View.GONE);
- binding.openInBrowser.setVisibility(View.GONE);
- binding.switchMute.setVisibility(View.GONE);
- binding.playerCloseButton.setVisibility(View.GONE);
- binding.topControls.bringToFront();
- binding.topControls.setClickable(false);
- binding.topControls.setFocusable(false);
- binding.bottomControls.bringToFront();
- closeItemsList();
- } else if (videoPlayerSelected()) {
- binding.fullScreenButton.setVisibility(View.GONE);
- setupScreenRotationButton();
- binding.resizeTextView.setVisibility(View.VISIBLE);
- binding.getRoot().findViewById(R.id.metadataView).setVisibility(View.VISIBLE);
- binding.moreOptionsButton.setVisibility(View.VISIBLE);
- binding.topControls.setOrientation(LinearLayout.VERTICAL);
- binding.primaryControls.getLayoutParams().width
- = LinearLayout.LayoutParams.MATCH_PARENT;
- binding.secondaryControls.setVisibility(View.INVISIBLE);
- binding.moreOptionsButton.setImageDrawable(AppCompatResources.getDrawable(context,
- R.drawable.ic_expand_more));
- binding.share.setVisibility(View.VISIBLE);
- binding.openInBrowser.setVisibility(View.VISIBLE);
- binding.switchMute.setVisibility(View.VISIBLE);
- binding.playerCloseButton.setVisibility(isFullscreen ? View.GONE : View.VISIBLE);
- // Top controls have a large minHeight which is allows to drag the player
- // down in fullscreen mode (just larger area to make easy to locate by finger)
- binding.topControls.setClickable(true);
- binding.topControls.setFocusable(true);
- }
- showHideKodiButton();
-
- if (isFullscreen) {
- binding.titleTextView.setVisibility(View.VISIBLE);
- binding.channelTextView.setVisibility(View.VISIBLE);
- } else {
- binding.titleTextView.setVisibility(View.GONE);
- binding.channelTextView.setVisibility(View.GONE);
- }
- setMuteButton(binding.switchMute, isMuted());
-
- animateRotation(binding.moreOptionsButton, DEFAULT_CONTROLS_DURATION, 0);
- }
-
- /**
- * Changes padding, size of elements based on player selected right now.
- * Popup player has small padding in comparison with the main player
- */
- private void setupElementsSize() {
- final Resources res = context.getResources();
- final int buttonsMinWidth;
- final int playerTopPad;
- final int controlsPad;
- final int buttonsPad;
-
- if (popupPlayerSelected()) {
- buttonsMinWidth = 0;
- playerTopPad = 0;
- controlsPad = res.getDimensionPixelSize(R.dimen.player_popup_controls_padding);
- buttonsPad = res.getDimensionPixelSize(R.dimen.player_popup_buttons_padding);
- } else if (videoPlayerSelected()) {
- buttonsMinWidth = res.getDimensionPixelSize(R.dimen.player_main_buttons_min_width);
- playerTopPad = res.getDimensionPixelSize(R.dimen.player_main_top_padding);
- controlsPad = res.getDimensionPixelSize(R.dimen.player_main_controls_padding);
- buttonsPad = res.getDimensionPixelSize(R.dimen.player_main_buttons_padding);
- } else {
- return;
- }
-
- binding.topControls.setPaddingRelative(controlsPad, playerTopPad, controlsPad, 0);
- binding.bottomControls.setPaddingRelative(controlsPad, 0, controlsPad, 0);
- binding.qualityTextView.setPadding(buttonsPad, buttonsPad, buttonsPad, buttonsPad);
- binding.playbackSpeed.setPadding(buttonsPad, buttonsPad, buttonsPad, buttonsPad);
- binding.playbackSpeed.setMinimumWidth(buttonsMinWidth);
- binding.captionTextView.setPadding(buttonsPad, buttonsPad, buttonsPad, buttonsPad);
- }
-
- private void showHideKodiButton() {
- // show kodi button if it supports the current service and it is enabled in settings
- binding.playWithKodi.setVisibility(videoPlayerSelected()
- && playQueue != null && playQueue.getItem() != null
- && KoreUtils.shouldShowPlayWithKodi(context, playQueue.getItem().getServiceId())
- ? View.VISIBLE : View.GONE);
+ setRecovery();
+ UIs.call(PlayerUi::smoothStopForImmediateReusing);
}
//endregion
@@ -1191,6 +644,12 @@ private void showHideKodiButton() {
//////////////////////////////////////////////////////////////////////////*/
//region Broadcast receiver
+ /**
+ * This function prepares the broadcast receiver and is called only in the constructor.
+ * Therefore if you want any PlayerUi to receive a broadcast action, you should add it here,
+ * even if that player ui might never be added to the player. In that case the received
+ * broadcast would not do anything.
+ */
private void setupBroadcastReceiver() {
if (DEBUG) {
Log.d(TAG, "setupBroadcastReceiver() called");
@@ -1243,11 +702,6 @@ private void onBroadcastReceived(final Intent intent) {
break;
case ACTION_PLAY_PAUSE:
playPause();
- if (!fragmentIsVisible) {
- // Ensure that we have audio-only stream playing when a user
- // started to play from notification's play button from outside of the app
- onFragmentStopped();
- }
break;
case ACTION_PLAY_PREVIOUS:
playPrevious();
@@ -1262,62 +716,20 @@ private void onBroadcastReceived(final Intent intent) {
fastForward();
break;
case ACTION_REPEAT:
- onRepeatClicked();
+ cycleNextRepeatMode();
break;
case ACTION_SHUFFLE:
- onShuffleClicked();
- break;
- case ACTION_RECREATE_NOTIFICATION:
- NotificationUtil.getInstance().createNotificationIfNeededAndUpdate(this, true);
- break;
- case VideoDetailFragment.ACTION_VIDEO_FRAGMENT_RESUMED:
- fragmentIsVisible = true;
- useVideoSource(true);
- break;
- case VideoDetailFragment.ACTION_VIDEO_FRAGMENT_STOPPED:
- fragmentIsVisible = false;
- onFragmentStopped();
+ toggleShuffleModeEnabled();
break;
case Intent.ACTION_CONFIGURATION_CHANGED:
assureCorrectAppLanguage(service);
if (DEBUG) {
- Log.d(TAG, "onConfigurationChanged() called");
- }
- if (popupPlayerSelected()) {
- updateScreenSize();
- changePopupSize(popupLayoutParams.width);
- checkPopupPositionBounds();
- }
- // Close it because when changing orientation from portrait
- // (in fullscreen mode) the size of queue layout can be larger than the screen size
- closeItemsList();
- // When the orientation changed, the screen height might be smaller.
- // If the end screen thumbnail is not re-scaled,
- // it can be larger than the current screen height
- // and thus enlarging the whole player.
- // This causes the seekbar to be ouf the visible area.
- updateEndScreenThumbnail();
- break;
- case Intent.ACTION_SCREEN_ON:
- // Interrupt playback only when screen turns on
- // and user is watching video in popup player.
- // Same actions for video player will be handled in ACTION_VIDEO_FRAGMENT_RESUMED
- if (popupPlayerSelected() && (isPlaying() || isLoading())) {
- useVideoSource(true);
- }
- break;
- case Intent.ACTION_SCREEN_OFF:
- // Interrupt playback only when screen turns off with popup player working
- if (popupPlayerSelected() && (isPlaying() || isLoading())) {
- useVideoSource(false);
+ Log.d(TAG, "ACTION_CONFIGURATION_CHANGED received");
}
break;
- case Intent.ACTION_HEADSET_PLUG: //FIXME
- /*notificationManager.cancel(NOTIFICATION_ID);
- mediaSessionManager.dispose();
- mediaSessionManager.enable(getBaseContext(), basePlayerImpl.simpleExoPlayer);*/
- break;
}
+
+ UIs.call(playerUi -> playerUi.onBroadcastReceived(intent));
}
private void registerBroadcastReceiver() {
@@ -1343,295 +755,72 @@ private void unregisterBroadcastReceiver() {
//////////////////////////////////////////////////////////////////////////*/
//region Thumbnail loading
- private void initThumbnail(final String url) {
- if (DEBUG) {
- Log.d(TAG, "Thumbnail - initThumbnail() called with url = ["
- + (url == null ? "null" : url) + "]");
- }
- if (isNullOrEmpty(url)) {
- return;
- }
-
- // scale down the notification thumbnail for performance
- PicassoHelper.loadScaledDownThumbnail(context, url).into(new Target() {
+ private Target getCurrentThumbnailTarget() {
+ // a Picasso target is just a listener for thumbnail loading events
+ return new Target() {
@Override
public void onBitmapLoaded(final Bitmap bitmap, final Picasso.LoadedFrom from) {
if (DEBUG) {
- Log.d(TAG, "Thumbnail - onLoadingComplete() called with: url = [" + url
- + "], " + "loadedImage = [" + bitmap + " -> " + bitmap.getWidth() + "x"
- + bitmap.getHeight() + "], from = [" + from + "]");
+ Log.d(TAG, "Thumbnail - onBitmapLoaded() called with: bitmap = [" + bitmap
+ + " -> " + bitmap.getWidth() + "x" + bitmap.getHeight() + "], from = ["
+ + from + "]");
}
-
- currentThumbnail = bitmap;
- NotificationUtil.getInstance()
- .createNotificationIfNeededAndUpdate(Player.this, false);
- // there is a new thumbnail, so changed the end screen thumbnail, too.
- updateEndScreenThumbnail();
+ // there is a new thumbnail, so e.g. the end screen thumbnail needs to change, too.
+ onThumbnailLoaded(bitmap);
}
@Override
public void onBitmapFailed(final Exception e, final Drawable errorDrawable) {
- Log.e(TAG, "Thumbnail - onBitmapFailed() called with: url = [" + url + "]", e);
- currentThumbnail = null;
- NotificationUtil.getInstance()
- .createNotificationIfNeededAndUpdate(Player.this, false);
+ Log.e(TAG, "Thumbnail - onBitmapFailed() called", e);
+ // there is a new thumbnail, so e.g. the end screen thumbnail needs to change, too.
+ onThumbnailLoaded(null);
}
@Override
public void onPrepareLoad(final Drawable placeHolderDrawable) {
if (DEBUG) {
- Log.d(TAG, "Thumbnail - onLoadingStarted() called with: url = [" + url + "]");
+ Log.d(TAG, "Thumbnail - onPrepareLoad() called");
}
}
- });
+ };
}
- /**
- * Scale the player audio / end screen thumbnail down if necessary.
- *
- * This is necessary when the thumbnail's height is larger than the device's height
- * and thus is enlarging the player's height
- * causing the bottom playback controls to be out of the visible screen.
- *
- */
- public void updateEndScreenThumbnail() {
- if (currentThumbnail == null) {
- return;
- }
-
- final float endScreenHeight = calculateMaxEndScreenThumbnailHeight();
-
- final Bitmap endScreenBitmap = Bitmap.createScaledBitmap(
- currentThumbnail,
- (int) (currentThumbnail.getWidth()
- / (currentThumbnail.getHeight() / endScreenHeight)),
- (int) endScreenHeight,
- true);
-
- if (DEBUG) {
- Log.d(TAG, "Thumbnail - updateEndScreenThumbnail() called with: "
- + "currentThumbnail = [" + currentThumbnail + "], "
- + currentThumbnail.getWidth() + "x" + currentThumbnail.getHeight()
- + ", scaled end screen height = " + endScreenHeight
- + ", scaled end screen width = " + endScreenBitmap.getWidth());
- }
-
- binding.endScreen.setImageBitmap(endScreenBitmap);
- }
-
- /**
- * Calculate the maximum allowed height for the {@link R.id.endScreen}
- * to prevent it from enlarging the player.
- *
- * The calculating follows these rules:
- *
- *
- * Show at least stream title and content creator on TVs and tablets
- * when in landscape (always the case for TVs) and not in fullscreen mode.
- * This requires to have at least 85dp free space for {@link R.id.detail_root}
- * and additional space for the stream title text size
- * ({@link R.id.detail_title_root_layout}).
- * The text size is 15sp on tablets and 16sp on TVs,
- * see {@link R.id.titleTextView}.
- *
- *
- * Otherwise, the max thumbnail height is the screen height.
- *
- *
- *
- * @return the maximum height for the end screen thumbnail
- */
- private float calculateMaxEndScreenThumbnailHeight() {
- // ensure that screenHeight is initialized and thus not 0
- updateScreenSize();
-
- if (DeviceUtils.isTv(context) && !isFullscreen) {
- final int videoInfoHeight =
- DeviceUtils.dpToPx(85, context) + DeviceUtils.spToPx(16, context);
- return Math.min(currentThumbnail.getHeight(), screenHeight - videoInfoHeight);
- } else if (DeviceUtils.isTablet(context) && service.isLandscape() && !isFullscreen) {
- final int videoInfoHeight =
- DeviceUtils.dpToPx(85, context) + DeviceUtils.spToPx(15, context);
- return Math.min(currentThumbnail.getHeight(), screenHeight - videoInfoHeight);
- } else { // fullscreen player: max height is the device height
- return Math.min(currentThumbnail.getHeight(), screenHeight);
- }
- }
- //endregion
-
-
-
- /*//////////////////////////////////////////////////////////////////////////
- // Popup player utils
- //////////////////////////////////////////////////////////////////////////*/
- //region Popup player utils
-
- /**
- * Check if {@link #popupLayoutParams}' position is within a arbitrary boundary
- * that goes from (0, 0) to (screenWidth, screenHeight).
- *
- * If it's out of these boundaries, {@link #popupLayoutParams}' position is changed
- * and {@code true} is returned to represent this change.
- *
- */
- public void checkPopupPositionBounds() {
+ private void loadCurrentThumbnail(final String url) {
if (DEBUG) {
- Log.d(TAG, "checkPopupPositionBounds() called with: "
- + "screenWidth = [" + screenWidth + "], "
- + "screenHeight = [" + screenHeight + "]");
- }
- if (popupLayoutParams == null) {
- return;
- }
-
- if (popupLayoutParams.x < 0) {
- popupLayoutParams.x = 0;
- } else if (popupLayoutParams.x > screenWidth - popupLayoutParams.width) {
- popupLayoutParams.x = (int) (screenWidth - popupLayoutParams.width);
- }
-
- if (popupLayoutParams.y < 0) {
- popupLayoutParams.y = 0;
- } else if (popupLayoutParams.y > screenHeight - popupLayoutParams.height) {
- popupLayoutParams.y = (int) (screenHeight - popupLayoutParams.height);
- }
- }
-
- public void updateScreenSize() {
- if (windowManager != null) {
- final DisplayMetrics metrics = new DisplayMetrics();
- windowManager.getDefaultDisplay().getMetrics(metrics);
-
- screenWidth = metrics.widthPixels;
- screenHeight = metrics.heightPixels;
- if (DEBUG) {
- Log.d(TAG, "updateScreenSize() called: screenWidth = ["
- + screenWidth + "], screenHeight = [" + screenHeight + "]");
- }
+ Log.d(TAG, "Thumbnail - loadCurrentThumbnail() called with url = ["
+ + (url == null ? "null" : url) + "]");
}
- }
- /**
- * Changes the size of the popup based on the width.
- * @param width the new width, height is calculated with
- * {@link PlayerHelper#getMinimumVideoHeight(float)}
- */
- public void changePopupSize(final int width) {
- if (DEBUG) {
- Log.d(TAG, "changePopupSize() called with: width = [" + width + "]");
- }
+ // first cancel any previous loading
+ cancelLoadingCurrentThumbnail();
- if (anyPopupViewIsNull()) {
+ // Unset currentThumbnail, since it is now outdated. This ensures it is not used in media
+ // session metadata while the new thumbnail is being loaded by Picasso.
+ onThumbnailLoaded(null);
+ if (isNullOrEmpty(url)) {
return;
}
- final float minimumWidth = context.getResources().getDimension(R.dimen.popup_minimum_width);
- final int actualWidth = (int) (width > screenWidth ? screenWidth
- : (width < minimumWidth ? minimumWidth : width));
- final int actualHeight = (int) getMinimumVideoHeight(width);
- if (DEBUG) {
- Log.d(TAG, "updatePopupSize() updated values:"
- + " width = [" + actualWidth + "], height = [" + actualHeight + "]");
- }
-
- popupLayoutParams.width = actualWidth;
- popupLayoutParams.height = actualHeight;
- binding.surfaceView.setHeights(popupLayoutParams.height, popupLayoutParams.height);
- Objects.requireNonNull(windowManager)
- .updateViewLayout(binding.getRoot(), popupLayoutParams);
- }
-
- private void changePopupWindowFlags(final int flags) {
- if (DEBUG) {
- Log.d(TAG, "changePopupWindowFlags() called with: flags = [" + flags + "]");
- }
-
- if (!anyPopupViewIsNull()) {
- popupLayoutParams.flags = flags;
- Objects.requireNonNull(windowManager)
- .updateViewLayout(binding.getRoot(), popupLayoutParams);
- }
+ // scale down the notification thumbnail for performance
+ PicassoHelper.loadScaledDownThumbnail(context, url)
+ .tag(PICASSO_PLAYER_THUMBNAIL_TAG)
+ .into(currentThumbnailTarget);
}
- public void closePopup() {
- if (DEBUG) {
- Log.d(TAG, "closePopup() called, isPopupClosing = " + isPopupClosing);
- }
- if (isPopupClosing) {
- return;
- }
- isPopupClosing = true;
-
- saveStreamProgressState();
- Objects.requireNonNull(windowManager).removeView(binding.getRoot());
-
- animatePopupOverlayAndFinishService();
+ private void cancelLoadingCurrentThumbnail() {
+ // cancel the Picasso job associated with the player thumbnail, if any
+ PicassoHelper.cancelTag(PICASSO_PLAYER_THUMBNAIL_TAG);
}
- public void removePopupFromView() {
- if (windowManager != null) {
- // wrap in try-catch since it could sometimes generate errors randomly
- try {
- if (popupHasParent()) {
- windowManager.removeView(binding.getRoot());
- }
- } catch (final IllegalArgumentException e) {
- Log.w(TAG, "Failed to remove popup from window manager", e);
- }
-
- try {
- final boolean closeOverlayHasParent = closeOverlayBinding != null
- && closeOverlayBinding.getRoot().getParent() != null;
- if (closeOverlayHasParent) {
- windowManager.removeView(closeOverlayBinding.getRoot());
- }
- } catch (final IllegalArgumentException e) {
- Log.w(TAG, "Failed to remove popup overlay from window manager", e);
- }
+ private void onThumbnailLoaded(@Nullable final Bitmap bitmap) {
+ // Avoid useless thumbnail updates, if the thumbnail has not actually changed. Based on the
+ // thumbnail loading code, this if would be skipped only when both bitmaps are `null`, since
+ // onThumbnailLoaded won't be called twice with the same nonnull bitmap by Picasso's target.
+ if (currentThumbnail != bitmap) {
+ currentThumbnail = bitmap;
+ UIs.call(playerUi -> playerUi.onThumbnailLoaded(bitmap));
}
}
-
- private void animatePopupOverlayAndFinishService() {
- final int targetTranslationY =
- (int) (closeOverlayBinding.closeButton.getRootView().getHeight()
- - closeOverlayBinding.closeButton.getY());
-
- closeOverlayBinding.closeButton.animate().setListener(null).cancel();
- closeOverlayBinding.closeButton.animate()
- .setInterpolator(new AnticipateInterpolator())
- .translationY(targetTranslationY)
- .setDuration(400)
- .setListener(new AnimatorListenerAdapter() {
- @Override
- public void onAnimationCancel(final Animator animation) {
- end();
- }
-
- @Override
- public void onAnimationEnd(final Animator animation) {
- end();
- }
-
- private void end() {
- Objects.requireNonNull(windowManager)
- .removeView(closeOverlayBinding.getRoot());
- closeOverlayBinding = null;
- service.stopService();
- }
- }).start();
- }
-
- private boolean popupHasParent() {
- return binding != null
- && binding.getRoot().getLayoutParams() instanceof WindowManager.LayoutParams
- && binding.getRoot().getParent() != null;
- }
-
- private boolean anyPopupViewIsNull() {
- // TODO understand why checking getParentActivity() != null
- return popupLayoutParams == null || windowManager == null
- || getParentActivity() != null || binding.getRoot().getParent() == null;
- }
//endregion
@@ -1645,7 +834,7 @@ public float getPlaybackSpeed() {
return getPlaybackParameters().speed;
}
- private void setPlaybackSpeed(final float speed) {
+ public void setPlaybackSpeed(final float speed) {
setPlaybackParameters(speed, getPlaybackPitch(), getPlaybackSkipSilence());
}
@@ -1694,40 +883,13 @@ public void setPlaybackParameters(final float speed, final float pitch,
private void onUpdateProgress(final int currentProgress,
final int duration,
final int bufferPercent) {
- if (!isPrepared) {
- return;
- }
-
- if (duration != binding.playbackSeekBar.getMax()) {
- setVideoDurationToControls(duration);
- }
- if (currentState != STATE_PAUSED) {
- updatePlayBackElementsCurrentDuration(currentProgress);
- }
- if (simpleExoPlayer.isLoading() || bufferPercent > 90) {
- binding.playbackSeekBar.setSecondaryProgress(
- (int) (binding.playbackSeekBar.getMax() * ((float) bufferPercent / 100)));
- }
- if (DEBUG && bufferPercent % 20 == 0) { //Limit log
- Log.d(TAG, "notifyProgressUpdateToListeners() called with: "
- + "isVisible = " + isControlsVisible() + ", "
- + "currentProgress = [" + currentProgress + "], "
- + "duration = [" + duration + "], bufferPercent = [" + bufferPercent + "]");
- }
- binding.playbackLiveSync.setClickable(!isLiveEdge());
-
- notifyProgressUpdateToListeners(currentProgress, duration, bufferPercent);
-
- if (areSegmentsVisible) {
- segmentAdapter.selectSegmentAt(getNearestStreamSegmentPosition(currentProgress));
- }
-
- if (isQueueVisible) {
- updateQueueTime(currentProgress);
+ if (isPrepared) {
+ UIs.call(ui -> ui.onUpdateProgress(currentProgress, duration, bufferPercent));
+ notifyProgressUpdateToListeners(currentProgress, duration, bufferPercent);
}
}
- private void startProgressLoop() {
+ public void startProgressLoop() {
progressUpdateDisposable.set(getProgressUpdateDisposable());
}
@@ -1735,11 +897,11 @@ private void stopProgressLoop() {
progressUpdateDisposable.set(null);
}
- private boolean isProgressLoopRunning() {
+ public boolean isProgressLoopRunning() {
return progressUpdateDisposable.get() != null;
}
- private void triggerProgressUpdate() {
+ public void triggerProgressUpdate() {
if (exoPlayerIsNull()) {
return;
}
@@ -1756,229 +918,12 @@ private Disposable getProgressUpdateDisposable() {
error -> Log.e(TAG, "Progress update failure: ", error));
}
- @Override // seekbar listener
- public void onProgressChanged(final SeekBar seekBar, final int progress,
- final boolean fromUser) {
- // Currently we don't need method execution when fromUser is false
- if (!fromUser) {
- return;
- }
- if (DEBUG) {
- Log.d(TAG, "onProgressChanged() called with: "
- + "seekBar = [" + seekBar + "], progress = [" + progress + "]");
- }
-
- binding.currentDisplaySeek.setText(getTimeString(progress));
-
- // Seekbar Preview Thumbnail
- SeekbarPreviewThumbnailHelper
- .tryResizeAndSetSeekbarPreviewThumbnail(
- getContext(),
- seekbarPreviewThumbnailHolder.getBitmapAt(progress),
- binding.currentSeekbarPreviewThumbnail,
- binding.subtitleView::getWidth);
-
- adjustSeekbarPreviewContainer();
- }
-
- private void adjustSeekbarPreviewContainer() {
- try {
- // Should only be required when an error occurred before
- // and the layout was positioned in the center
- binding.bottomSeekbarPreviewLayout.setGravity(Gravity.NO_GRAVITY);
-
- // Calculate the current left position of seekbar progress in px
- // More info: https://stackoverflow.com/q/20493577
- final int currentSeekbarLeft =
- binding.playbackSeekBar.getLeft()
- + binding.playbackSeekBar.getPaddingLeft()
- + binding.playbackSeekBar.getThumb().getBounds().left;
-
- // Calculate the (unchecked) left position of the container
- final int uncheckedContainerLeft =
- currentSeekbarLeft - (binding.seekbarPreviewContainer.getWidth() / 2);
-
- // Fix the position so it's within the boundaries
- final int checkedContainerLeft =
- Math.max(
- Math.min(
- uncheckedContainerLeft,
- // Max left
- binding.playbackWindowRoot.getWidth()
- - binding.seekbarPreviewContainer.getWidth()
- ),
- 0 // Min left
- );
-
- // See also: https://stackoverflow.com/a/23249734
- final LinearLayout.LayoutParams params =
- new LinearLayout.LayoutParams(
- binding.seekbarPreviewContainer.getLayoutParams());
- params.setMarginStart(checkedContainerLeft);
- binding.seekbarPreviewContainer.setLayoutParams(params);
- } catch (final Exception ex) {
- Log.e(TAG, "Failed to adjust seekbarPreviewContainer", ex);
- // Fallback - position in the middle
- binding.bottomSeekbarPreviewLayout.setGravity(Gravity.CENTER);
- }
- }
-
- @Override // seekbar listener
- public void onStartTrackingTouch(final SeekBar seekBar) {
- if (DEBUG) {
- Log.d(TAG, "onStartTrackingTouch() called with: seekBar = [" + seekBar + "]");
- }
- if (currentState != STATE_PAUSED_SEEK) {
- changeState(STATE_PAUSED_SEEK);
- }
-
- saveWasPlaying();
- if (isPlaying()) {
- simpleExoPlayer.pause();
- }
-
- showControls(0);
- animate(binding.currentDisplaySeek, true, DEFAULT_CONTROLS_DURATION,
- AnimationType.SCALE_AND_ALPHA);
- animate(binding.currentSeekbarPreviewThumbnail, true, DEFAULT_CONTROLS_DURATION,
- AnimationType.SCALE_AND_ALPHA);
- }
-
- @Override // seekbar listener
- public void onStopTrackingTouch(final SeekBar seekBar) {
- if (DEBUG) {
- Log.d(TAG, "onStopTrackingTouch() called with: seekBar = [" + seekBar + "]");
- }
-
- seekTo(seekBar.getProgress());
- if (wasPlaying || simpleExoPlayer.getDuration() == seekBar.getProgress()) {
- simpleExoPlayer.play();
- }
-
- binding.playbackCurrentTime.setText(getTimeString(seekBar.getProgress()));
- animate(binding.currentDisplaySeek, false, 200, AnimationType.SCALE_AND_ALPHA);
- animate(binding.currentSeekbarPreviewThumbnail, false, 200, AnimationType.SCALE_AND_ALPHA);
-
- if (currentState == STATE_PAUSED_SEEK) {
- changeState(STATE_BUFFERING);
- }
- if (!isProgressLoopRunning()) {
- startProgressLoop();
- }
- if (wasPlaying) {
- showControlsThenHide();
- }
- }
-
public void saveWasPlaying() {
this.wasPlaying = getPlayWhenReady();
}
- //endregion
-
-
-
- /*//////////////////////////////////////////////////////////////////////////
- // Controls showing / hiding
- //////////////////////////////////////////////////////////////////////////*/
- //region Controls showing / hiding
-
- public boolean isControlsVisible() {
- return binding != null && binding.playbackControlRoot.getVisibility() == View.VISIBLE;
- }
-
- public void showControlsThenHide() {
- if (DEBUG) {
- Log.d(TAG, "showControlsThenHide() called");
- }
- showOrHideButtons();
- showSystemUIPartially();
-
- final int hideTime = binding.playbackControlRoot.isInTouchMode()
- ? DEFAULT_CONTROLS_HIDE_TIME
- : DPAD_CONTROLS_HIDE_TIME;
-
- showHideShadow(true, DEFAULT_CONTROLS_DURATION);
- animate(binding.playbackControlRoot, true, DEFAULT_CONTROLS_DURATION,
- AnimationType.ALPHA, 0, () -> hideControls(DEFAULT_CONTROLS_DURATION, hideTime));
- }
-
- public void showControls(final long duration) {
- if (DEBUG) {
- Log.d(TAG, "showControls() called");
- }
- showOrHideButtons();
- showSystemUIPartially();
- controlsVisibilityHandler.removeCallbacksAndMessages(null);
- showHideShadow(true, duration);
- animate(binding.playbackControlRoot, true, duration);
- }
-
- public void hideControls(final long duration, final long delay) {
- if (DEBUG) {
- Log.d(TAG, "hideControls() called with: duration = [" + duration
- + "], delay = [" + delay + "]");
- }
-
- showOrHideButtons();
-
- controlsVisibilityHandler.removeCallbacksAndMessages(null);
- controlsVisibilityHandler.postDelayed(() -> {
- showHideShadow(false, duration);
- animate(binding.playbackControlRoot, false, duration, AnimationType.ALPHA,
- 0, this::hideSystemUIIfNeeded);
- }, delay);
- }
-
- public void showHideShadow(final boolean show, final long duration) {
- animate(binding.playbackControlsShadow, show, duration, AnimationType.ALPHA, 0, null);
- animate(binding.playerTopShadow, show, duration, AnimationType.ALPHA, 0, null);
- animate(binding.playerBottomShadow, show, duration, AnimationType.ALPHA, 0, null);
- }
-
- private void showOrHideButtons() {
- if (playQueue == null) {
- return;
- }
-
- final boolean showPrev = playQueue.getIndex() != 0;
- final boolean showNext = playQueue.getIndex() + 1 != playQueue.getStreams().size();
- final boolean showQueue = playQueue.getStreams().size() > 1 && !popupPlayerSelected();
- /* only when stream has segments and is not playing in popup player */
- final boolean showSegment = !popupPlayerSelected()
- && !getCurrentStreamInfo()
- .map(StreamInfo::getStreamSegments)
- .map(List::isEmpty)
- .orElse(/*no stream info=*/true);
-
- binding.playPreviousButton.setVisibility(showPrev ? View.VISIBLE : View.INVISIBLE);
- binding.playPreviousButton.setAlpha(showPrev ? 1.0f : 0.0f);
- binding.playNextButton.setVisibility(showNext ? View.VISIBLE : View.INVISIBLE);
- binding.playNextButton.setAlpha(showNext ? 1.0f : 0.0f);
- binding.queueButton.setVisibility(showQueue ? View.VISIBLE : View.GONE);
- binding.queueButton.setAlpha(showQueue ? 1.0f : 0.0f);
- binding.segmentsButton.setVisibility(showSegment ? View.VISIBLE : View.GONE);
- binding.segmentsButton.setAlpha(showSegment ? 1.0f : 0.0f);
- }
-
- private void showSystemUIPartially() {
- final AppCompatActivity activity = getParentActivity();
- if (isFullscreen && activity != null) {
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
- activity.getWindow().setStatusBarColor(Color.TRANSPARENT);
- activity.getWindow().setNavigationBarColor(Color.TRANSPARENT);
- }
- final int visibility = View.SYSTEM_UI_FLAG_LAYOUT_STABLE
- | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
- | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION;
- activity.getWindow().getDecorView().setSystemUiVisibility(visibility);
- activity.getWindow().clearFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN);
- }
- }
- private void hideSystemUIIfNeeded() {
- if (fragmentListener != null) {
- fragmentListener.hideSystemUiIfNeeded();
- }
+ public boolean wasPlaying() {
+ return wasPlaying;
}
//endregion
@@ -2012,7 +957,7 @@ public void onPlaybackStateChanged(final int playbackState) {
private void updatePlaybackState(final boolean playWhenReady, final int playbackState) {
if (DEBUG) {
- Log.d(TAG, "ExoPlayer - onPlayerStateChanged() called with: "
+ Log.d(TAG, "ExoPlayer - updatePlaybackState() called with: "
+ "playWhenReady = [" + playWhenReady + "], "
+ "playbackState = [" + playbackState + "]");
}
@@ -2123,9 +1068,7 @@ private void onPrepared(final boolean playWhenReady) {
Log.d(TAG, "onPrepared() called with: playWhenReady = [" + playWhenReady + "]");
}
- setVideoDurationToControls((int) simpleExoPlayer.getDuration());
-
- binding.playbackSpeed.setText(formatSpeed(getPlaybackSpeed()));
+ UIs.call(PlayerUi::onPrepared);
if (playWhenReady) {
audioReactor.requestAudioFocus();
@@ -2140,22 +1083,7 @@ private void onBlocked() {
startProgressLoop();
}
- // if we are e.g. switching players, hide controls
- hideControls(DEFAULT_CONTROLS_DURATION, 0);
-
- binding.playbackSeekBar.setEnabled(false);
- binding.playbackSeekBar.getThumb()
- .setColorFilter(new PorterDuffColorFilter(Color.RED, PorterDuff.Mode.SRC_IN));
-
- binding.loadingPanel.setBackgroundColor(Color.BLACK);
- animate(binding.loadingPanel, true, 0);
- animate(binding.surfaceForeground, true, 100);
-
- binding.playPauseButton.setImageResource(R.drawable.ic_play_arrow);
- animatePlayButtons(false, 100);
- binding.getRoot().setKeepScreenOn(false);
-
- NotificationUtil.getInstance().createNotificationIfNeededAndUpdate(this, false);
+ UIs.call(PlayerUi::onBlocked);
}
private void onPlaying() {
@@ -2166,44 +1094,15 @@ private void onPlaying() {
startProgressLoop();
}
- updateStreamRelatedViews();
-
- binding.playbackSeekBar.setEnabled(true);
- binding.playbackSeekBar.getThumb()
- .setColorFilter(new PorterDuffColorFilter(Color.RED, PorterDuff.Mode.SRC_IN));
-
- binding.loadingPanel.setVisibility(View.GONE);
-
- animate(binding.currentDisplaySeek, false, 200, AnimationType.SCALE_AND_ALPHA);
-
- animate(binding.playPauseButton, false, 80, AnimationType.SCALE_AND_ALPHA, 0,
- () -> {
- binding.playPauseButton.setImageResource(R.drawable.ic_pause);
- animatePlayButtons(true, 200);
- if (!isQueueVisible) {
- binding.playPauseButton.requestFocus();
- }
- });
-
- changePopupWindowFlags(ONGOING_PLAYBACK_WINDOW_FLAGS);
- checkLandscape();
- binding.getRoot().setKeepScreenOn(true);
-
- NotificationUtil.getInstance().createNotificationIfNeededAndUpdate(this, false);
+ UIs.call(PlayerUi::onPlaying);
}
private void onBuffering() {
if (DEBUG) {
Log.d(TAG, "onBuffering() called");
}
- binding.loadingPanel.setBackgroundColor(Color.TRANSPARENT);
- binding.loadingPanel.setVisibility(View.VISIBLE);
-
- binding.getRoot().setKeepScreenOn(true);
- if (NotificationUtil.getInstance().shouldUpdateBufferingSlot()) {
- NotificationUtil.getInstance().createNotificationIfNeededAndUpdate(this, false);
- }
+ UIs.call(PlayerUi::onBuffering);
}
private void onPaused() {
@@ -2215,43 +1114,14 @@ private void onPaused() {
stopProgressLoop();
}
- // Don't let UI elements popup during double tap seeking. This state is entered sometimes
- // during seeking/loading. This if-else check ensures that the controls aren't popping up.
- if (!playerGestureListener.isDoubleTapping()) {
- showControls(400);
- binding.loadingPanel.setVisibility(View.GONE);
-
- animate(binding.playPauseButton, false, 80, AnimationType.SCALE_AND_ALPHA, 0,
- () -> {
- binding.playPauseButton.setImageResource(R.drawable.ic_play_arrow);
- animatePlayButtons(true, 200);
- if (!isQueueVisible) {
- binding.playPauseButton.requestFocus();
- }
- });
- }
- changePopupWindowFlags(IDLE_WINDOW_FLAGS);
-
- // Remove running notification when user does not want minimization to background or popup
- if (PlayerHelper.getMinimizeOnExitAction(context) == MINIMIZE_ON_EXIT_MODE_NONE
- && videoPlayerSelected()) {
- NotificationUtil.getInstance().cancelNotificationAndStopForeground(service);
- } else {
- NotificationUtil.getInstance().createNotificationIfNeededAndUpdate(this, false);
- }
-
- binding.getRoot().setKeepScreenOn(false);
+ UIs.call(PlayerUi::onPaused);
}
private void onPausedSeek() {
if (DEBUG) {
Log.d(TAG, "onPausedSeek() called");
}
-
- animatePlayButtons(false, 100);
- binding.getRoot().setKeepScreenOn(true);
-
- NotificationUtil.getInstance().createNotificationIfNeededAndUpdate(this, false);
+ UIs.call(PlayerUi::onPausedSeek);
}
private void onCompleted() {
@@ -2262,19 +1132,7 @@ private void onCompleted() {
return;
}
- animate(binding.playPauseButton, false, 0, AnimationType.SCALE_AND_ALPHA, 0,
- () -> {
- binding.playPauseButton.setImageResource(R.drawable.ic_replay);
- animatePlayButtons(true, DEFAULT_CONTROLS_DURATION);
- });
-
- binding.getRoot().setKeepScreenOn(false);
- changePopupWindowFlags(IDLE_WINDOW_FLAGS);
-
- NotificationUtil.getInstance().createNotificationIfNeededAndUpdate(this, false);
- if (isFullscreen) {
- toggleFullscreen();
- }
+ UIs.call(PlayerUi::onCompleted);
if (playQueue.getIndex() < playQueue.size() - 1) {
playQueue.offsetIndex(+1);
@@ -2282,38 +1140,6 @@ private void onCompleted() {
if (isProgressLoopRunning()) {
stopProgressLoop();
}
-
- // When a (short) video ends the elements have to display the correct values - see #6180
- updatePlayBackElementsCurrentDuration(binding.playbackSeekBar.getMax());
-
- showControls(500);
- animate(binding.currentDisplaySeek, false, 200, AnimationType.SCALE_AND_ALPHA);
- binding.loadingPanel.setVisibility(View.GONE);
- animate(binding.surfaceForeground, true, 100);
- }
-
- private void animatePlayButtons(final boolean show, final int duration) {
- animate(binding.playPauseButton, show, duration, AnimationType.SCALE_AND_ALPHA);
-
- boolean showQueueButtons = show;
- if (playQueue == null) {
- showQueueButtons = false;
- }
-
- if (!showQueueButtons || playQueue.getIndex() > 0) {
- animate(
- binding.playPreviousButton,
- showQueueButtons,
- duration,
- AnimationType.SCALE_AND_ALPHA);
- }
- if (!showQueueButtons || playQueue.getIndex() + 1 < playQueue.getStreams().size()) {
- animate(
- binding.playNextButton,
- showQueueButtons,
- duration,
- AnimationType.SCALE_AND_ALPHA);
- }
}
//endregion
@@ -2324,43 +1150,29 @@ private void animatePlayButtons(final boolean show, final int duration) {
//////////////////////////////////////////////////////////////////////////*/
//region Repeat and shuffle
- public void onRepeatClicked() {
- if (DEBUG) {
- Log.d(TAG, "onRepeatClicked() called");
- }
- setRepeatMode(nextRepeatMode(getRepeatMode()));
- }
-
- public void onShuffleClicked() {
- if (DEBUG) {
- Log.d(TAG, "onShuffleClicked() called");
- }
-
- if (exoPlayerIsNull()) {
- return;
- }
- simpleExoPlayer.setShuffleModeEnabled(!simpleExoPlayer.getShuffleModeEnabled());
- }
-
@RepeatMode
public int getRepeatMode() {
return exoPlayerIsNull() ? REPEAT_MODE_OFF : simpleExoPlayer.getRepeatMode();
}
- private void setRepeatMode(@RepeatMode final int repeatMode) {
+ public void setRepeatMode(@RepeatMode final int repeatMode) {
if (!exoPlayerIsNull()) {
simpleExoPlayer.setRepeatMode(repeatMode);
}
}
+ public void cycleNextRepeatMode() {
+ setRepeatMode(nextRepeatMode(getRepeatMode()));
+ }
+
@Override
public void onRepeatModeChanged(@RepeatMode final int repeatMode) {
if (DEBUG) {
Log.d(TAG, "ExoPlayer - onRepeatModeChanged() called with: "
+ "repeatMode = [" + repeatMode + "]");
}
- setRepeatModeButton(binding.repeatButton, repeatMode);
- onShuffleOrRepeatModeChanged();
+ UIs.call(playerUi -> playerUi.onRepeatModeChanged(repeatMode));
+ notifyPlaybackUpdateToListeners();
}
@Override
@@ -2378,57 +1190,13 @@ public void onShuffleModeEnabledChanged(final boolean shuffleModeEnabled) {
}
}
- setShuffleButton(binding.shuffleButton, shuffleModeEnabled);
- onShuffleOrRepeatModeChanged();
- }
-
- private void onShuffleOrRepeatModeChanged() {
+ UIs.call(playerUi -> playerUi.onShuffleModeEnabledChanged(shuffleModeEnabled));
notifyPlaybackUpdateToListeners();
- NotificationUtil.getInstance().createNotificationIfNeededAndUpdate(this, false);
}
- private void setRepeatModeButton(final AppCompatImageButton imageButton,
- @RepeatMode final int repeatMode) {
- switch (repeatMode) {
- case REPEAT_MODE_OFF:
- imageButton.setImageResource(R.drawable.exo_controls_repeat_off);
- break;
- case REPEAT_MODE_ONE:
- imageButton.setImageResource(R.drawable.exo_controls_repeat_one);
- break;
- case REPEAT_MODE_ALL:
- imageButton.setImageResource(R.drawable.exo_controls_repeat_all);
- break;
- }
- }
-
- private void setShuffleButton(@NonNull final ImageButton button, final boolean shuffled) {
- button.setImageAlpha(shuffled ? 255 : 77);
- }
- //endregion
-
-
-
- /*//////////////////////////////////////////////////////////////////////////
- // Playlist append
- //////////////////////////////////////////////////////////////////////////*/
- //region Playlist append
-
- public void onAddToPlaylistClicked(@NonNull final FragmentManager fragmentManager) {
- if (DEBUG) {
- Log.d(TAG, "onAddToPlaylistClicked() called");
- }
-
- if (getPlayQueue() != null) {
- PlaylistDialog.createCorrespondingDialog(
- getContext(),
- getPlayQueue()
- .getStreams()
- .stream()
- .map(StreamEntity::new)
- .collect(Collectors.toList()),
- dialog -> dialog.show(fragmentManager, TAG)
- );
+ public void toggleShuffleModeEnabled() {
+ if (!exoPlayerIsNull()) {
+ simpleExoPlayer.setShuffleModeEnabled(!simpleExoPlayer.getShuffleModeEnabled());
}
}
//endregion
@@ -2440,23 +1208,16 @@ public void onAddToPlaylistClicked(@NonNull final FragmentManager fragmentManage
//////////////////////////////////////////////////////////////////////////*/
//region Mute / Unmute
- public void onMuteUnmuteButtonClicked() {
- if (DEBUG) {
- Log.d(TAG, "onMuteUnmuteButtonClicked() called");
- }
- simpleExoPlayer.setVolume(isMuted() ? 1 : 0);
+ public void toggleMute() {
+ final boolean wasMuted = isMuted();
+ simpleExoPlayer.setVolume(wasMuted ? 1 : 0);
+ UIs.call(playerUi -> playerUi.onMuteUnmuteChanged(!wasMuted));
notifyPlaybackUpdateToListeners();
- setMuteButton(binding.switchMute, isMuted());
}
- boolean isMuted() {
+ public boolean isMuted() {
return !exoPlayerIsNull() && simpleExoPlayer.getVolume() == 0;
}
-
- private void setMuteButton(@NonNull final ImageButton button, final boolean isMuted) {
- button.setImageDrawable(AppCompatResources.getDrawable(context, isMuted
- ? R.drawable.ic_volume_off : R.drawable.ic_volume_up));
- }
//endregion
@@ -2515,12 +1276,12 @@ public void onEvents(@NonNull final com.google.android.exoplayer2.Player player,
}
@Override
- public void onTracksInfoChanged(@NonNull final TracksInfo tracksInfo) {
+ public void onTracksChanged(@NonNull final Tracks tracks) {
if (DEBUG) {
Log.d(TAG, "ExoPlayer - onTracksChanged(), "
- + "track group size = " + tracksInfo.getTrackGroupInfos().size());
+ + "track group size = " + tracks.getGroups().size());
}
- onTextTracksChanged(tracksInfo);
+ UIs.call(playerUi -> playerUi.onTextTracksChanged(tracks));
}
@Override
@@ -2529,7 +1290,7 @@ public void onPlaybackParametersChanged(@NonNull final PlaybackParameters playba
Log.d(TAG, "ExoPlayer - playbackParameters(), speed = [" + playbackParameters.speed
+ "], pitch = [" + playbackParameters.pitch + "]");
}
- binding.playbackSpeed.setText(formatSpeed(playbackParameters.speed));
+ UIs.call(playerUi -> playerUi.onPlaybackParametersChanged(playbackParameters));
}
@Override
@@ -2581,13 +1342,12 @@ public void onPositionDiscontinuity(@NonNull final PositionInfo oldPosition,
@Override
public void onRenderedFirstFrame() {
- //TODO check if this causes black screen when switching to fullscreen
- animate(binding.surfaceForeground, false, DEFAULT_CONTROLS_DURATION);
+ UIs.call(PlayerUi::onRenderedFirstFrame);
}
@Override
- public void onCues(@NonNull final List cues) {
- binding.subtitleView.onCues(cues);
+ public void onCues(@NonNull final CueGroup cueGroup) {
+ UIs.call(playerUi -> playerUi.onCues(cueGroup.cues));
}
//endregion
@@ -2628,7 +1388,7 @@ public void onCues(@NonNull final List cues) {
// Any error code not explicitly covered here are either unrelated to NewPipe use case
// (e.g. DRM) or not recoverable (e.g. Decoder error). In both cases, the player should
// shutdown.
- @SuppressLint("SwitchIntDef")
+ @SuppressWarnings("SwitchIntDef")
@Override
public void onPlayerError(@NonNull final PlaybackException error) {
Log.e(TAG, "ExoPlayer - onPlayerError() called with:", error);
@@ -2707,18 +1467,6 @@ private void createErrorNotification(@NonNull final PlaybackException error) {
//////////////////////////////////////////////////////////////////////////*/
//region Playback position and seek
- /**
- * Sets the current duration into the corresponding elements.
- * @param currentProgress
- */
- private void updatePlayBackElementsCurrentDuration(final int currentProgress) {
- // Don't set seekbar progress while user is seeking
- if (currentState != STATE_PAUSED_SEEK) {
- binding.playbackSeekBar.setProgress(currentProgress);
- }
- binding.playbackCurrentTime.setText(getTimeString(currentProgress));
- }
-
@Override // own playback listener (this is a getter)
public boolean isApproachingPlaybackEdge(final long timeToEndMillis) {
// If live, then not near playback edge
@@ -2761,48 +1509,50 @@ public void onPlaybackSynchronize(@NonNull final PlayQueueItem item, final boole
Log.d(TAG, "Playback - onPlaybackSynchronize(was blocked: " + wasBlocked
+ ") called with item=[" + item.getTitle() + "], url=[" + item.getUrl() + "]");
}
- if (exoPlayerIsNull() || playQueue == null) {
- return;
+ if (exoPlayerIsNull() || playQueue == null || currentItem == item) {
+ return; // nothing to synchronize
}
- final boolean hasPlayQueueItemChanged = currentItem != item;
-
- final int currentPlayQueueIndex = playQueue.indexOf(item);
- final int currentPlaylistIndex = simpleExoPlayer.getCurrentMediaItemIndex();
- final int currentPlaylistSize = simpleExoPlayer.getCurrentTimeline().getWindowCount();
+ final int playQueueIndex = playQueue.indexOf(item);
+ final int playlistIndex = simpleExoPlayer.getCurrentMediaItemIndex();
+ final int playlistSize = simpleExoPlayer.getCurrentTimeline().getWindowCount();
+ final boolean removeThumbnailBeforeSync = currentItem == null
+ || currentItem.getServiceId() != item.getServiceId()
+ || !currentItem.getUrl().equals(item.getUrl());
- // If nothing to synchronize
- if (!hasPlayQueueItemChanged) {
- return;
- }
currentItem = item;
- // Check if on wrong window
- if (currentPlayQueueIndex != playQueue.getIndex()) {
- Log.e(TAG, "Playback - Play Queue may be desynchronized: item "
- + "index=[" + currentPlayQueueIndex + "], "
- + "queue index=[" + playQueue.getIndex() + "]");
-
- // Check if bad seek position
- } else if ((currentPlaylistSize > 0 && currentPlayQueueIndex >= currentPlaylistSize)
- || currentPlayQueueIndex < 0) {
- Log.e(TAG, "Playback - Trying to seek to invalid "
- + "index=[" + currentPlayQueueIndex + "] with "
- + "playlist length=[" + currentPlaylistSize + "]");
-
- } else if (wasBlocked || currentPlaylistIndex != currentPlayQueueIndex || !isPlaying()) {
+ if (playQueueIndex != playQueue.getIndex()) {
+ // wrong window (this should be impossible, as this method is called with
+ // `item=playQueue.getItem()`, so the index of that item must be equal to `getIndex()`)
+ Log.e(TAG, "Playback - Play Queue may be not in sync: item index=["
+ + playQueueIndex + "], " + "queue index=[" + playQueue.getIndex() + "]");
+
+ } else if ((playlistSize > 0 && playQueueIndex >= playlistSize) || playQueueIndex < 0) {
+ // the queue and the player's timeline are not in sync, since the play queue index
+ // points outside of the timeline
+ Log.e(TAG, "Playback - Trying to seek to invalid index=[" + playQueueIndex
+ + "] with playlist length=[" + playlistSize + "]");
+
+ } else if (wasBlocked || playlistIndex != playQueueIndex || !isPlaying()) {
+ // either the player needs to be unblocked, or the play queue index has just been
+ // changed and needs to be synchronized, or the player is not playing
if (DEBUG) {
- Log.d(TAG, "Playback - Rewinding to correct "
- + "index=[" + currentPlayQueueIndex + "], "
- + "from=[" + currentPlaylistIndex + "], "
- + "size=[" + currentPlaylistSize + "].");
+ Log.d(TAG, "Playback - Rewinding to correct index=[" + playQueueIndex + "], "
+ + "from=[" + playlistIndex + "], size=[" + playlistSize + "].");
+ }
+
+ if (removeThumbnailBeforeSync) {
+ // unset the current (now outdated) thumbnail to ensure it is not used during sync
+ onThumbnailLoaded(null);
}
+ // sync the player index with the queue index, and seek to the correct position
if (item.getRecoveryPosition() != PlayQueueItem.RECOVERY_UNSET) {
- simpleExoPlayer.seekTo(currentPlayQueueIndex, item.getRecoveryPosition());
- playQueue.unsetRecovery(currentPlayQueueIndex);
+ simpleExoPlayer.seekTo(playQueueIndex, item.getRecoveryPosition());
+ playQueue.unsetRecovery(playQueueIndex);
} else {
- simpleExoPlayer.seekToDefaultPosition(currentPlayQueueIndex);
+ simpleExoPlayer.seekToDefaultPosition(playQueueIndex);
}
}
}
@@ -2813,14 +1563,8 @@ public void seekTo(final long positionMillis) {
}
if (!exoPlayerIsNull()) {
// prevent invalid positions when fast-forwarding/-rewinding
- long normalizedPositionMillis = positionMillis;
- if (normalizedPositionMillis < 0) {
- normalizedPositionMillis = 0;
- } else if (normalizedPositionMillis > simpleExoPlayer.getDuration()) {
- normalizedPositionMillis = simpleExoPlayer.getDuration();
- }
-
- simpleExoPlayer.seekTo(normalizedPositionMillis);
+ simpleExoPlayer.seekTo(MathUtils.clamp(positionMillis, 0,
+ simpleExoPlayer.getDuration()));
}
}
@@ -2836,20 +1580,6 @@ public void seekToDefault() {
simpleExoPlayer.seekToDefaultPosition();
}
}
-
- /**
- * Sets the video duration time into all control components (e.g. seekbar).
- * @param duration
- */
- private void setVideoDurationToControls(final int duration) {
- binding.playbackEndTime.setText(getTimeString(duration));
-
- binding.playbackSeekBar.setMax(duration);
- // This is important for Android TVs otherwise it would apply the default from
- // setMax/Min methods which is (max - min) / 20
- binding.playbackSeekBar.setKeyProgressIncrement(
- PlayerHelper.retrieveSeekDurationFromPreferences(this));
- }
//endregion
@@ -2973,6 +1703,7 @@ private void registerStreamViewed() {
}
private void saveStreamProgressState(final long progressMillis) {
+ //noinspection SimplifyOptionalCallChains
if (!getCurrentStreamInfo().isPresent()
|| !prefs.getBoolean(context.getString(R.string.enable_watch_history_key), true)) {
return;
@@ -3022,66 +1753,33 @@ public void saveStreamProgressStateCompleted() {
//////////////////////////////////////////////////////////////////////////*/
//region Metadata
- private void onMetadataChanged(@NonNull final StreamInfo info) {
+ private void updateMetadataWith(@NonNull final StreamInfo info) {
if (DEBUG) {
Log.d(TAG, "Playback - onMetadataChanged() called, playing: " + info.getName());
}
-
- initThumbnail(info.getThumbnailUrl());
- registerStreamViewed();
- updateStreamRelatedViews();
- showHideKodiButton();
-
- binding.titleTextView.setText(info.getName());
- binding.channelTextView.setText(info.getUploaderName());
-
- this.seekbarPreviewThumbnailHolder.resetFrom(this.getContext(), info.getPreviewFrames());
-
- NotificationUtil.getInstance().createNotificationIfNeededAndUpdate(this, false);
-
- final boolean showThumbnail = prefs.getBoolean(
- context.getString(R.string.show_thumbnail_key), true);
- mediaSessionManager.setMetadata(
- getVideoTitle(),
- getUploaderName(),
- showThumbnail ? Optional.ofNullable(getThumbnail()) : Optional.empty(),
- StreamTypeUtil.isLiveStream(info.getStreamType()) ? -1 : info.getDuration()
- );
-
- notifyMetadataUpdateToListeners();
-
- if (areSegmentsVisible) {
- if (segmentAdapter.setItems(info)) {
- final int adapterPosition = getNearestStreamSegmentPosition(
- simpleExoPlayer.getCurrentPosition());
- segmentAdapter.selectSegmentAt(adapterPosition);
- binding.itemsList.scrollToPosition(adapterPosition);
- } else {
- closeItemsList();
- }
- }
- }
-
- private void updateMetadataWith(@NonNull final StreamInfo streamInfo) {
if (exoPlayerIsNull()) {
return;
}
- maybeAutoQueueNextStream(streamInfo);
- onMetadataChanged(streamInfo);
- NotificationUtil.getInstance().createNotificationIfNeededAndUpdate(this, true);
+ maybeAutoQueueNextStream(info);
+
+ loadCurrentThumbnail(info.getThumbnailUrl());
+ registerStreamViewed();
+
+ notifyMetadataUpdateToListeners();
+ UIs.call(playerUi -> playerUi.onMetadataChanged(info));
}
@NonNull
- private String getVideoUrl() {
+ public String getVideoUrl() {
return currentMetadata == null
? context.getString(R.string.unknown_content)
: currentMetadata.getStreamUrl();
}
@NonNull
- private String getVideoUrlAtCurrentTime() {
- final int timeSeconds = binding.playbackSeekBar.getProgress() / 1000;
+ public String getVideoUrlAtCurrentTime() {
+ final long timeSeconds = simpleExoPlayer.getCurrentPosition() / 1000;
String videoUrl = getVideoUrl();
if (!isLive() && timeSeconds >= 0 && currentMetadata != null
&& currentMetadata.getServiceId() == YouTube.getServiceId()) {
@@ -3107,10 +1805,6 @@ public String getUploaderName() {
@Nullable
public Bitmap getThumbnail() {
- if (currentThumbnail == null) {
- currentThumbnail = BitmapFactory.decodeResource(
- context.getResources(), R.drawable.dummy_thumbnail);
- }
return currentThumbnail;
}
//endregion
@@ -3157,188 +1851,7 @@ public void selectQueueItem(final PlayQueueItem item) {
@Override
public void onPlayQueueEdited() {
notifyPlaybackUpdateToListeners();
- showOrHideButtons();
- NotificationUtil.getInstance().createNotificationIfNeededAndUpdate(this, false);
- }
-
- private void onQueueClicked() {
- isQueueVisible = true;
-
- hideSystemUIIfNeeded();
- buildQueue();
-
- binding.itemsListHeaderTitle.setVisibility(View.GONE);
- binding.itemsListHeaderDuration.setVisibility(View.VISIBLE);
- binding.shuffleButton.setVisibility(View.VISIBLE);
- binding.repeatButton.setVisibility(View.VISIBLE);
- binding.addToPlaylistButton.setVisibility(View.VISIBLE);
-
- hideControls(0, 0);
- binding.itemsListPanel.requestFocus();
- animate(binding.itemsListPanel, true, DEFAULT_CONTROLS_DURATION,
- AnimationType.SLIDE_AND_ALPHA);
-
- binding.itemsList.scrollToPosition(playQueue.getIndex());
-
- updateQueueTime((int) simpleExoPlayer.getCurrentPosition());
- }
-
- private void buildQueue() {
- binding.itemsList.setAdapter(playQueueAdapter);
- binding.itemsList.setClickable(true);
- binding.itemsList.setLongClickable(true);
-
- binding.itemsList.clearOnScrollListeners();
- binding.itemsList.addOnScrollListener(getQueueScrollListener());
-
- itemTouchHelper = new ItemTouchHelper(getItemTouchCallback());
- itemTouchHelper.attachToRecyclerView(binding.itemsList);
-
- playQueueAdapter.setSelectedListener(getOnSelectedListener());
-
- binding.itemsListClose.setOnClickListener(view -> closeItemsList());
- }
-
- private void onSegmentsClicked() {
- areSegmentsVisible = true;
-
- hideSystemUIIfNeeded();
- buildSegments();
-
- binding.itemsListHeaderTitle.setVisibility(View.VISIBLE);
- binding.itemsListHeaderDuration.setVisibility(View.GONE);
- binding.shuffleButton.setVisibility(View.GONE);
- binding.repeatButton.setVisibility(View.GONE);
- binding.addToPlaylistButton.setVisibility(View.GONE);
-
- hideControls(0, 0);
- binding.itemsListPanel.requestFocus();
- animate(binding.itemsListPanel, true, DEFAULT_CONTROLS_DURATION,
- AnimationType.SLIDE_AND_ALPHA);
-
- final int adapterPosition = getNearestStreamSegmentPosition(simpleExoPlayer
- .getCurrentPosition());
- segmentAdapter.selectSegmentAt(adapterPosition);
- binding.itemsList.scrollToPosition(adapterPosition);
- }
-
- private void buildSegments() {
- binding.itemsList.setAdapter(segmentAdapter);
- binding.itemsList.setClickable(true);
- binding.itemsList.setLongClickable(false);
-
- binding.itemsList.clearOnScrollListeners();
- if (itemTouchHelper != null) {
- itemTouchHelper.attachToRecyclerView(null);
- }
-
- getCurrentStreamInfo().ifPresent(segmentAdapter::setItems);
-
- binding.shuffleButton.setVisibility(View.GONE);
- binding.repeatButton.setVisibility(View.GONE);
- binding.addToPlaylistButton.setVisibility(View.GONE);
- binding.itemsListClose.setOnClickListener(view -> closeItemsList());
- }
-
- public void closeItemsList() {
- if (isQueueVisible || areSegmentsVisible) {
- isQueueVisible = false;
- areSegmentsVisible = false;
-
- if (itemTouchHelper != null) {
- itemTouchHelper.attachToRecyclerView(null);
- }
-
- animate(binding.itemsListPanel, false, DEFAULT_CONTROLS_DURATION,
- AnimationType.SLIDE_AND_ALPHA, 0, () -> {
- // Even when queueLayout is GONE it receives touch events
- // and ruins normal behavior of the app. This line fixes it
- binding.itemsListPanel.setTranslationY(
- -binding.itemsListPanel.getHeight() * 5);
- });
-
- // clear focus, otherwise a white rectangle remains on top of the player
- binding.itemsListClose.clearFocus();
- binding.playPauseButton.requestFocus();
- }
- }
-
- private OnScrollBelowItemsListener getQueueScrollListener() {
- return new OnScrollBelowItemsListener() {
- @Override
- public void onScrolledDown(final RecyclerView recyclerView) {
- if (playQueue != null && !playQueue.isComplete()) {
- playQueue.fetch();
- } else if (binding != null) {
- binding.itemsList.clearOnScrollListeners();
- }
- }
- };
- }
-
- private StreamSegmentAdapter.StreamSegmentListener getStreamSegmentListener() {
- return (item, seconds) -> {
- segmentAdapter.selectSegment(item);
- seekTo(seconds * 1000L);
- triggerProgressUpdate();
- };
- }
-
- private int getNearestStreamSegmentPosition(final long playbackPosition) {
- int nearestPosition = 0;
- final List segments = getCurrentStreamInfo()
- .map(StreamInfo::getStreamSegments)
- .orElse(Collections.emptyList());
-
- for (int i = 0; i < segments.size(); i++) {
- if (segments.get(i).getStartTimeSeconds() * 1000L > playbackPosition) {
- break;
- }
- nearestPosition++;
- }
- return Math.max(0, nearestPosition - 1);
- }
-
- private ItemTouchHelper.SimpleCallback getItemTouchCallback() {
- return new PlayQueueItemTouchCallback() {
- @Override
- public void onMove(final int sourceIndex, final int targetIndex) {
- if (playQueue != null) {
- playQueue.move(sourceIndex, targetIndex);
- }
- }
-
- @Override
- public void onSwiped(final int index) {
- if (index != -1) {
- playQueue.remove(index);
- }
- }
- };
- }
-
- private PlayQueueItemBuilder.OnSelectedListener getOnSelectedListener() {
- return new PlayQueueItemBuilder.OnSelectedListener() {
- @Override
- public void selected(final PlayQueueItem item, final View view) {
- selectQueueItem(item);
- }
-
- @Override
- public void held(final PlayQueueItem item, final View view) {
- if (playQueue.indexOf(item) != -1) {
- openPopupMenu(playQueue, item, view, true,
- getParentActivity().getSupportFragmentManager(), context);
- }
- }
-
- @Override
- public void onStartDrag(final PlayQueueItemHolder viewHolder) {
- if (itemTouchHelper != null) {
- itemTouchHelper.startDrag(viewHolder);
- }
- }
- };
+ UIs.call(PlayerUi::onPlayQueueEdited);
}
@Override // own playback listener
@@ -3373,279 +1886,21 @@ public void disablePreloadingOfCurrentTrack() {
@Nullable
public VideoStream getSelectedVideoStream() {
- return (selectedStreamIndex >= 0 && availableStreams != null
- && availableStreams.size() > selectedStreamIndex)
- ? availableStreams.get(selectedStreamIndex) : null;
- }
-
- private void updateStreamRelatedViews() {
- if (!getCurrentStreamInfo().isPresent()) {
- return;
- }
- final StreamInfo info = getCurrentStreamInfo().get();
-
- binding.qualityTextView.setVisibility(View.GONE);
- binding.playbackSpeed.setVisibility(View.GONE);
-
- binding.playbackEndTime.setVisibility(View.GONE);
- binding.playbackLiveSync.setVisibility(View.GONE);
-
- switch (info.getStreamType()) {
- case AUDIO_STREAM:
- case POST_LIVE_AUDIO_STREAM:
- binding.surfaceView.setVisibility(View.GONE);
- binding.endScreen.setVisibility(View.VISIBLE);
- binding.playbackEndTime.setVisibility(View.VISIBLE);
- break;
-
- case AUDIO_LIVE_STREAM:
- binding.surfaceView.setVisibility(View.GONE);
- binding.endScreen.setVisibility(View.VISIBLE);
- binding.playbackLiveSync.setVisibility(View.VISIBLE);
- break;
-
- case LIVE_STREAM:
- binding.surfaceView.setVisibility(View.VISIBLE);
- binding.endScreen.setVisibility(View.GONE);
- binding.playbackLiveSync.setVisibility(View.VISIBLE);
- break;
-
- case VIDEO_STREAM:
- case POST_LIVE_STREAM:
- if (currentMetadata == null
- || !currentMetadata.getMaybeQuality().isPresent()
- || (info.getVideoStreams().isEmpty()
- && info.getVideoOnlyStreams().isEmpty())) {
- break;
- }
-
- availableStreams = currentMetadata.getMaybeQuality().get().getSortedVideoStreams();
- selectedStreamIndex =
- currentMetadata.getMaybeQuality().get().getSelectedVideoStreamIndex();
- buildQualityMenu();
-
- binding.qualityTextView.setVisibility(View.VISIBLE);
- binding.surfaceView.setVisibility(View.VISIBLE);
- default:
- binding.endScreen.setVisibility(View.GONE);
- binding.playbackEndTime.setVisibility(View.VISIBLE);
- break;
- }
-
- buildPlaybackSpeedMenu();
- binding.playbackSpeed.setVisibility(View.VISIBLE);
- }
-
- private void updateQueueTime(final int currentTime) {
- final int currentStream = playQueue.getIndex();
- int before = 0;
- int after = 0;
-
- final List streams = playQueue.getStreams();
- final int nStreams = streams.size();
-
- for (int i = 0; i < nStreams; i++) {
- if (i < currentStream) {
- before += streams.get(i).getDuration();
- } else {
- after += streams.get(i).getDuration();
- }
- }
-
- before *= 1000;
- after *= 1000;
-
- binding.itemsListHeaderDuration.setText(
- String.format("%s/%s",
- getTimeString(currentTime + before),
- getTimeString(before + after)
- ));
- }
- //endregion
-
-
-
- /*//////////////////////////////////////////////////////////////////////////
- // Popup menus ("popup" means that they pop up, not that they belong to the popup player)
- //////////////////////////////////////////////////////////////////////////*/
- //region Popup menus ("popup" means that they pop up, not that they belong to the popup player)
-
- private void buildQualityMenu() {
- if (qualityPopupMenu == null) {
- return;
- }
- qualityPopupMenu.getMenu().removeGroup(POPUP_MENU_ID_QUALITY);
-
- for (int i = 0; i < availableStreams.size(); i++) {
- final VideoStream videoStream = availableStreams.get(i);
- qualityPopupMenu.getMenu().add(POPUP_MENU_ID_QUALITY, i, Menu.NONE, MediaFormat
- .getNameById(videoStream.getFormatId()) + " " + videoStream.getResolution());
- }
- if (getSelectedVideoStream() != null) {
- binding.qualityTextView.setText(getSelectedVideoStream().getResolution());
- }
- qualityPopupMenu.setOnMenuItemClickListener(this);
- qualityPopupMenu.setOnDismissListener(this);
- }
-
- private void buildPlaybackSpeedMenu() {
- if (playbackSpeedPopupMenu == null) {
- return;
- }
- playbackSpeedPopupMenu.getMenu().removeGroup(POPUP_MENU_ID_PLAYBACK_SPEED);
-
- for (int i = 0; i < PLAYBACK_SPEEDS.length; i++) {
- playbackSpeedPopupMenu.getMenu().add(POPUP_MENU_ID_PLAYBACK_SPEED, i, Menu.NONE,
- formatSpeed(PLAYBACK_SPEEDS[i]));
- }
- binding.playbackSpeed.setText(formatSpeed(getPlaybackSpeed()));
- playbackSpeedPopupMenu.setOnMenuItemClickListener(this);
- playbackSpeedPopupMenu.setOnDismissListener(this);
- }
-
- private void buildCaptionMenu(@NonNull final List availableLanguages) {
- if (captionPopupMenu == null) {
- return;
- }
- captionPopupMenu.getMenu().removeGroup(POPUP_MENU_ID_CAPTION);
- captionPopupMenu.setOnDismissListener(this);
-
- // Add option for turning off caption
- final MenuItem captionOffItem = captionPopupMenu.getMenu().add(POPUP_MENU_ID_CAPTION,
- 0, Menu.NONE, R.string.caption_none);
- captionOffItem.setOnMenuItemClickListener(menuItem -> {
- final int textRendererIndex = getCaptionRendererIndex();
- if (textRendererIndex != RENDERER_UNAVAILABLE) {
- trackSelector.setParameters(trackSelector.buildUponParameters()
- .setRendererDisabled(textRendererIndex, true));
- }
- prefs.edit().remove(context.getString(R.string.caption_user_set_key)).apply();
- return true;
- });
-
- // Add all available captions
- for (int i = 0; i < availableLanguages.size(); i++) {
- final String captionLanguage = availableLanguages.get(i);
- final MenuItem captionItem = captionPopupMenu.getMenu().add(POPUP_MENU_ID_CAPTION,
- i + 1, Menu.NONE, captionLanguage);
- captionItem.setOnMenuItemClickListener(menuItem -> {
- final int textRendererIndex = getCaptionRendererIndex();
- if (textRendererIndex != RENDERER_UNAVAILABLE) {
- // DefaultTrackSelector will select for text tracks in the following order.
- // When multiple tracks share the same rank, a random track will be chosen.
- // 1. ANY track exactly matching preferred language name
- // 2. ANY track exactly matching preferred language stem
- // 3. ROLE_FLAG_CAPTION track matching preferred language stem
- // 4. ROLE_FLAG_DESCRIBES_MUSIC_AND_SOUND track matching preferred language stem
- // This means if a caption track of preferred language is not available,
- // then an auto-generated track of that language will be chosen automatically.
- trackSelector.setParameters(trackSelector.buildUponParameters()
- .setPreferredTextLanguages(captionLanguage,
- PlayerHelper.captionLanguageStemOf(captionLanguage))
- .setPreferredTextRoleFlags(C.ROLE_FLAG_CAPTION)
- .setRendererDisabled(textRendererIndex, false));
- prefs.edit().putString(context.getString(R.string.caption_user_set_key),
- captionLanguage).apply();
- }
- return true;
- });
- }
-
- // apply caption language from previous user preference
- final int textRendererIndex = getCaptionRendererIndex();
- if (textRendererIndex == RENDERER_UNAVAILABLE) {
- return;
- }
-
- // If user prefers to show no caption, then disable the renderer.
- // Otherwise, DefaultTrackSelector may automatically find an available caption
- // and display that.
- final String userPreferredLanguage =
- prefs.getString(context.getString(R.string.caption_user_set_key), null);
- if (userPreferredLanguage == null) {
- trackSelector.setParameters(trackSelector.buildUponParameters()
- .setRendererDisabled(textRendererIndex, true));
- return;
- }
-
- // Only set preferred language if it does not match the user preference,
- // otherwise there might be an infinite cycle at onTextTracksChanged.
- final List selectedPreferredLanguages =
- trackSelector.getParameters().preferredTextLanguages;
- if (!selectedPreferredLanguages.contains(userPreferredLanguage)) {
- trackSelector.setParameters(trackSelector.buildUponParameters()
- .setPreferredTextLanguages(userPreferredLanguage,
- PlayerHelper.captionLanguageStemOf(userPreferredLanguage))
- .setPreferredTextRoleFlags(C.ROLE_FLAG_CAPTION)
- .setRendererDisabled(textRendererIndex, false));
- }
- }
-
- /**
- * Called when an item of the quality selector or the playback speed selector is selected.
- */
- @Override
- public boolean onMenuItemClick(@NonNull final MenuItem menuItem) {
- if (DEBUG) {
- Log.d(TAG, "onMenuItemClick() called with: "
- + "menuItem = [" + menuItem + "], "
- + "menuItem.getItemId = [" + menuItem.getItemId() + "]");
- }
-
- if (menuItem.getGroupId() == POPUP_MENU_ID_QUALITY) {
- final int menuItemIndex = menuItem.getItemId();
- if (selectedStreamIndex == menuItemIndex || availableStreams == null
- || availableStreams.size() <= menuItemIndex) {
- return true;
- }
-
- saveStreamProgressState(); //TODO added, check if good
- final String newResolution = availableStreams.get(menuItemIndex).getResolution();
- setRecovery();
- setPlaybackQuality(newResolution);
- reloadPlayQueueManager();
-
- binding.qualityTextView.setText(menuItem.getTitle());
- return true;
- } else if (menuItem.getGroupId() == POPUP_MENU_ID_PLAYBACK_SPEED) {
- final int speedIndex = menuItem.getItemId();
- final float speed = PLAYBACK_SPEEDS[speedIndex];
-
- setPlaybackSpeed(speed);
- binding.playbackSpeed.setText(formatSpeed(speed));
+ @Nullable final MediaItemTag.Quality quality = Optional.ofNullable(currentMetadata)
+ .flatMap(MediaItemTag::getMaybeQuality)
+ .orElse(null);
+ if (quality == null) {
+ return null;
}
- return false;
- }
+ final List availableStreams = quality.getSortedVideoStreams();
+ final int selectedStreamIndex = quality.getSelectedVideoStreamIndex();
- /**
- * Called when some popup menu is dismissed.
- */
- @Override
- public void onDismiss(@Nullable final PopupMenu menu) {
- if (DEBUG) {
- Log.d(TAG, "onDismiss() called with: menu = [" + menu + "]");
- }
- isSomePopupMenuVisible = false; //TODO check if this works
- if (getSelectedVideoStream() != null) {
- binding.qualityTextView.setText(getSelectedVideoStream().getResolution());
- }
- if (isPlaying()) {
- hideControls(DEFAULT_CONTROLS_DURATION, 0);
- hideSystemUIIfNeeded();
- }
- }
-
- private void onCaptionClicked() {
- if (DEBUG) {
- Log.d(TAG, "onCaptionClicked() called");
+ if (selectedStreamIndex >= 0 && availableStreams.size() > selectedStreamIndex) {
+ return availableStreams.get(selectedStreamIndex);
+ } else {
+ return null;
}
- captionPopupMenu.show();
- isSomePopupMenuVisible = true;
- }
-
- private void setPlaybackQuality(@Nullable final String quality) {
- videoResolver.setPlaybackQuality(quality);
}
//endregion
@@ -3656,67 +1911,7 @@ private void setPlaybackQuality(@Nullable final String quality) {
//////////////////////////////////////////////////////////////////////////*/
//region Captions (text tracks)
- private void setupSubtitleView() {
- final float captionScale = PlayerHelper.getCaptionScale(context);
- final CaptionStyleCompat captionStyle = PlayerHelper.getCaptionStyle(context);
- if (popupPlayerSelected()) {
- final float captionRatio = (captionScale - 1.0f) / 5.0f + 1.0f;
- binding.subtitleView.setFractionalTextSize(
- SubtitleView.DEFAULT_TEXT_SIZE_FRACTION * captionRatio);
- } else {
- final DisplayMetrics metrics = context.getResources().getDisplayMetrics();
- final int minimumLength = Math.min(metrics.heightPixels, metrics.widthPixels);
- final float captionRatioInverse = 20f + 4f * (1.0f - captionScale);
- binding.subtitleView.setFixedTextSize(
- TypedValue.COMPLEX_UNIT_PX, minimumLength / captionRatioInverse);
- }
- binding.subtitleView.setApplyEmbeddedStyles(captionStyle == CaptionStyleCompat.DEFAULT);
- binding.subtitleView.setStyle(captionStyle);
- }
-
- private void onTextTracksChanged(@NonNull final TracksInfo currentTrackInfo) {
- if (binding == null) {
- return;
- }
-
- if (trackSelector.getCurrentMappedTrackInfo() == null
- || !currentTrackInfo.isTypeSupportedOrEmpty(C.TRACK_TYPE_TEXT)) {
- binding.captionTextView.setVisibility(View.GONE);
- return;
- }
-
- // Extract all loaded languages
- final List textTracks = currentTrackInfo
- .getTrackGroupInfos()
- .stream()
- .filter(trackGroupInfo -> C.TRACK_TYPE_TEXT == trackGroupInfo.getTrackType())
- .collect(Collectors.toList());
- final List availableLanguages = textTracks.stream()
- .map(TracksInfo.TrackGroupInfo::getTrackGroup)
- .filter(textTrack -> textTrack.length > 0)
- .map(textTrack -> textTrack.getFormat(0).language)
- .collect(Collectors.toList());
-
- // Find selected text track
- final Optional selectedTracks = textTracks.stream()
- .filter(TracksInfo.TrackGroupInfo::isSelected)
- .filter(info -> info.getTrackGroup().length >= 1)
- .map(info -> info.getTrackGroup().getFormat(0))
- .findFirst();
-
- // Build UI
- buildCaptionMenu(availableLanguages);
- if (trackSelector.getParameters().getRendererDisabled(getCaptionRendererIndex())
- || !selectedTracks.isPresent()) {
- binding.captionTextView.setText(R.string.caption_none);
- } else {
- binding.captionTextView.setText(selectedTracks.get().language);
- }
- binding.captionTextView.setVisibility(
- availableLanguages.isEmpty() ? View.GONE : View.VISIBLE);
- }
-
- private int getCaptionRendererIndex() {
+ public int getCaptionRendererIndex() {
if (exoPlayerIsNull()) {
return RENDERER_UNAVAILABLE;
}
@@ -3732,218 +1927,10 @@ private int getCaptionRendererIndex() {
//endregion
-
- /*//////////////////////////////////////////////////////////////////////////
- // Click listeners
- //////////////////////////////////////////////////////////////////////////*/
- //region Click listeners
-
- @Override
- public void onClick(final View v) {
- if (DEBUG) {
- Log.d(TAG, "onClick() called with: v = [" + v + "]");
- }
- if (v.getId() == binding.resizeTextView.getId()) {
- onResizeClicked();
- } else if (v.getId() == binding.captionTextView.getId()) {
- onCaptionClicked();
- } else if (v.getId() == binding.playbackLiveSync.getId()) {
- seekToDefault();
- } else if (v.getId() == binding.playPauseButton.getId()) {
- playPause();
- } else if (v.getId() == binding.playPreviousButton.getId()) {
- playPrevious();
- } else if (v.getId() == binding.playNextButton.getId()) {
- playNext();
- } else if (v.getId() == binding.moreOptionsButton.getId()) {
- onMoreOptionsClicked();
- } else if (v.getId() == binding.share.getId()) {
- ShareUtils.shareText(context, getVideoTitle(), getVideoUrlAtCurrentTime(),
- currentItem.getThumbnailUrl());
- } else if (v.getId() == binding.playWithKodi.getId()) {
- onPlayWithKodiClicked();
- } else if (v.getId() == binding.openInBrowser.getId()) {
- onOpenInBrowserClicked();
- } else if (v.getId() == binding.fullScreenButton.getId()) {
- setRecovery();
- NavigationHelper.playOnMainPlayer(context, playQueue, true);
- return;
- } else if (v.getId() == binding.screenRotationButton.getId()) {
- // Only if it's not a vertical video or vertical video but in landscape with locked
- // orientation a screen orientation can be changed automatically
- if (!isVerticalVideo
- || (service.isLandscape() && globalScreenOrientationLocked(context))) {
- fragmentListener.onScreenRotationButtonClicked();
- } else {
- toggleFullscreen();
- }
- } else if (v.getId() == binding.switchMute.getId()) {
- onMuteUnmuteButtonClicked();
- } else if (v.getId() == binding.playerCloseButton.getId()) {
- context.sendBroadcast(new Intent(VideoDetailFragment.ACTION_HIDE_MAIN_PLAYER));
- }
-
- manageControlsAfterOnClick(v);
- }
-
- /**
- * Manages the controls after a click occurred on the player UI.
- * @param v – The view that was clicked
- */
- public void manageControlsAfterOnClick(@NonNull final View v) {
- if (currentState == STATE_COMPLETED) {
- return;
- }
-
- controlsVisibilityHandler.removeCallbacksAndMessages(null);
- showHideShadow(true, DEFAULT_CONTROLS_DURATION);
- animate(binding.playbackControlRoot, true, DEFAULT_CONTROLS_DURATION,
- AnimationType.ALPHA, 0, () -> {
- if (currentState == STATE_PLAYING && !isSomePopupMenuVisible) {
- if (v.getId() == binding.playPauseButton.getId()
- // Hide controls in fullscreen immediately
- || (v.getId() == binding.screenRotationButton.getId()
- && isFullscreen)) {
- hideControls(0, 0);
- } else {
- hideControls(DEFAULT_CONTROLS_DURATION, DEFAULT_CONTROLS_HIDE_TIME);
- }
- }
- });
- }
-
- @Override
- public boolean onLongClick(final View v) {
- if (v.getId() == binding.moreOptionsButton.getId() && isFullscreen) {
- fragmentListener.onMoreOptionsLongClicked();
- hideControls(0, 0);
- hideSystemUIIfNeeded();
- } else if (v.getId() == binding.share.getId()) {
- ShareUtils.copyToClipboard(context, getVideoUrlAtCurrentTime());
- }
- return true;
- }
-
- public boolean onKeyDown(final int keyCode) {
- switch (keyCode) {
- default:
- break;
- case KeyEvent.KEYCODE_SPACE:
- if (isFullscreen) {
- playPause();
- if (isPlaying()) {
- hideControls(0, 0);
- }
- return true;
- }
- break;
- case KeyEvent.KEYCODE_BACK:
- if (DeviceUtils.isTv(context) && isControlsVisible()) {
- hideControls(0, 0);
- return true;
- }
- break;
- case KeyEvent.KEYCODE_DPAD_UP:
- case KeyEvent.KEYCODE_DPAD_LEFT:
- case KeyEvent.KEYCODE_DPAD_DOWN:
- case KeyEvent.KEYCODE_DPAD_RIGHT:
- case KeyEvent.KEYCODE_DPAD_CENTER:
- if ((binding.getRoot().hasFocus() && !binding.playbackControlRoot.hasFocus())
- || isQueueVisible) {
- // do not interfere with focus in playlist and play queue etc.
- return false;
- }
-
- if (currentState == Player.STATE_BLOCKED) {
- return true;
- }
-
- if (isControlsVisible()) {
- hideControls(DEFAULT_CONTROLS_DURATION, DPAD_CONTROLS_HIDE_TIME);
- } else {
- binding.playPauseButton.requestFocus();
- showControlsThenHide();
- showSystemUIPartially();
- return true;
- }
- break;
- }
-
- return false;
- }
-
- private void onMoreOptionsClicked() {
- if (DEBUG) {
- Log.d(TAG, "onMoreOptionsClicked() called");
- }
-
- final boolean isMoreControlsVisible =
- binding.secondaryControls.getVisibility() == View.VISIBLE;
-
- animateRotation(binding.moreOptionsButton, DEFAULT_CONTROLS_DURATION,
- isMoreControlsVisible ? 0 : 180);
- animate(binding.secondaryControls, !isMoreControlsVisible, DEFAULT_CONTROLS_DURATION,
- AnimationType.SLIDE_AND_ALPHA, 0, () -> {
- // Fix for a ripple effect on background drawable.
- // When view returns from GONE state it takes more milliseconds than returning
- // from INVISIBLE state. And the delay makes ripple background end to fast
- if (isMoreControlsVisible) {
- binding.secondaryControls.setVisibility(View.INVISIBLE);
- }
- });
- showControls(DEFAULT_CONTROLS_DURATION);
- }
-
- private void onPlayWithKodiClicked() {
- if (currentMetadata != null) {
- pause();
- try {
- NavigationHelper.playWithKore(context, Uri.parse(getVideoUrl()));
- } catch (final Exception e) {
- if (DEBUG) {
- Log.i(TAG, "Failed to start kore", e);
- }
- KoreUtils.showInstallKoreDialog(getParentActivity());
- }
- }
- }
-
- private void onOpenInBrowserClicked() {
- getCurrentStreamInfo()
- .map(Info::getOriginalUrl)
- .ifPresent(originalUrl -> ShareUtils.openUrlInBrowser(
- Objects.requireNonNull(getParentActivity()), originalUrl));
- }
- //endregion
-
-
-
/*//////////////////////////////////////////////////////////////////////////
- // Video size, resize, orientation, fullscreen
+ // Video size
//////////////////////////////////////////////////////////////////////////*/
- //region Video size, resize, orientation, fullscreen
-
- private void setupScreenRotationButton() {
- binding.screenRotationButton.setVisibility(videoPlayerSelected()
- && (globalScreenOrientationLocked(context) || isVerticalVideo
- || DeviceUtils.isTablet(context))
- ? View.VISIBLE : View.GONE);
- binding.screenRotationButton.setImageDrawable(AppCompatResources.getDrawable(context,
- isFullscreen ? R.drawable.ic_fullscreen_exit
- : R.drawable.ic_fullscreen));
- }
-
- private void setResizeMode(@AspectRatioFrameLayout.ResizeMode final int resizeMode) {
- binding.surfaceView.setResizeMode(resizeMode);
- binding.resizeTextView.setText(PlayerHelper.resizeTypeOf(context, resizeMode));
- }
-
- void onResizeClicked() {
- if (binding != null) {
- setResizeMode(nextResizeModeAndSaveToPrefs(this, binding.surfaceView.getResizeMode()));
- }
- }
-
+ //region Video size
@Override // exoplayer listener
public void onVideoSizeChanged(@NonNull final VideoSize videoSize) {
if (DEBUG) {
@@ -3954,137 +1941,11 @@ public void onVideoSizeChanged(@NonNull final VideoSize videoSize) {
+ "pixelWidthHeightRatio = [" + videoSize.pixelWidthHeightRatio + "]");
}
- binding.surfaceView.setAspectRatio(((float) videoSize.width) / videoSize.height);
- isVerticalVideo = videoSize.width < videoSize.height;
-
- if (globalScreenOrientationLocked(context)
- && isFullscreen
- && service.isLandscape() == isVerticalVideo
- && !DeviceUtils.isTv(context)
- && !DeviceUtils.isTablet(context)
- && fragmentListener != null) {
- // set correct orientation
- fragmentListener.onScreenRotationButtonClicked();
- }
-
- setupScreenRotationButton();
- }
-
- public void toggleFullscreen() {
- if (DEBUG) {
- Log.d(TAG, "toggleFullscreen() called");
- }
- if (popupPlayerSelected() || exoPlayerIsNull() || fragmentListener == null) {
- return;
- }
-
- isFullscreen = !isFullscreen;
- if (!isFullscreen) {
- // Apply window insets because Android will not do it when orientation changes
- // from landscape to portrait (open vertical video to reproduce)
- binding.playbackControlRoot.setPadding(0, 0, 0, 0);
- } else {
- // Android needs tens milliseconds to send new insets but a user is able to see
- // how controls changes it's position from `0` to `nav bar height` padding.
- // So just hide the controls to hide this visual inconsistency
- hideControls(0, 0);
- }
- fragmentListener.onFullscreenStateChanged(isFullscreen);
-
- if (isFullscreen) {
- binding.titleTextView.setVisibility(View.VISIBLE);
- binding.channelTextView.setVisibility(View.VISIBLE);
- binding.playerCloseButton.setVisibility(View.GONE);
- } else {
- binding.titleTextView.setVisibility(View.GONE);
- binding.channelTextView.setVisibility(View.GONE);
- binding.playerCloseButton.setVisibility(
- videoPlayerSelected() ? View.VISIBLE : View.GONE);
- }
- setupScreenRotationButton();
- }
-
- public void checkLandscape() {
- final AppCompatActivity parent = getParentActivity();
- final boolean videoInLandscapeButNotInFullscreen =
- service.isLandscape() && !isFullscreen && videoPlayerSelected() && !isAudioOnly;
-
- final boolean notPaused = currentState != STATE_COMPLETED && currentState != STATE_PAUSED;
- if (parent != null
- && videoInLandscapeButNotInFullscreen
- && notPaused
- && !DeviceUtils.isTablet(context)) {
- toggleFullscreen();
- }
- }
- //endregion
-
-
-
- /*//////////////////////////////////////////////////////////////////////////
- // Gestures
- //////////////////////////////////////////////////////////////////////////*/
- //region Gestures
-
- @SuppressWarnings("checkstyle:ParameterNumber")
- private void onLayoutChange(final View view, final int l, final int t, final int r, final int b,
- final int ol, final int ot, final int or, final int ob) {
- if (l != ol || t != ot || r != or || b != ob) {
- // Use smaller value to be consistent between screen orientations
- // (and to make usage easier)
- final int width = r - l;
- final int height = b - t;
- final int min = Math.min(width, height);
- maxGestureLength = (int) (min * MAX_GESTURE_LENGTH);
-
- if (DEBUG) {
- Log.d(TAG, "maxGestureLength = " + maxGestureLength);
- }
-
- binding.volumeProgressBar.setMax(maxGestureLength);
- binding.brightnessProgressBar.setMax(maxGestureLength);
-
- setInitialGestureValues();
- binding.itemsListPanel.getLayoutParams().height
- = height - binding.itemsListPanel.getTop();
- }
- }
-
- private void setInitialGestureValues() {
- if (audioReactor != null) {
- final float currentVolumeNormalized =
- (float) audioReactor.getVolume() / audioReactor.getMaxVolume();
- binding.volumeProgressBar.setProgress(
- (int) (binding.volumeProgressBar.getMax() * currentVolumeNormalized));
- }
- }
-
- private int distanceFromCloseButton(@NonNull final MotionEvent popupMotionEvent) {
- final int closeOverlayButtonX = closeOverlayBinding.closeButton.getLeft()
- + closeOverlayBinding.closeButton.getWidth() / 2;
- final int closeOverlayButtonY = closeOverlayBinding.closeButton.getTop()
- + closeOverlayBinding.closeButton.getHeight() / 2;
-
- final float fingerX = popupLayoutParams.x + popupMotionEvent.getX();
- final float fingerY = popupLayoutParams.y + popupMotionEvent.getY();
-
- return (int) Math.sqrt(Math.pow(closeOverlayButtonX - fingerX, 2)
- + Math.pow(closeOverlayButtonY - fingerY, 2));
- }
-
- private float getClosingRadius() {
- final int buttonRadius = closeOverlayBinding.closeButton.getWidth() / 2;
- // 20% wider than the button itself
- return buttonRadius * 1.2f;
- }
-
- public boolean isInsideClosingRadius(@NonNull final MotionEvent popupMotionEvent) {
- return distanceFromCloseButton(popupMotionEvent) <= getClosingRadius();
+ UIs.call(playerUi -> playerUi.onVideoSizeChanged(videoSize));
}
//endregion
-
/*//////////////////////////////////////////////////////////////////////////
// Activity / fragment binding
//////////////////////////////////////////////////////////////////////////*/
@@ -4092,13 +1953,7 @@ public boolean isInsideClosingRadius(@NonNull final MotionEvent popupMotionEvent
public void setFragmentListener(final PlayerServiceEventListener listener) {
fragmentListener = listener;
- fragmentIsVisible = true;
- // Apply window insets because Android will not do it when orientation changes
- // from landscape to portrait
- if (!isFullscreen) {
- binding.playbackControlRoot.setPadding(0, 0, 0, 0);
- }
- binding.itemsListPanel.setPadding(0, 0, 0, 0);
+ UIs.call(PlayerUi::onFragmentListenerSet);
notifyQueueUpdateToListeners();
notifyMetadataUpdateToListeners();
notifyPlaybackUpdateToListeners();
@@ -4136,28 +1991,6 @@ void stopActivityBinding() {
}
}
- /**
- * This will be called when a user goes to another app/activity, turns off a screen.
- * We don't want to interrupt playback and don't want to see notification so
- * next lines of code will enable audio-only playback only if needed
- */
- private void onFragmentStopped() {
- if (videoPlayerSelected() && (isPlaying() || isLoading())) {
- switch (getMinimizeOnExitAction(context)) {
- case MINIMIZE_ON_EXIT_MODE_BACKGROUND:
- useVideoSource(false);
- break;
- case MINIMIZE_ON_EXIT_MODE_POPUP:
- setRecovery();
- NavigationHelper.playOnPopupPlayer(getParentActivity(), playQueue, true);
- break;
- case MINIMIZE_ON_EXIT_MODE_NONE: default:
- pause();
- break;
- }
- }
- }
-
private void notifyQueueUpdateToListeners() {
if (fragmentListener != null && playQueue != null) {
fragmentListener.onQueueUpdate(playQueue);
@@ -4200,27 +2033,12 @@ private void notifyProgressUpdateToListeners(final int currentProgress,
}
}
- @Nullable
- public AppCompatActivity getParentActivity() {
- // ! instanceof ViewGroup means that view was added via windowManager for Popup
- if (binding == null || !(binding.getRoot().getParent() instanceof ViewGroup)) {
- return null;
- }
-
- return (AppCompatActivity) ((ViewGroup) binding.getRoot().getParent()).getContext();
- }
-
- private void useVideoSource(final boolean videoEnabled) {
+ public void useVideoSource(final boolean videoEnabled) {
if (playQueue == null || isAudioOnly == !videoEnabled || audioPlayerSelected()) {
return;
}
isAudioOnly = !videoEnabled;
- // When a user returns from background, controls could be hidden but SystemUI will be shown
- // 100%. Hide it.
- if (!isAudioOnly && !isControlsVisible()) {
- hideSystemUIIfNeeded();
- }
// The current metadata may be null sometimes (for e.g. when using an unstable connection
// in livestreams) so we will be not able to execute the block below.
@@ -4249,20 +2067,12 @@ private void useVideoSource(final boolean videoEnabled) {
return;
}
- final DefaultTrackSelector.ParametersBuilder parametersBuilder =
+ final DefaultTrackSelector.Parameters.Builder parametersBuilder =
trackSelector.buildUponParameters();
- if (videoEnabled) {
- // Enable again the video track and the subtitles, if there is one selected
- parametersBuilder.setDisabledTrackTypes(Collections.emptySet());
- } else {
- // Disable the video track and the ability to select subtitles
- // Use an ArraySet because we can't use Set.of() on all supported APIs by the app
- final ArraySet disabledTracks = new ArraySet<>();
- disabledTracks.add(C.TRACK_TYPE_TEXT);
- disabledTracks.add(C.TRACK_TYPE_VIDEO);
- parametersBuilder.setDisabledTrackTypes(disabledTracks);
- }
+ // Enable/disable the video track and the ability to select subtitles
+ parametersBuilder.setTrackTypeDisabled(C.TRACK_TYPE_TEXT, !videoEnabled);
+ parametersBuilder.setTrackTypeDisabled(C.TRACK_TYPE_VIDEO, !videoEnabled);
trackSelector.setParameters(parametersBuilder);
}
@@ -4340,7 +2150,7 @@ && isNullOrEmpty(streamInfo.getAudioStreams()))) {
//////////////////////////////////////////////////////////////////////////*/
//region Getters
- private Optional getCurrentStreamInfo() {
+ public Optional getCurrentStreamInfo() {
return Optional.ofNullable(currentMetadata).flatMap(MediaItemTag::getMaybeStreamInfo);
}
@@ -4352,6 +2162,10 @@ public boolean exoPlayerIsNull() {
return simpleExoPlayer == null;
}
+ public ExoPlayer getExoPlayer() {
+ return simpleExoPlayer;
+ }
+
public boolean isStopped() {
return exoPlayerIsNull() || simpleExoPlayer.getPlaybackState() == ExoPlayer.STATE_IDLE;
}
@@ -4364,7 +2178,7 @@ public boolean getPlayWhenReady() {
return !exoPlayerIsNull() && simpleExoPlayer.getPlayWhenReady();
}
- private boolean isLoading() {
+ public boolean isLoading() {
return !exoPlayerIsNull() && simpleExoPlayer.isLoading();
}
@@ -4380,6 +2194,10 @@ private boolean isLive() {
}
}
+ public void setPlaybackQuality(@Nullable final String quality) {
+ videoResolver.setPlaybackQuality(quality);
+ }
+
@NonNull
public Context getContext() {
@@ -4391,10 +2209,6 @@ public SharedPreferences getPrefs() {
return prefs;
}
- public MediaSessionManager getMediaSessionManager() {
- return mediaSessionManager;
- }
-
public PlayerType getPlayerType() {
return playerType;
@@ -4405,7 +2219,7 @@ public boolean audioPlayerSelected() {
}
public boolean videoPlayerSelected() {
- return playerType == PlayerType.VIDEO;
+ return playerType == PlayerType.MAIN;
}
public boolean popupPlayerSelected() {
@@ -4422,156 +2236,40 @@ public AudioReactor getAudioReactor() {
return audioReactor;
}
- public GestureDetector getGestureDetector() {
- return gestureDetector;
- }
-
- public boolean isFullscreen() {
- return isFullscreen;
- }
-
- public boolean isVerticalVideo() {
- return isVerticalVideo;
- }
-
- public boolean isPopupClosing() {
- return isPopupClosing;
- }
-
-
- public boolean isSomePopupMenuVisible() {
- return isSomePopupMenuVisible;
- }
-
- public void setSomePopupMenuVisible(final boolean somePopupMenuVisible) {
- isSomePopupMenuVisible = somePopupMenuVisible;
- }
-
- public ImageButton getPlayPauseButton() {
- return binding.playPauseButton;
- }
-
- public View getClosingOverlayView() {
- return binding.closingOverlay;
- }
-
- public ProgressBar getVolumeProgressBar() {
- return binding.volumeProgressBar;
- }
-
- public ProgressBar getBrightnessProgressBar() {
- return binding.brightnessProgressBar;
- }
-
- public int getMaxGestureLength() {
- return maxGestureLength;
+ public PlayerService getService() {
+ return service;
}
- public ImageView getVolumeImageView() {
- return binding.volumeImageView;
+ public boolean isAudioOnly() {
+ return isAudioOnly;
}
- public RelativeLayout getVolumeRelativeLayout() {
- return binding.volumeRelativeLayout;
- }
-
- public ImageView getBrightnessImageView() {
- return binding.brightnessImageView;
- }
-
- public RelativeLayout getBrightnessRelativeLayout() {
- return binding.brightnessRelativeLayout;
- }
-
- public FloatingActionButton getCloseOverlayButton() {
- return closeOverlayBinding.closeButton;
- }
-
- public View getLoadingPanel() {
- return binding.loadingPanel;
- }
-
- public TextView getCurrentDisplaySeek() {
- return binding.currentDisplaySeek;
- }
-
- public PlayerFastSeekOverlay getFastSeekOverlay() {
- return binding.fastSeekOverlay;
+ @NonNull
+ public DefaultTrackSelector getTrackSelector() {
+ return trackSelector;
}
@Nullable
- public WindowManager.LayoutParams getPopupLayoutParams() {
- return popupLayoutParams;
+ public MediaItemTag getCurrentMetadata() {
+ return currentMetadata;
}
@Nullable
- public WindowManager getWindowManager() {
- return windowManager;
+ public PlayQueueItem getCurrentItem() {
+ return currentItem;
}
- public float getScreenWidth() {
- return screenWidth;
+ public Optional getFragmentListener() {
+ return Optional.ofNullable(fragmentListener);
}
- public float getScreenHeight() {
- return screenHeight;
- }
-
- public View getRootView() {
- return binding.getRoot();
- }
-
- public ExpandableSurfaceView getSurfaceView() {
- return binding.surfaceView;
- }
-
- public PlayQueueAdapter getPlayQueueAdapter() {
- return playQueueAdapter;
- }
-
- public PlayerBinding getBinding() {
- return binding;
- }
-
- //endregion
-
-
- /*//////////////////////////////////////////////////////////////////////////
- // SurfaceHolderCallback helpers
- //////////////////////////////////////////////////////////////////////////*/
- //region SurfaceHolderCallback helpers
-
- private void setupVideoSurface() {
- // make sure there is nothing left over from previous calls
- cleanupVideoSurface();
-
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { // >=API23
- surfaceHolderCallback = new SurfaceHolderCallback(context, simpleExoPlayer);
- binding.surfaceView.getHolder().addCallback(surfaceHolderCallback);
- final Surface surface = binding.surfaceView.getHolder().getSurface();
- // ensure player is using an unreleased surface, which the surfaceView might not be
- // when starting playback on background or during player switching
- if (surface.isValid()) {
- // initially set the surface manually otherwise
- // onRenderedFirstFrame() will not be called
- simpleExoPlayer.setVideoSurface(surface);
- }
- } else {
- simpleExoPlayer.setVideoSurfaceView(binding.surfaceView);
- }
- }
-
- private void cleanupVideoSurface() {
- // Only for API >= 23
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && surfaceHolderCallback != null) {
- if (binding != null) {
- binding.surfaceView.getHolder().removeCallback(surfaceHolderCallback);
- }
- surfaceHolderCallback.release();
- surfaceHolderCallback = null;
- }
+ /**
+ * @return the user interfaces connected with the player
+ */
+ @SuppressWarnings("MethodName") // keep the unusual method name
+ public PlayerUiList UIs() {
+ return UIs;
}
- //endregion
/**
* Get the video renderer index of the current playing stream.
@@ -4600,4 +2298,5 @@ private int getVideoRendererIndex() {
// No video renderer index with at least one track found: return unavailable index
.orElse(RENDERER_UNAVAILABLE);
}
+ //endregion
}
diff --git a/app/src/main/java/org/schabi/newpipe/player/PlayerService.java b/app/src/main/java/org/schabi/newpipe/player/PlayerService.java
new file mode 100644
index 00000000000..33b024e3dc5
--- /dev/null
+++ b/app/src/main/java/org/schabi/newpipe/player/PlayerService.java
@@ -0,0 +1,149 @@
+/*
+ * Copyright 2017 Mauricio Colli
+ * 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 .
+ */
+
+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;
+ }
+ }
+}
diff --git a/app/src/main/java/org/schabi/newpipe/player/PlayerServiceBinder.java b/app/src/main/java/org/schabi/newpipe/player/PlayerServiceBinder.java
deleted file mode 100644
index 5c28c6c7b1b..00000000000
--- a/app/src/main/java/org/schabi/newpipe/player/PlayerServiceBinder.java
+++ /dev/null
@@ -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;
- }
-}
diff --git a/app/src/main/java/org/schabi/newpipe/player/PlayerState.java b/app/src/main/java/org/schabi/newpipe/player/PlayerState.java
deleted file mode 100644
index af875a32ba8..00000000000
--- a/app/src/main/java/org/schabi/newpipe/player/PlayerState.java
+++ /dev/null
@@ -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;
- }
-}
diff --git a/app/src/main/java/org/schabi/newpipe/player/PlayerType.java b/app/src/main/java/org/schabi/newpipe/player/PlayerType.java
new file mode 100644
index 00000000000..171a703953c
--- /dev/null
+++ b/app/src/main/java/org/schabi/newpipe/player/PlayerType.java
@@ -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())];
+ }
+}
diff --git a/app/src/main/java/org/schabi/newpipe/player/datasource/YoutubeHttpDataSource.java b/app/src/main/java/org/schabi/newpipe/player/datasource/YoutubeHttpDataSource.java
index c9abe65f62c..cf1f03b4597 100644
--- a/app/src/main/java/org/schabi/newpipe/player/datasource/YoutubeHttpDataSource.java
+++ b/app/src/main/java/org/schabi/newpipe/player/datasource/YoutubeHttpDataSource.java
@@ -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
* Apache License, Version 2.0.
diff --git a/app/src/main/java/org/schabi/newpipe/player/event/BasePlayerGestureListener.kt b/app/src/main/java/org/schabi/newpipe/player/event/BasePlayerGestureListener.kt
deleted file mode 100644
index c89eabb4785..00000000000
--- a/app/src/main/java/org/schabi/newpipe/player/event/BasePlayerGestureListener.kt
+++ /dev/null
@@ -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
- }
-}
diff --git a/app/src/main/java/org/schabi/newpipe/player/event/DoubleTapListener.kt b/app/src/main/java/org/schabi/newpipe/player/event/DoubleTapListener.kt
deleted file mode 100644
index 84cfb9b8d5a..00000000000
--- a/app/src/main/java/org/schabi/newpipe/player/event/DoubleTapListener.kt
+++ /dev/null
@@ -1,7 +0,0 @@
-package org.schabi.newpipe.player.event
-
-interface DoubleTapListener {
- fun onDoubleTapStarted(portion: DisplayPortion) {}
- fun onDoubleTapProgressDown(portion: DisplayPortion) {}
- fun onDoubleTapFinished() {}
-}
diff --git a/app/src/main/java/org/schabi/newpipe/player/event/PlayerEventListener.java b/app/src/main/java/org/schabi/newpipe/player/event/PlayerEventListener.java
index b5520e8bee7..84bd9d277b3 100644
--- a/app/src/main/java/org/schabi/newpipe/player/event/PlayerEventListener.java
+++ b/app/src/main/java/org/schabi/newpipe/player/event/PlayerEventListener.java
@@ -1,6 +1,5 @@
package org.schabi.newpipe.player.event;
-
import com.google.android.exoplayer2.PlaybackParameters;
import org.schabi.newpipe.extractor.stream.StreamInfo;
diff --git a/app/src/main/java/org/schabi/newpipe/player/event/PlayerGestureListener.java b/app/src/main/java/org/schabi/newpipe/player/event/PlayerGestureListener.java
deleted file mode 100644
index a7fb40c47af..00000000000
--- a/app/src/main/java/org/schabi/newpipe/player/event/PlayerGestureListener.java
+++ /dev/null
@@ -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");
- }
- }
-}
-
-
diff --git a/app/src/main/java/org/schabi/newpipe/player/event/PlayerServiceEventListener.java b/app/src/main/java/org/schabi/newpipe/player/event/PlayerServiceEventListener.java
index 359eab8b28e..8c18fd2ad1c 100644
--- a/app/src/main/java/org/schabi/newpipe/player/event/PlayerServiceEventListener.java
+++ b/app/src/main/java/org/schabi/newpipe/player/event/PlayerServiceEventListener.java
@@ -3,6 +3,8 @@
import com.google.android.exoplayer2.PlaybackException;
public interface PlayerServiceEventListener extends PlayerEventListener {
+ void onViewCreated();
+
void onFullscreenStateChanged(boolean fullscreen);
void onScreenRotationButtonClicked();
diff --git a/app/src/main/java/org/schabi/newpipe/player/event/PlayerServiceExtendedEventListener.java b/app/src/main/java/org/schabi/newpipe/player/event/PlayerServiceExtendedEventListener.java
index f774c90a0e7..8effe2f0e93 100644
--- a/app/src/main/java/org/schabi/newpipe/player/event/PlayerServiceExtendedEventListener.java
+++ b/app/src/main/java/org/schabi/newpipe/player/event/PlayerServiceExtendedEventListener.java
@@ -1,11 +1,11 @@
package org.schabi.newpipe.player.event;
-import org.schabi.newpipe.player.MainPlayer;
+import org.schabi.newpipe.player.PlayerService;
import org.schabi.newpipe.player.Player;
public interface PlayerServiceExtendedEventListener extends PlayerServiceEventListener {
void onServiceConnected(Player player,
- MainPlayer playerService,
+ PlayerService playerService,
boolean playAfterConnect);
void onServiceDisconnected();
}
diff --git a/app/src/main/java/org/schabi/newpipe/player/gesture/BasePlayerGestureListener.kt b/app/src/main/java/org/schabi/newpipe/player/gesture/BasePlayerGestureListener.kt
new file mode 100644
index 00000000000..555c34f9632
--- /dev/null
+++ b/app/src/main/java/org/schabi/newpipe/player/gesture/BasePlayerGestureListener.kt
@@ -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
+ }
+}
diff --git a/app/src/main/java/org/schabi/newpipe/player/event/CustomBottomSheetBehavior.java b/app/src/main/java/org/schabi/newpipe/player/gesture/CustomBottomSheetBehavior.java
similarity index 87%
rename from app/src/main/java/org/schabi/newpipe/player/event/CustomBottomSheetBehavior.java
rename to app/src/main/java/org/schabi/newpipe/player/gesture/CustomBottomSheetBehavior.java
index a5de56e7569..0970dbeb693 100644
--- a/app/src/main/java/org/schabi/newpipe/player/event/CustomBottomSheetBehavior.java
+++ b/app/src/main/java/org/schabi/newpipe/player/gesture/CustomBottomSheetBehavior.java
@@ -1,4 +1,4 @@
-package org.schabi.newpipe.player.event;
+package org.schabi.newpipe.player.gesture;
import android.content.Context;
import android.graphics.Rect;
@@ -8,24 +8,25 @@
import android.widget.FrameLayout;
import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
import androidx.coordinatorlayout.widget.CoordinatorLayout;
import com.google.android.material.bottomsheet.BottomSheetBehavior;
import org.schabi.newpipe.R;
-import java.util.Arrays;
import java.util.List;
public class CustomBottomSheetBehavior extends BottomSheetBehavior {
- public CustomBottomSheetBehavior(final Context context, final AttributeSet attrs) {
+ public CustomBottomSheetBehavior(@NonNull final Context context,
+ @Nullable final AttributeSet attrs) {
super(context, attrs);
}
Rect globalRect = new Rect();
private boolean skippingInterception = false;
- private final List skipInterceptionOfElements = Arrays.asList(
+ private final List skipInterceptionOfElements = List.of(
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.playPauseButton, R.id.playPreviousButton, R.id.playNextButton);
@@ -33,7 +34,7 @@ public CustomBottomSheetBehavior(final Context context, final AttributeSet attrs
@Override
public boolean onInterceptTouchEvent(@NonNull final CoordinatorLayout parent,
@NonNull final FrameLayout child,
- final MotionEvent event) {
+ @NonNull final MotionEvent event) {
// Drop following when action ends
if (event.getAction() == MotionEvent.ACTION_CANCEL
|| event.getAction() == MotionEvent.ACTION_UP) {
@@ -57,7 +58,7 @@ public boolean onInterceptTouchEvent(@NonNull final CoordinatorLayout parent,
if (getState() == BottomSheetBehavior.STATE_EXPANDED
&& event.getAction() == MotionEvent.ACTION_DOWN) {
// 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);
if (view != null) {
final boolean visible = view.getGlobalVisibleRect(globalRect);
diff --git a/app/src/main/java/org/schabi/newpipe/player/event/DisplayPortion.kt b/app/src/main/java/org/schabi/newpipe/player/gesture/DisplayPortion.kt
similarity index 65%
rename from app/src/main/java/org/schabi/newpipe/player/event/DisplayPortion.kt
rename to app/src/main/java/org/schabi/newpipe/player/gesture/DisplayPortion.kt
index f15e42897ca..684f6d326f3 100644
--- a/app/src/main/java/org/schabi/newpipe/player/event/DisplayPortion.kt
+++ b/app/src/main/java/org/schabi/newpipe/player/gesture/DisplayPortion.kt
@@ -1,4 +1,4 @@
-package org.schabi.newpipe.player.event
+package org.schabi.newpipe.player.gesture
enum class DisplayPortion {
LEFT, MIDDLE, RIGHT, LEFT_HALF, RIGHT_HALF
diff --git a/app/src/main/java/org/schabi/newpipe/player/gesture/DoubleTapListener.kt b/app/src/main/java/org/schabi/newpipe/player/gesture/DoubleTapListener.kt
new file mode 100644
index 00000000000..fc026abd9b2
--- /dev/null
+++ b/app/src/main/java/org/schabi/newpipe/player/gesture/DoubleTapListener.kt
@@ -0,0 +1,7 @@
+package org.schabi.newpipe.player.gesture
+
+interface DoubleTapListener {
+ fun onDoubleTapStarted(portion: DisplayPortion)
+ fun onDoubleTapProgressDown(portion: DisplayPortion)
+ fun onDoubleTapFinished()
+}
diff --git a/app/src/main/java/org/schabi/newpipe/player/gesture/MainPlayerGestureListener.kt b/app/src/main/java/org/schabi/newpipe/player/gesture/MainPlayerGestureListener.kt
new file mode 100644
index 00000000000..095b3ccdb77
--- /dev/null
+++ b/app/src/main/java/org/schabi/newpipe/player/gesture/MainPlayerGestureListener.kt
@@ -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
+ }
+}
diff --git a/app/src/main/java/org/schabi/newpipe/player/gesture/PopupPlayerGestureListener.kt b/app/src/main/java/org/schabi/newpipe/player/gesture/PopupPlayerGestureListener.kt
new file mode 100644
index 00000000000..666ea6a461f
--- /dev/null
+++ b/app/src/main/java/org/schabi/newpipe/player/gesture/PopupPlayerGestureListener.kt
@@ -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
+ }
+}
diff --git a/app/src/main/java/org/schabi/newpipe/player/helper/CacheFactory.java b/app/src/main/java/org/schabi/newpipe/player/helper/CacheFactory.java
index d189616d193..41fcc823a7e 100644
--- a/app/src/main/java/org/schabi/newpipe/player/helper/CacheFactory.java
+++ b/app/src/main/java/org/schabi/newpipe/player/helper/CacheFactory.java
@@ -39,8 +39,8 @@ public DataSource createDataSource() {
.createDataSource();
final FileDataSource fileSource = new FileDataSource();
- final CacheDataSink dataSink
- = new CacheDataSink(cache, PlayerHelper.getPreferredFileSize());
+ final CacheDataSink dataSink =
+ new CacheDataSink(cache, PlayerHelper.getPreferredFileSize());
return new CacheDataSource(cache, dataSource, fileSource, dataSink, CACHE_FLAGS, null);
}
}
diff --git a/app/src/main/java/org/schabi/newpipe/player/helper/MediaSessionManager.java b/app/src/main/java/org/schabi/newpipe/player/helper/MediaSessionManager.java
deleted file mode 100644
index a8735dc08bc..00000000000
--- a/app/src/main/java/org/schabi/newpipe/player/helper/MediaSessionManager.java
+++ /dev/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 optAlbumArt,
- final long duration
- ) {
- if (DEBUG) {
- Log.d(TAG, "setMetadata called:"
- + " t: " + title
- + " a: " + artist
- + " thumb: " + (
- optAlbumArt.isPresent()
- ? optAlbumArt.get().hashCode()
- : "")
- + " 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()
- : "")
- + " 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 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();
- }
-}
diff --git a/app/src/main/java/org/schabi/newpipe/player/helper/PlaybackParameterDialog.java b/app/src/main/java/org/schabi/newpipe/player/helper/PlaybackParameterDialog.java
index 19a5a645bbe..796208a0498 100644
--- a/app/src/main/java/org/schabi/newpipe/player/helper/PlaybackParameterDialog.java
+++ b/app/src/main/java/org/schabi/newpipe/player/helper/PlaybackParameterDialog.java
@@ -11,7 +11,6 @@
import android.graphics.drawable.LayerDrawable;
import android.os.Bundle;
import android.util.Log;
-import android.view.LayoutInflater;
import android.view.View;
import android.widget.CheckBox;
import android.widget.SeekBar;
@@ -21,16 +20,16 @@
import androidx.annotation.Nullable;
import androidx.annotation.StringRes;
import androidx.appcompat.app.AlertDialog;
+import androidx.core.math.MathUtils;
import androidx.fragment.app.DialogFragment;
import androidx.preference.PreferenceManager;
import org.schabi.newpipe.R;
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.SliderStrategy;
-import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
import java.util.function.Consumer;
@@ -149,7 +148,7 @@ public Dialog onCreateDialog(@Nullable final Bundle savedInstanceState) {
assureCorrectAppLanguage(getContext());
Icepick.restoreInstanceState(this, savedInstanceState);
- binding = DialogPlaybackParameterBinding.inflate(LayoutInflater.from(getContext()));
+ binding = DialogPlaybackParameterBinding.inflate(getLayoutInflater());
initUI();
final AlertDialog.Builder dialogBuilder = new AlertDialog.Builder(requireActivity())
@@ -207,7 +206,7 @@ private void initUI() {
? View.VISIBLE
: View.GONE);
animateRotation(binding.pitchToogleControlModes,
- Player.DEFAULT_CONTROLS_DURATION,
+ VideoPlayerUi.DEFAULT_CONTROLS_DURATION,
isCurrentlyVisible ? 180 : 0);
});
@@ -334,10 +333,8 @@ private void setupPitchControlModeTextView(
}
private Map getPitchControlModeComponentMappings() {
- final Map mappings = new HashMap<>();
- mappings.put(PITCH_CTRL_MODE_PERCENT, binding.pitchControlModePercent);
- mappings.put(PITCH_CTRL_MODE_SEMITONE, binding.pitchControlModeSemitone);
- return mappings;
+ return Map.of(PITCH_CTRL_MODE_PERCENT, binding.pitchControlModePercent,
+ PITCH_CTRL_MODE_SEMITONE, binding.pitchControlModeSemitone);
}
private void changePitchControlMode(final boolean semitones) {
@@ -407,13 +404,11 @@ private void setupStepTextView(
}
private Map getStepSizeComponentMappings() {
- final Map mappings = new HashMap<>();
- mappings.put(STEP_1_PERCENT_VALUE, binding.stepSizeOnePercent);
- mappings.put(STEP_5_PERCENT_VALUE, binding.stepSizeFivePercent);
- mappings.put(STEP_10_PERCENT_VALUE, binding.stepSizeTenPercent);
- mappings.put(STEP_25_PERCENT_VALUE, binding.stepSizeTwentyFivePercent);
- mappings.put(STEP_100_PERCENT_VALUE, binding.stepSizeOneHundredPercent);
- return mappings;
+ return Map.of(STEP_1_PERCENT_VALUE, binding.stepSizeOnePercent,
+ STEP_5_PERCENT_VALUE, binding.stepSizeFivePercent,
+ STEP_10_PERCENT_VALUE, binding.stepSizeTenPercent,
+ STEP_25_PERCENT_VALUE, binding.stepSizeTwentyFivePercent,
+ STEP_100_PERCENT_VALUE, binding.stepSizeOneHundredPercent);
}
private void setStepSizeToUI(final double newStepSize) {
@@ -532,7 +527,7 @@ private void setSliders(final double newValue) {
}
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));
setText(binding.tempoCurrentText, PlayerHelper::formatSpeed, tempo);
@@ -551,13 +546,8 @@ private void setAndUpdatePitch(final double newPitch) {
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) {
- final double calcPitch =
- Math.max(MIN_PITCH_OR_SPEED, Math.min(MAX_PITCH_OR_SPEED, newPitch));
+ final double calcPitch = MathUtils.clamp(newPitch, MIN_PITCH_OR_SPEED, MAX_PITCH_OR_SPEED);
if (!isCurrentPitchControlModeSemitone()) {
return calcPitch;
diff --git a/app/src/main/java/org/schabi/newpipe/player/helper/PlayerDataSource.java b/app/src/main/java/org/schabi/newpipe/player/helper/PlayerDataSource.java
index 88f25e194ef..0530d56e921 100644
--- a/app/src/main/java/org/schabi/newpipe/player/helper/PlayerDataSource.java
+++ b/app/src/main/java/org/schabi/newpipe/player/helper/PlayerDataSource.java
@@ -208,8 +208,8 @@ private static void instantiateCacheIfNeeded(final Context context) {
Log.w(TAG, "instantiateCacheIfNeeded: could not create cache dir");
}
- final LeastRecentlyUsedCacheEvictor evictor
- = new LeastRecentlyUsedCacheEvictor(PlayerHelper.getPreferredCacheSize());
+ final LeastRecentlyUsedCacheEvictor evictor =
+ new LeastRecentlyUsedCacheEvictor(PlayerHelper.getPreferredCacheSize());
cache = new SimpleCache(cacheDir, evictor, new StandaloneDatabaseProvider(context));
}
}
diff --git a/app/src/main/java/org/schabi/newpipe/player/helper/PlayerHelper.java b/app/src/main/java/org/schabi/newpipe/player/helper/PlayerHelper.java
index 2131861bff6..abde7c3d12c 100644
--- a/app/src/main/java/org/schabi/newpipe/player/helper/PlayerHelper.java
+++ b/app/src/main/java/org/schabi/newpipe/player/helper/PlayerHelper.java
@@ -3,8 +3,6 @@
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_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_NEVER;
import static org.schabi.newpipe.player.helper.PlayerHelper.AutoplayType.AUTOPLAY_TYPE_WIFI;
@@ -15,14 +13,8 @@
import android.annotation.SuppressLint;
import android.content.Context;
-import android.content.Intent;
import android.content.SharedPreferences;
-import android.graphics.PixelFormat;
-import android.os.Build;
import android.provider.Settings;
-import android.view.Gravity;
-import android.view.ViewGroup;
-import android.view.WindowManager;
import android.view.accessibility.CaptioningManager;
import androidx.annotation.IntDef;
@@ -49,7 +41,6 @@
import org.schabi.newpipe.extractor.stream.StreamInfoItem;
import org.schabi.newpipe.extractor.stream.SubtitlesStream;
import org.schabi.newpipe.extractor.utils.Utils;
-import org.schabi.newpipe.player.MainPlayer;
import org.schabi.newpipe.player.Player;
import org.schabi.newpipe.player.playqueue.PlayQueue;
import org.schabi.newpipe.player.playqueue.PlayQueueItem;
@@ -71,25 +62,11 @@
public final class PlayerHelper {
private static final StringBuilder STRING_BUILDER = new StringBuilder();
- private static final Formatter STRING_FORMATTER
- = new Formatter(STRING_BUILDER, Locale.getDefault());
+ private static final Formatter STRING_FORMATTER =
+ new Formatter(STRING_BUILDER, Locale.getDefault());
private static final NumberFormat SPEED_FORMATTER = new DecimalFormat("0.##x");
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.
- *
- *
- * 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.
- *
- *
- * @see WindowManager.LayoutParams#FLAG_NOT_TOUCHABLE
- */
- private static final float MAXIMUM_OPACITY_ALLOWED_FOR_S_AND_HIGHER = 0.8f;
-
@Retention(SOURCE)
@IntDef({AUTOPLAY_TYPE_ALWAYS, AUTOPLAY_TYPE_WIFI,
AUTOPLAY_TYPE_NEVER})
@@ -339,10 +316,6 @@ public static boolean isUsingDSP() {
return true;
}
- public static int getTossFlingVelocity() {
- return 2500;
- }
-
@NonNull
public static CaptionStyleCompat getCaptionStyle(@NonNull final Context context) {
final CaptioningManager captioningManager = ContextCompat.getSystemService(context,
@@ -452,12 +425,6 @@ private static SinglePlayQueue getAutoQueuedSinglePlayQueue(
// 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) {
return player.getPrefs().getBoolean(
player.getContext().getString(R.string.enable_watch_history_key), true)
@@ -528,90 +495,10 @@ public static void savePlaybackParametersToPrefs(final Player player,
.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) {
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) {
return Integer.parseInt(Objects.requireNonNull(player.getPrefs().getString(
player.getContext().getString(R.string.seek_duration_key),
diff --git a/app/src/main/java/org/schabi/newpipe/player/helper/PlayerHolder.java b/app/src/main/java/org/schabi/newpipe/player/helper/PlayerHolder.java
index 4c09ed3c19a..5eaecd48dec 100644
--- a/app/src/main/java/org/schabi/newpipe/player/helper/PlayerHolder.java
+++ b/app/src/main/java/org/schabi/newpipe/player/helper/PlayerHolder.java
@@ -16,8 +16,9 @@
import org.schabi.newpipe.App;
import org.schabi.newpipe.MainActivity;
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.PlayerType;
import org.schabi.newpipe.player.event.PlayerServiceEventListener;
import org.schabi.newpipe.player.event.PlayerServiceExtendedEventListener;
import org.schabi.newpipe.player.playqueue.PlayQueue;
@@ -42,17 +43,17 @@ public static synchronized PlayerHolder getInstance() {
private final PlayerServiceConnection serviceConnection = new PlayerServiceConnection();
private boolean bound;
- @Nullable private MainPlayer playerService;
+ @Nullable private PlayerService playerService;
@Nullable private Player player;
/**
- * Returns the current {@link MainPlayer.PlayerType} of the {@link MainPlayer} service,
- * otherwise `null` if no service running.
+ * Returns the current {@link PlayerType} of the {@link PlayerService} service,
+ * otherwise `null` if no service is running.
*
* @return Current PlayerType
*/
@Nullable
- public MainPlayer.PlayerType getType() {
+ public PlayerType getType() {
if (player == null) {
return null;
}
@@ -122,7 +123,7 @@ public void startService(final boolean playAfterConnect,
// and NullPointerExceptions inside the service because the service will be
// bound twice. Prevent it with unbinding first
unbind(context);
- ContextCompat.startForegroundService(context, new Intent(context, MainPlayer.class));
+ ContextCompat.startForegroundService(context, new Intent(context, PlayerService.class));
serviceConnection.doPlayAfterConnect(playAfterConnect);
bind(context);
}
@@ -130,7 +131,7 @@ public void startService(final boolean playAfterConnect,
public void stopService() {
final Context context = getCommonContext();
unbind(context);
- context.stopService(new Intent(context, MainPlayer.class));
+ context.stopService(new Intent(context, PlayerService.class));
}
class PlayerServiceConnection implements ServiceConnection {
@@ -156,7 +157,7 @@ public void onServiceConnected(final ComponentName compName, final IBinder servi
if (DEBUG) {
Log.d(TAG, "Player service is connected");
}
- final MainPlayer.LocalBinder localBinder = (MainPlayer.LocalBinder) service;
+ final PlayerService.LocalBinder localBinder = (PlayerService.LocalBinder) service;
playerService = localBinder.getService();
player = localBinder.getPlayer();
@@ -172,7 +173,7 @@ private void bind(final Context context) {
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,
Context.BIND_AUTO_CREATE);
if (!bound) {
@@ -211,6 +212,13 @@ private void stopPlayerListener() {
private final PlayerServiceEventListener internalListener =
new PlayerServiceEventListener() {
+ @Override
+ public void onViewCreated() {
+ if (listener != null) {
+ listener.onViewCreated();
+ }
+ }
+
@Override
public void onFullscreenStateChanged(final boolean fullscreen) {
if (listener != null) {
diff --git a/app/src/main/java/org/schabi/newpipe/player/helper/PlayerSemitoneHelper.java b/app/src/main/java/org/schabi/newpipe/player/helper/PlayerSemitoneHelper.java
index f3a71d7cd9e..f1ba90f8ed4 100644
--- a/app/src/main/java/org/schabi/newpipe/player/helper/PlayerSemitoneHelper.java
+++ b/app/src/main/java/org/schabi/newpipe/player/helper/PlayerSemitoneHelper.java
@@ -1,5 +1,7 @@
package org.schabi.newpipe.player.helper;
+import androidx.core.math.MathUtils;
+
/**
* Converts between percent and 12-tone equal temperament semitones.
*
@@ -33,6 +35,6 @@ public static int percentToSemitones(final double percent) {
}
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);
}
}
diff --git a/app/src/main/java/org/schabi/newpipe/player/listeners/view/PlaybackSpeedClickListener.kt b/app/src/main/java/org/schabi/newpipe/player/listeners/view/PlaybackSpeedClickListener.kt
deleted file mode 100644
index 52eff5a1cce..00000000000
--- a/app/src/main/java/org/schabi/newpipe/player/listeners/view/PlaybackSpeedClickListener.kt
+++ /dev/null
@@ -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)
- }
-}
diff --git a/app/src/main/java/org/schabi/newpipe/player/listeners/view/QualityClickListener.kt b/app/src/main/java/org/schabi/newpipe/player/listeners/view/QualityClickListener.kt
deleted file mode 100644
index 43e8288e605..00000000000
--- a/app/src/main/java/org/schabi/newpipe/player/listeners/view/QualityClickListener.kt
+++ /dev/null
@@ -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)
- }
-}
diff --git a/app/src/main/java/org/schabi/newpipe/player/mediaitem/MediaItemTag.java b/app/src/main/java/org/schabi/newpipe/player/mediaitem/MediaItemTag.java
index f84b0383adb..d23dd4574cd 100644
--- a/app/src/main/java/org/schabi/newpipe/player/mediaitem/MediaItemTag.java
+++ b/app/src/main/java/org/schabi/newpipe/player/mediaitem/MediaItemTag.java
@@ -3,6 +3,7 @@
import android.net.Uri;
import com.google.android.exoplayer2.MediaItem;
+import com.google.android.exoplayer2.MediaItem.RequestMetadata;
import com.google.android.exoplayer2.MediaMetadata;
import com.google.android.exoplayer2.Player;
@@ -76,7 +77,6 @@ default String makeMediaId() {
@NonNull
default MediaItem asMediaItem() {
final MediaMetadata mediaMetadata = new MediaMetadata.Builder()
- .setMediaUri(Uri.parse(getStreamUrl()))
.setArtworkUri(Uri.parse(getThumbnailUrl()))
.setArtist(getUploaderName())
.setDescription(getTitle())
@@ -84,10 +84,15 @@ default MediaItem asMediaItem() {
.setTitle(getTitle())
.build();
+ final RequestMetadata requestMetaData = new RequestMetadata.Builder()
+ .setMediaUri(Uri.parse(getStreamUrl()))
+ .build();
+
return MediaItem.fromUri(getStreamUrl())
.buildUpon()
.setMediaId(makeMediaId())
.setMediaMetadata(mediaMetadata)
+ .setRequestMetadata(requestMetaData)
.setTag(this)
.build();
}
diff --git a/app/src/main/java/org/schabi/newpipe/player/mediasession/MediaSessionCallback.java b/app/src/main/java/org/schabi/newpipe/player/mediasession/MediaSessionCallback.java
deleted file mode 100644
index c4b02d9857f..00000000000
--- a/app/src/main/java/org/schabi/newpipe/player/mediasession/MediaSessionCallback.java
+++ /dev/null
@@ -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();
-}
diff --git a/app/src/main/java/org/schabi/newpipe/player/mediasession/MediaSessionPlayerUi.java b/app/src/main/java/org/schabi/newpipe/player/mediasession/MediaSessionPlayerUi.java
new file mode 100644
index 00000000000..e9541ab06d8
--- /dev/null
+++ b/app/src/main/java/org/schabi/newpipe/player/mediasession/MediaSessionPlayerUi.java
@@ -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 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();
+ }
+}
diff --git a/app/src/main/java/org/schabi/newpipe/player/mediasession/PlayQueueNavigator.java b/app/src/main/java/org/schabi/newpipe/player/mediasession/PlayQueueNavigator.java
index 92cd425c5fb..2e54b1129d7 100644
--- a/app/src/main/java/org/schabi/newpipe/player/mediasession/PlayQueueNavigator.java
+++ b/app/src/main/java/org/schabi/newpipe/player/mediasession/PlayQueueNavigator.java
@@ -1,106 +1,152 @@
package org.schabi.newpipe.player.mediasession;
+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_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.Player;
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 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_QUEUE_ITEM;
+import java.util.Optional;
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 MediaSessionCallback callback;
- private final int maxQueueSize;
+ private final Player player;
private long activeQueueItemId;
public PlayQueueNavigator(@NonNull final MediaSessionCompat mediaSession,
- @NonNull final MediaSessionCallback callback) {
+ @NonNull final Player player) {
this.mediaSession = mediaSession;
- this.callback = callback;
- this.maxQueueSize = DEFAULT_MAX_QUEUE_SIZE;
+ this.player = player;
this.activeQueueItemId = MediaSessionCompat.QueueItem.UNKNOWN_ID;
}
@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;
}
@Override
- public void onTimelineChanged(@NonNull final Player player) {
+ public void onTimelineChanged(@NonNull final com.google.android.exoplayer2.Player exoPlayer) {
publishFloatingQueueWindow();
}
@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
- || player.getCurrentTimeline().getWindowCount() > maxQueueSize) {
+ || exoPlayer.getCurrentTimeline().getWindowCount() > MAX_QUEUE_SIZE) {
publishFloatingQueueWindow();
- } else if (!player.getCurrentTimeline().isEmpty()) {
- activeQueueItemId = player.getCurrentMediaItemIndex();
+ } else if (!exoPlayer.getCurrentTimeline().isEmpty()) {
+ activeQueueItemId = exoPlayer.getCurrentMediaItemIndex();
}
}
@Override
- public long getActiveQueueItemId(@Nullable final Player player) {
- return callback.getCurrentPlayingIndex();
+ public long getActiveQueueItemId(
+ @Nullable final com.google.android.exoplayer2.Player exoPlayer) {
+ return Optional.ofNullable(player.getPlayQueue()).map(PlayQueue::getIndex).orElse(-1);
}
@Override
- public void onSkipToPrevious(@NonNull final Player player) {
- callback.playPrevious();
+ public void onSkipToPrevious(@NonNull final com.google.android.exoplayer2.Player exoPlayer) {
+ player.playPrevious();
}
@Override
- public void onSkipToQueueItem(@NonNull final Player player, final long id) {
- callback.playItemAtIndex((int) id);
+ public void onSkipToQueueItem(@NonNull final com.google.android.exoplayer2.Player exoPlayer,
+ final long id) {
+ if (player.getPlayQueue() != null) {
+ player.selectQueueItem(player.getPlayQueue().getItem((int) id));
+ }
}
@Override
- public void onSkipToNext(@NonNull final Player player) {
- callback.playNext();
+ public void onSkipToNext(@NonNull final com.google.android.exoplayer2.Player exoPlayer) {
+ player.playNext();
}
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());
activeQueueItemId = MediaSessionCompat.QueueItem.UNKNOWN_ID;
return;
}
// Yes this is almost a copypasta, got a problem with that? =\
- final int windowCount = callback.getQueueSize();
- final int currentWindowIndex = callback.getCurrentPlayingIndex();
- final int queueSize = Math.min(maxQueueSize, windowCount);
+ final int currentWindowIndex = player.getPlayQueue().getIndex();
+ final int queueSize = Math.min(MAX_QUEUE_SIZE, windowCount);
final int startIndex = Util.constrainValue(currentWindowIndex - ((queueSize - 1) / 2), 0,
windowCount - queueSize);
final List queue = new ArrayList<>();
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);
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
- public boolean onCommand(@NonNull final Player player,
+ public boolean onCommand(@NonNull final com.google.android.exoplayer2.Player exoPlayer,
@NonNull final String command,
@Nullable final Bundle extras,
@Nullable final ResultReceiver cb) {
diff --git a/app/src/main/java/org/schabi/newpipe/player/mediasource/FailedMediaSource.java b/app/src/main/java/org/schabi/newpipe/player/mediasource/FailedMediaSource.java
index 8aad356d0ae..b9ca90d89fa 100644
--- a/app/src/main/java/org/schabi/newpipe/player/mediasource/FailedMediaSource.java
+++ b/app/src/main/java/org/schabi/newpipe/player/mediasource/FailedMediaSource.java
@@ -16,7 +16,7 @@
import org.schabi.newpipe.player.playqueue.PlayQueueItem;
import java.io.IOException;
-import java.util.Collections;
+import java.util.List;
import java.util.concurrent.TimeUnit;
import androidx.annotation.NonNull;
@@ -56,9 +56,7 @@ public FailedMediaSource(@NonNull final PlayQueueItem playQueueItem,
this.playQueueItem = playQueueItem;
this.error = error;
this.retryTimestamp = retryTimestamp;
- this.mediaItem = ExceptionTag
- .of(playQueueItem, Collections.singletonList(error))
- .withExtras(this)
+ this.mediaItem = ExceptionTag.of(playQueueItem, List.of(error)).withExtras(this)
.asMediaItem();
}
diff --git a/app/src/main/java/org/schabi/newpipe/player/NotificationConstants.java b/app/src/main/java/org/schabi/newpipe/player/notification/NotificationConstants.java
similarity index 80%
rename from app/src/main/java/org/schabi/newpipe/player/NotificationConstants.java
rename to app/src/main/java/org/schabi/newpipe/player/notification/NotificationConstants.java
index 6c9858d1bdf..89bf0b22ae2 100644
--- a/app/src/main/java/org/schabi/newpipe/player/NotificationConstants.java
+++ b/app/src/main/java/org/schabi/newpipe/player/notification/NotificationConstants.java
@@ -1,4 +1,4 @@
-package org.schabi.newpipe.player;
+package org.schabi.newpipe.player.notification;
import android.content.Context;
import android.content.SharedPreferences;
@@ -7,20 +7,48 @@
import androidx.annotation.IntDef;
import androidx.annotation.NonNull;
+import org.schabi.newpipe.App;
import org.schabi.newpipe.R;
import org.schabi.newpipe.util.Localization;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.ArrayList;
-import java.util.Arrays;
import java.util.List;
import java.util.SortedSet;
import java.util.TreeSet;
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;
@@ -86,7 +114,7 @@ private NotificationConstants() { }
};
- public static final Integer[] SLOT_COMPACT_DEFAULTS = {0, 1, 2};
+ public static final List SLOT_COMPACT_DEFAULTS = List.of(0, 1, 2);
public static final int[] SLOT_COMPACT_PREF_KEYS = {
R.string.notification_slot_compact_0_key,
@@ -152,7 +180,7 @@ public static List getCompactSlotsFromPreferences(
if (compactSlot == Integer.MAX_VALUE) {
// 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
diff --git a/app/src/main/java/org/schabi/newpipe/player/notification/NotificationPlayerUi.java b/app/src/main/java/org/schabi/newpipe/player/notification/NotificationPlayerUi.java
new file mode 100644
index 00000000000..4b1fc417f0e
--- /dev/null
+++ b/app/src/main/java/org/schabi/newpipe/player/notification/NotificationPlayerUi.java
@@ -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);
+ }
+}
diff --git a/app/src/main/java/org/schabi/newpipe/player/NotificationUtil.java b/app/src/main/java/org/schabi/newpipe/player/notification/NotificationUtil.java
similarity index 69%
rename from app/src/main/java/org/schabi/newpipe/player/NotificationUtil.java
rename to app/src/main/java/org/schabi/newpipe/player/notification/NotificationUtil.java
index 948343be2ba..3488ec61e29 100644
--- a/app/src/main/java/org/schabi/newpipe/player/NotificationUtil.java
+++ b/app/src/main/java/org/schabi/newpipe/player/notification/NotificationUtil.java
@@ -1,16 +1,15 @@
-package org.schabi.newpipe.player;
+package org.schabi.newpipe.player.notification;
import android.annotation.SuppressLint;
import android.app.PendingIntent;
-import android.app.Service;
import android.content.Intent;
import android.content.pm.ServiceInfo;
import android.graphics.Bitmap;
-import android.graphics.Matrix;
import android.os.Build;
import android.util.Log;
import androidx.annotation.DrawableRes;
+import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.StringRes;
import androidx.core.app.NotificationCompat;
@@ -20,48 +19,45 @@
import org.schabi.newpipe.MainActivity;
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 java.util.List;
+import java.util.Objects;
+import java.util.Optional;
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_ONE;
-import static org.schabi.newpipe.player.MainPlayer.ACTION_CLOSE;
-import static org.schabi.newpipe.player.MainPlayer.ACTION_FAST_FORWARD;
-import static org.schabi.newpipe.player.MainPlayer.ACTION_FAST_REWIND;
-import static org.schabi.newpipe.player.MainPlayer.ACTION_PLAY_NEXT;
-import static org.schabi.newpipe.player.MainPlayer.ACTION_PLAY_PAUSE;
-import static org.schabi.newpipe.player.MainPlayer.ACTION_PLAY_PREVIOUS;
-import static org.schabi.newpipe.player.MainPlayer.ACTION_REPEAT;
-import static org.schabi.newpipe.player.MainPlayer.ACTION_SHUFFLE;
+import static org.schabi.newpipe.player.notification.NotificationConstants.ACTION_CLOSE;
+import static org.schabi.newpipe.player.notification.NotificationConstants.ACTION_FAST_FORWARD;
+import static org.schabi.newpipe.player.notification.NotificationConstants.ACTION_FAST_REWIND;
+import static org.schabi.newpipe.player.notification.NotificationConstants.ACTION_PLAY_NEXT;
+import static org.schabi.newpipe.player.notification.NotificationConstants.ACTION_PLAY_PAUSE;
+import static org.schabi.newpipe.player.notification.NotificationConstants.ACTION_PLAY_PREVIOUS;
+import static org.schabi.newpipe.player.notification.NotificationConstants.ACTION_REPEAT;
+import static org.schabi.newpipe.player.notification.NotificationConstants.ACTION_SHUFFLE;
/**
* This is a utility class for player notifications.
- *
- * @author cool-student
*/
public final class NotificationUtil {
private static final String TAG = NotificationUtil.class.getSimpleName();
private static final boolean DEBUG = Player.DEBUG;
private static final int NOTIFICATION_ID = 123789;
- @Nullable private static NotificationUtil instance = null;
-
@NotificationConstants.Action
private final int[] notificationSlots = NotificationConstants.SLOT_DEFAULTS.clone();
private NotificationManagerCompat notificationManager;
private NotificationCompat.Builder notificationBuilder;
- private NotificationUtil() {
- }
+ private final Player player;
- public static NotificationUtil getInstance() {
- if (instance == null) {
- instance = new NotificationUtil();
- }
- return instance;
+ public NotificationUtil(final Player player) {
+ this.player = player;
}
@@ -72,20 +68,31 @@ public static NotificationUtil getInstance() {
/**
* 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.
- * @param player the player currently open, to take data from
* @param forceRecreate whether to force the recreation of the notification even if it already
* exists
*/
- synchronized void createNotificationIfNeededAndUpdate(final Player player,
- final boolean forceRecreate) {
+ public synchronized void createNotificationIfNeededAndUpdate(final boolean forceRecreate) {
if (forceRecreate || notificationBuilder == null) {
- notificationBuilder = createNotification(player);
+ notificationBuilder = createNotification();
}
- updateNotification(player);
+ updateNotification();
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) {
Log.d(TAG, "createNotification()");
}
@@ -94,7 +101,7 @@ private synchronized NotificationCompat.Builder createNotification(final Player
new NotificationCompat.Builder(player.getContext(),
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
int nonNothingSlotCount = 5;
@@ -108,14 +115,15 @@ private synchronized NotificationCompat.Builder createNotification(final Player
// build the compact slot indices array (need code to convert from Integer... because Java)
final List compactSlotList = NotificationConstants.getCompactSlotsFromPreferences(
player.getContext(), player.getPrefs(), nonNothingSlotCount);
- final int[] compactSlots = new int[compactSlotList.size()];
- for (int i = 0; i < compactSlotList.size(); i++) {
- compactSlots[i] = compactSlotList.get(i);
- }
+ final int[] compactSlots = compactSlotList.stream().mapToInt(Integer::intValue).toArray();
- builder.setStyle(new androidx.media.app.NotificationCompat.MediaStyle()
- .setMediaSession(player.getMediaSessionManager().getSessionToken())
- .setShowActionsInCompactView(compactSlots))
+ final MediaStyle mediaStyle = new MediaStyle().setShowActionsInCompactView(compactSlots);
+ player.UIs()
+ .get(MediaSessionPlayerUi.class)
+ .flatMap(MediaSessionPlayerUi::getSessionToken)
+ .ifPresent(mediaStyle::setMediaSession);
+
+ builder.setStyle(mediaStyle)
.setPriority(NotificationCompat.PRIORITY_HIGH)
.setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
.setCategory(NotificationCompat.CATEGORY_TRANSPORT)
@@ -128,35 +136,33 @@ private synchronized NotificationCompat.Builder createNotification(final Player
.setDeleteIntent(PendingIntent.getBroadcast(player.getContext(), NOTIFICATION_ID,
new Intent(ACTION_CLOSE), FLAG_UPDATE_CURRENT));
+ // set the initial value for the video thumbnail, updatable with updateNotificationThumbnail
+ setLargeIcon(builder);
+
return builder;
}
/**
* 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) {
Log.d(TAG, "updateNotification()");
}
// also update content intent, in case the user switched players
notificationBuilder.setContentIntent(PendingIntent.getActivity(player.getContext(),
- NOTIFICATION_ID, getIntentForNotification(player), FLAG_UPDATE_CURRENT));
+ NOTIFICATION_ID, getIntentForNotification(), FLAG_UPDATE_CURRENT));
notificationBuilder.setContentTitle(player.getVideoTitle());
notificationBuilder.setContentText(player.getUploaderName());
notificationBuilder.setTicker(player.getVideoTitle());
- updateActions(notificationBuilder, player);
- final boolean showThumbnail = player.getPrefs().getBoolean(
- player.getContext().getString(R.string.show_thumbnail_key), true);
- if (showThumbnail) {
- setLargeIcon(notificationBuilder, player);
- }
+
+ updateActions(notificationBuilder);
}
@SuppressLint("RestrictedApi")
- boolean shouldUpdateBufferingSlot() {
+ public boolean shouldUpdateBufferingSlot() {
if (notificationBuilder == null) {
// if there is no notification active, there is no point in updating it
return false;
@@ -174,22 +180,22 @@ boolean shouldUpdateBufferingSlot() {
}
- void createNotificationAndStartForeground(final Player player, final Service service) {
+ public void createNotificationAndStartForeground() {
if (notificationBuilder == null) {
- notificationBuilder = createNotification(player);
+ notificationBuilder = createNotification();
}
- updateNotification(player);
+ updateNotification();
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);
} else {
- service.startForeground(NOTIFICATION_ID, notificationBuilder.build());
+ player.getService().startForeground(NOTIFICATION_ID, notificationBuilder.build());
}
}
- void cancelNotificationAndStopForeground(final Service service) {
- ServiceCompat.stopForeground(service, ServiceCompat.STOP_FOREGROUND_REMOVE);
+ public void cancelNotificationAndStopForeground() {
+ ServiceCompat.stopForeground(player.getService(), ServiceCompat.STOP_FOREGROUND_REMOVE);
if (notificationManager != null) {
notificationManager.cancel(NOTIFICATION_ID);
@@ -203,7 +209,7 @@ void cancelNotificationAndStopForeground(final Service service) {
// ACTIONS
/////////////////////////////////////////////////////
- private void initializeNotificationSlots(final Player player) {
+ private void initializeNotificationSlots() {
for (int i = 0; i < 5; ++i) {
notificationSlots[i] = player.getPrefs().getInt(
player.getContext().getString(NotificationConstants.SLOT_PREF_KEYS[i]),
@@ -212,17 +218,16 @@ private void initializeNotificationSlots(final Player player) {
}
@SuppressLint("RestrictedApi")
- private void updateActions(final NotificationCompat.Builder builder, final Player player) {
+ private void updateActions(final NotificationCompat.Builder builder) {
builder.mActions.clear();
for (int i = 0; i < 5; ++i) {
- addAction(builder, player, notificationSlots[i]);
+ addAction(builder, notificationSlots[i]);
}
}
private void addAction(final NotificationCompat.Builder builder,
- final Player player,
@NotificationConstants.Action final int slot) {
- final NotificationCompat.Action action = getAction(player, slot);
+ final NotificationCompat.Action action = getAction(slot);
if (action != null) {
builder.addAction(action);
}
@@ -230,41 +235,40 @@ private void addAction(final NotificationCompat.Builder builder,
@Nullable
private NotificationCompat.Action getAction(
- final Player player,
@NotificationConstants.Action final int selectedAction) {
final int baseActionIcon = NotificationConstants.ACTION_ICONS[selectedAction];
switch (selectedAction) {
case NotificationConstants.PREVIOUS:
- return getAction(player, baseActionIcon,
+ return getAction(baseActionIcon,
R.string.exo_controls_previous_description, ACTION_PLAY_PREVIOUS);
case NotificationConstants.NEXT:
- return getAction(player, baseActionIcon,
+ return getAction(baseActionIcon,
R.string.exo_controls_next_description, ACTION_PLAY_NEXT);
case NotificationConstants.REWIND:
- return getAction(player, baseActionIcon,
+ return getAction(baseActionIcon,
R.string.exo_controls_rewind_description, ACTION_FAST_REWIND);
case NotificationConstants.FORWARD:
- return getAction(player, baseActionIcon,
+ return getAction(baseActionIcon,
R.string.exo_controls_fastforward_description, ACTION_FAST_FORWARD);
case NotificationConstants.SMART_REWIND_PREVIOUS:
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);
} 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);
}
case NotificationConstants.SMART_FORWARD_NEXT:
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);
} 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);
}
@@ -278,44 +282,45 @@ private NotificationCompat.Action getAction(
null);
}
+ // fallthrough
case NotificationConstants.PLAY_PAUSE:
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);
} else if (player.isPlaying()
|| player.getCurrentState() == Player.STATE_PREFLIGHT
|| player.getCurrentState() == Player.STATE_BLOCKED
|| 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);
} 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);
}
case NotificationConstants.REPEAT:
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);
} 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);
} 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);
}
case NotificationConstants.SHUFFLE:
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);
} 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);
}
case NotificationConstants.CLOSE:
- return getAction(player, R.drawable.ic_close,
+ return getAction(R.drawable.ic_close,
R.string.close, ACTION_CLOSE);
case NotificationConstants.NOTHING:
@@ -325,8 +330,7 @@ private NotificationCompat.Action getAction(
}
}
- private NotificationCompat.Action getAction(final Player player,
- @DrawableRes final int drawable,
+ private NotificationCompat.Action getAction(@DrawableRes final int drawable,
@StringRes final int title,
final String intentAction) {
return new NotificationCompat.Action(drawable, player.getContext().getString(title),
@@ -334,7 +338,7 @@ private NotificationCompat.Action getAction(final Player player,
new Intent(intentAction), FLAG_UPDATE_CURRENT));
}
- private Intent getIntentForNotification(final Player player) {
+ private Intent getIntentForNotification() {
if (player.audioPlayerSelected() || player.popupPlayerSelected()) {
// Means we play in popup or audio only. Let's show the play queue
return NavigationHelper.getPlayQueueActivityIntent(player.getContext());
@@ -354,28 +358,34 @@ private Intent getIntentForNotification(final Player player) {
// 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(
player.getContext().getString(R.string.scale_to_square_image_in_notifications_key),
false);
if (scaleImageToSquareAspectRatio) {
- builder.setLargeIcon(getBitmapWithSquareAspectRatio(player.getThumbnail()));
+ builder.setLargeIcon(getBitmapWithSquareAspectRatio(thumbnail));
} else {
- builder.setLargeIcon(player.getThumbnail());
+ builder.setLargeIcon(thumbnail);
}
}
- private Bitmap getBitmapWithSquareAspectRatio(final Bitmap bitmap) {
- return getResizedBitmap(bitmap, bitmap.getWidth(), bitmap.getWidth());
- }
-
- private Bitmap getResizedBitmap(final Bitmap bitmap, final int newWidth, final int newHeight) {
- final int width = bitmap.getWidth();
- final int height = bitmap.getHeight();
- final float scaleWidth = ((float) newWidth) / width;
- final float scaleHeight = ((float) newHeight) / height;
- final Matrix matrix = new Matrix();
- matrix.postScale(scaleWidth, scaleHeight);
- return Bitmap.createBitmap(bitmap, 0, 0, width, height, matrix, false);
+ private Bitmap getBitmapWithSquareAspectRatio(@NonNull final Bitmap bitmap) {
+ // Find the smaller dimension and then take a center portion of the image that
+ // has that size.
+ final int w = bitmap.getWidth();
+ final int h = bitmap.getHeight();
+ final int dstSize = Math.min(w, h);
+ final int x = (w - dstSize) / 2;
+ final int y = (h - dstSize) / 2;
+ return Bitmap.createBitmap(bitmap, x, y, dstSize, dstSize);
}
}
diff --git a/app/src/main/java/org/schabi/newpipe/player/playback/PlayerMediaSession.java b/app/src/main/java/org/schabi/newpipe/player/playback/PlayerMediaSession.java
deleted file mode 100644
index ee0a6f11819..00000000000
--- a/app/src/main/java/org/schabi/newpipe/player/playback/PlayerMediaSession.java
+++ /dev/null
@@ -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();
- }
-}
diff --git a/app/src/main/java/org/schabi/newpipe/player/playback/SurfaceHolderCallback.java b/app/src/main/java/org/schabi/newpipe/player/playback/SurfaceHolderCallback.java
index 5d67e6967ef..da6cb36d4fb 100644
--- a/app/src/main/java/org/schabi/newpipe/player/playback/SurfaceHolderCallback.java
+++ b/app/src/main/java/org/schabi/newpipe/player/playback/SurfaceHolderCallback.java
@@ -4,7 +4,7 @@
import android.view.SurfaceHolder;
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'
@@ -26,7 +26,7 @@ public final class SurfaceHolderCallback implements SurfaceHolder.Callback {
private final Context context;
private final Player player;
- private DummySurface dummySurface;
+ private PlaceholderSurface placeholderSurface;
public SurfaceHolderCallback(final Context context, final Player player) {
this.context = context;
@@ -47,16 +47,16 @@ public void surfaceChanged(final SurfaceHolder holder,
@Override
public void surfaceDestroyed(final SurfaceHolder holder) {
- if (dummySurface == null) {
- dummySurface = DummySurface.newInstanceV17(context, false);
+ if (placeholderSurface == null) {
+ placeholderSurface = PlaceholderSurface.newInstanceV17(context, false);
}
- player.setVideoSurface(dummySurface);
+ player.setVideoSurface(placeholderSurface);
}
public void release() {
- if (dummySurface != null) {
- dummySurface.release();
- dummySurface = null;
+ if (placeholderSurface != null) {
+ placeholderSurface.release();
+ placeholderSurface = null;
}
}
}
diff --git a/app/src/main/java/org/schabi/newpipe/player/playqueue/AbstractInfoPlayQueue.java b/app/src/main/java/org/schabi/newpipe/player/playqueue/AbstractInfoPlayQueue.java
index df2747c3b74..e51ee4720d4 100644
--- a/app/src/main/java/org/schabi/newpipe/player/playqueue/AbstractInfoPlayQueue.java
+++ b/app/src/main/java/org/schabi/newpipe/player/playqueue/AbstractInfoPlayQueue.java
@@ -82,7 +82,7 @@ public void onSuccess(@NonNull final T result) {
public void onError(@NonNull final Throwable e) {
Log.e(getTag(), "Error fetching more playlist, marking playlist as complete.", e);
isComplete = true;
- append(); // Notify change
+ notifyChange();
}
};
}
@@ -117,7 +117,7 @@ public void onSuccess(
public void onError(@NonNull final Throwable e) {
Log.e(getTag(), "Error fetching more playlist, marking playlist as complete.", e);
isComplete = true;
- append(); // Notify change
+ notifyChange();
}
};
}
diff --git a/app/src/main/java/org/schabi/newpipe/player/playqueue/PlayQueue.java b/app/src/main/java/org/schabi/newpipe/player/playqueue/PlayQueue.java
index f46c9d72fd0..edf5a771c02 100644
--- a/app/src/main/java/org/schabi/newpipe/player/playqueue/PlayQueue.java
+++ b/app/src/main/java/org/schabi/newpipe/player/playqueue/PlayQueue.java
@@ -16,7 +16,6 @@
import java.io.Serializable;
import java.util.ArrayList;
-import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;
@@ -258,13 +257,10 @@ public synchronized void offsetIndex(final int offset) {
}
/**
- * Appends the given {@link PlayQueueItem}s to the current play queue.
- *
- * @see #append(List items)
- * @param items {@link PlayQueueItem}s to append
+ * Notifies that a change has occurred.
*/
- public synchronized void append(@NonNull final PlayQueueItem... items) {
- append(Arrays.asList(items));
+ public synchronized void notifyChange() {
+ broadcast(new AppendEvent(0));
}
/**
diff --git a/app/src/main/java/org/schabi/newpipe/player/playqueue/PlayQueueItemTouchCallback.java b/app/src/main/java/org/schabi/newpipe/player/playqueue/PlayQueueItemTouchCallback.java
index b283e105ec6..6e2792d4f85 100644
--- a/app/src/main/java/org/schabi/newpipe/player/playqueue/PlayQueueItemTouchCallback.java
+++ b/app/src/main/java/org/schabi/newpipe/player/playqueue/PlayQueueItemTouchCallback.java
@@ -1,5 +1,7 @@
package org.schabi.newpipe.player.playqueue;
+import androidx.annotation.NonNull;
+import androidx.core.math.MathUtils;
import androidx.recyclerview.widget.ItemTouchHelper;
import androidx.recyclerview.widget.RecyclerView;
@@ -16,18 +18,21 @@ public PlayQueueItemTouchCallback() {
public abstract void onSwiped(int index);
@Override
- public int interpolateOutOfBoundsScroll(final RecyclerView recyclerView, final int viewSize,
- final int viewSizeOutOfBounds, final int totalSize,
+ public int interpolateOutOfBoundsScroll(@NonNull final RecyclerView recyclerView,
+ final int viewSize,
+ final int viewSizeOutOfBounds,
+ final int totalSize,
final long msSinceStartScroll) {
final int standardSpeed = super.interpolateOutOfBoundsScroll(recyclerView, viewSize,
viewSizeOutOfBounds, totalSize, msSinceStartScroll);
- final int clampedAbsVelocity = Math.max(MINIMUM_INITIAL_DRAG_VELOCITY,
- Math.min(Math.abs(standardSpeed), MAXIMUM_INITIAL_DRAG_VELOCITY));
+ final int clampedAbsVelocity = MathUtils.clamp(Math.abs(standardSpeed),
+ MINIMUM_INITIAL_DRAG_VELOCITY, MAXIMUM_INITIAL_DRAG_VELOCITY);
return clampedAbsVelocity * (int) Math.signum(viewSizeOutOfBounds);
}
@Override
- public boolean onMove(final RecyclerView recyclerView, final RecyclerView.ViewHolder source,
+ public boolean onMove(@NonNull final RecyclerView recyclerView,
+ final RecyclerView.ViewHolder source,
final RecyclerView.ViewHolder target) {
if (source.getItemViewType() != target.getItemViewType()) {
return false;
diff --git a/app/src/main/java/org/schabi/newpipe/player/playqueue/SinglePlayQueue.java b/app/src/main/java/org/schabi/newpipe/player/playqueue/SinglePlayQueue.java
index 527e804703c..e5117321407 100644
--- a/app/src/main/java/org/schabi/newpipe/player/playqueue/SinglePlayQueue.java
+++ b/app/src/main/java/org/schabi/newpipe/player/playqueue/SinglePlayQueue.java
@@ -4,20 +4,19 @@
import org.schabi.newpipe.extractor.stream.StreamInfoItem;
import java.util.ArrayList;
-import java.util.Collections;
import java.util.List;
public final class SinglePlayQueue extends PlayQueue {
public SinglePlayQueue(final StreamInfoItem item) {
- super(0, Collections.singletonList(new PlayQueueItem(item)));
+ super(0, List.of(new PlayQueueItem(item)));
}
public SinglePlayQueue(final StreamInfo info) {
- super(0, Collections.singletonList(new PlayQueueItem(info)));
+ super(0, List.of(new PlayQueueItem(info)));
}
public SinglePlayQueue(final StreamInfo info, final long startPosition) {
- super(0, Collections.singletonList(new PlayQueueItem(info)));
+ super(0, List.of(new PlayQueueItem(info)));
getItem().setRecoveryPosition(startPosition);
}
diff --git a/app/src/main/java/org/schabi/newpipe/player/resolver/PlaybackResolver.java b/app/src/main/java/org/schabi/newpipe/player/resolver/PlaybackResolver.java
index 34e7e9bd1f1..ead1272503c 100644
--- a/app/src/main/java/org/schabi/newpipe/player/resolver/PlaybackResolver.java
+++ b/app/src/main/java/org/schabi/newpipe/player/resolver/PlaybackResolver.java
@@ -172,9 +172,10 @@ static MediaSource maybeBuildLiveMediaSource(final PlayerDataSource dataSource,
try {
final StreamInfoTag tag = StreamInfoTag.of(info);
if (!info.getHlsUrl().isEmpty()) {
- return buildLiveMediaSource(dataSource, info.getHlsUrl(), C.TYPE_HLS, tag);
+ return buildLiveMediaSource(dataSource, info.getHlsUrl(), C.CONTENT_TYPE_HLS, tag);
} else if (!info.getDashMpdUrl().isEmpty()) {
- return buildLiveMediaSource(dataSource, info.getDashMpdUrl(), C.TYPE_DASH, tag);
+ return buildLiveMediaSource(
+ dataSource, info.getDashMpdUrl(), C.CONTENT_TYPE_DASH, tag);
}
} catch (final Exception e) {
Log.w(TAG, "Error when generating live media source, falling back to standard sources",
@@ -190,17 +191,17 @@ static MediaSource buildLiveMediaSource(final PlayerDataSource dataSource,
final MediaItemTag metadata) throws ResolverException {
final MediaSource.Factory factory;
switch (type) {
- case C.TYPE_SS:
+ case C.CONTENT_TYPE_SS:
factory = dataSource.getLiveSsMediaSourceFactory();
break;
- case C.TYPE_DASH:
+ case C.CONTENT_TYPE_DASH:
factory = dataSource.getLiveDashMediaSourceFactory();
break;
- case C.TYPE_HLS:
+ case C.CONTENT_TYPE_HLS:
factory = dataSource.getLiveHlsMediaSourceFactory();
break;
- case C.TYPE_OTHER:
- case C.TYPE_RTSP:
+ case C.CONTENT_TYPE_OTHER:
+ case C.CONTENT_TYPE_RTSP:
default:
throw new ResolverException("Unsupported type: " + type);
}
diff --git a/app/src/main/java/org/schabi/newpipe/player/seekbarpreview/SeekbarPreviewThumbnailHelper.java b/app/src/main/java/org/schabi/newpipe/player/seekbarpreview/SeekbarPreviewThumbnailHelper.java
index 54d11da8380..43d89055c7b 100644
--- a/app/src/main/java/org/schabi/newpipe/player/seekbarpreview/SeekbarPreviewThumbnailHelper.java
+++ b/app/src/main/java/org/schabi/newpipe/player/seekbarpreview/SeekbarPreviewThumbnailHelper.java
@@ -8,13 +8,13 @@
import androidx.annotation.IntDef;
import androidx.annotation.NonNull;
+import androidx.core.math.MathUtils;
import androidx.preference.PreferenceManager;
import org.schabi.newpipe.R;
import org.schabi.newpipe.util.DeviceUtils;
import java.lang.annotation.Retention;
-import java.util.Objects;
import java.util.Optional;
import java.util.function.IntSupplier;
@@ -79,19 +79,14 @@ public static void tryResizeAndSetSeekbarPreviewThumbnail(
// Resize original bitmap
try {
- Objects.requireNonNull(srcBitmap);
-
final int srcWidth = srcBitmap.getWidth() > 0 ? srcBitmap.getWidth() : 1;
- final int newWidth = Math.max(
- Math.min(
- // Use 1/4 of the width for the preview
- Math.round(baseViewWidthSupplier.getAsInt() / 4f),
- // Scaling more than that factor looks really pixelated -> max
- Math.round(srcWidth * 2.5f)
- ),
- // Min width = 10dp
- DeviceUtils.dpToPx(10, context)
- );
+ final int newWidth = MathUtils.clamp(
+ // Use 1/4 of the width for the preview
+ Math.round(baseViewWidthSupplier.getAsInt() / 4f),
+ // But have a min width of 10dp
+ DeviceUtils.dpToPx(10, context),
+ // And scaling more than that factor looks really pixelated -> max
+ Math.round(srcWidth * 2.5f));
final float scaleFactor = (float) newWidth / srcWidth;
final int newHeight = (int) (srcBitmap.getHeight() * scaleFactor);
diff --git a/app/src/main/java/org/schabi/newpipe/player/ui/MainPlayerUi.java b/app/src/main/java/org/schabi/newpipe/player/ui/MainPlayerUi.java
new file mode 100644
index 00000000000..81dc954d1a5
--- /dev/null
+++ b/app/src/main/java/org/schabi/newpipe/player/ui/MainPlayerUi.java
@@ -0,0 +1,981 @@
+package org.schabi.newpipe.player.ui;
+
+import static android.view.ViewGroup.LayoutParams.MATCH_PARENT;
+import static org.schabi.newpipe.MainActivity.DEBUG;
+import static org.schabi.newpipe.QueueItemMenuUtil.openPopupMenu;
+import static org.schabi.newpipe.ktx.ViewUtils.animate;
+import static org.schabi.newpipe.player.Player.STATE_COMPLETED;
+import static org.schabi.newpipe.player.Player.STATE_PAUSED;
+import static org.schabi.newpipe.player.helper.PlayerHelper.MinimizeMode.MINIMIZE_ON_EXIT_MODE_BACKGROUND;
+import static org.schabi.newpipe.player.helper.PlayerHelper.MinimizeMode.MINIMIZE_ON_EXIT_MODE_NONE;
+import static org.schabi.newpipe.player.helper.PlayerHelper.MinimizeMode.MINIMIZE_ON_EXIT_MODE_POPUP;
+import static org.schabi.newpipe.player.helper.PlayerHelper.getMinimizeOnExitAction;
+import static org.schabi.newpipe.player.helper.PlayerHelper.getTimeString;
+import static org.schabi.newpipe.player.helper.PlayerHelper.globalScreenOrientationLocked;
+import static org.schabi.newpipe.player.notification.NotificationConstants.ACTION_PLAY_PAUSE;
+
+import android.app.Activity;
+import android.content.Context;
+import android.content.Intent;
+import android.content.res.Resources;
+import android.database.ContentObserver;
+import android.graphics.Bitmap;
+import android.graphics.Color;
+import android.os.Handler;
+import android.os.Looper;
+import android.provider.Settings;
+import android.util.DisplayMetrics;
+import android.util.Log;
+import android.util.TypedValue;
+import android.view.KeyEvent;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.ViewParent;
+import android.view.WindowManager;
+import android.widget.FrameLayout;
+import android.widget.LinearLayout;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.appcompat.app.AppCompatActivity;
+import androidx.appcompat.content.res.AppCompatResources;
+import androidx.fragment.app.FragmentActivity;
+import androidx.recyclerview.widget.ItemTouchHelper;
+import androidx.recyclerview.widget.RecyclerView;
+
+import com.google.android.exoplayer2.video.VideoSize;
+
+import org.schabi.newpipe.R;
+import org.schabi.newpipe.databinding.PlayerBinding;
+import org.schabi.newpipe.extractor.stream.StreamInfo;
+import org.schabi.newpipe.extractor.stream.StreamSegment;
+import org.schabi.newpipe.fragments.OnScrollBelowItemsListener;
+import org.schabi.newpipe.fragments.detail.VideoDetailFragment;
+import org.schabi.newpipe.info_list.StreamSegmentAdapter;
+import org.schabi.newpipe.ktx.AnimationType;
+import org.schabi.newpipe.local.dialog.PlaylistDialog;
+import org.schabi.newpipe.player.Player;
+import org.schabi.newpipe.player.event.PlayerServiceEventListener;
+import org.schabi.newpipe.player.gesture.BasePlayerGestureListener;
+import org.schabi.newpipe.player.gesture.MainPlayerGestureListener;
+import org.schabi.newpipe.player.helper.PlaybackParameterDialog;
+import org.schabi.newpipe.player.helper.PlayerHelper;
+import org.schabi.newpipe.player.playqueue.PlayQueue;
+import org.schabi.newpipe.player.playqueue.PlayQueueAdapter;
+import org.schabi.newpipe.player.playqueue.PlayQueueItem;
+import org.schabi.newpipe.player.playqueue.PlayQueueItemBuilder;
+import org.schabi.newpipe.player.playqueue.PlayQueueItemHolder;
+import org.schabi.newpipe.player.playqueue.PlayQueueItemTouchCallback;
+import org.schabi.newpipe.util.DeviceUtils;
+import org.schabi.newpipe.util.NavigationHelper;
+import org.schabi.newpipe.util.external_communication.KoreUtils;
+
+import java.util.List;
+import java.util.Objects;
+import java.util.Optional;
+
+public final class MainPlayerUi extends VideoPlayerUi implements View.OnLayoutChangeListener {
+ private static final String TAG = MainPlayerUi.class.getSimpleName();
+
+ // see the Javadoc of calculateMaxEndScreenThumbnailHeight for information
+ private static final int DETAIL_ROOT_MINIMUM_HEIGHT = 85; // dp
+ private static final int DETAIL_TITLE_TEXT_SIZE_TV = 16; // sp
+ private static final int DETAIL_TITLE_TEXT_SIZE_TABLET = 15; // sp
+
+ private boolean isFullscreen = false;
+ private boolean isVerticalVideo = false;
+ private boolean fragmentIsVisible = false;
+
+ private ContentObserver settingsContentObserver;
+
+ private PlayQueueAdapter playQueueAdapter;
+ private StreamSegmentAdapter segmentAdapter;
+ private boolean isQueueVisible = false;
+ private boolean areSegmentsVisible = false;
+
+ // fullscreen player
+ private ItemTouchHelper itemTouchHelper;
+
+
+ /*//////////////////////////////////////////////////////////////////////////
+ // Constructor, setup, destroy
+ //////////////////////////////////////////////////////////////////////////*/
+ //region Constructor, setup, destroy
+
+ public MainPlayerUi(@NonNull final Player player,
+ @NonNull final PlayerBinding playerBinding) {
+ super(player, playerBinding);
+ }
+
+ /**
+ * Open fullscreen on tablets where the option to have the main player start automatically in
+ * fullscreen mode is on. Rotating the device to landscape is already done in {@link
+ * VideoDetailFragment#openVideoPlayer(boolean)} when the thumbnail is clicked, and that's
+ * enough for phones, but not for tablets since the mini player can be also shown in landscape.
+ */
+ private void directlyOpenFullscreenIfNeeded() {
+ if (PlayerHelper.isStartMainPlayerFullscreenEnabled(player.getService())
+ && DeviceUtils.isTablet(player.getService())
+ && PlayerHelper.globalScreenOrientationLocked(player.getService())) {
+ player.getFragmentListener().ifPresent(
+ PlayerServiceEventListener::onScreenRotationButtonClicked);
+ }
+ }
+
+ @Override
+ public void setupAfterIntent() {
+ // needed for tablets, check the function for a better explanation
+ directlyOpenFullscreenIfNeeded();
+
+ super.setupAfterIntent();
+
+ initVideoPlayer();
+ // Android TV: without it focus will frame the whole player
+ binding.playPauseButton.requestFocus();
+
+ // Note: This is for automatically playing (when "Resume playback" is off), see #6179
+ if (player.getPlayWhenReady()) {
+ player.play();
+ } else {
+ player.pause();
+ }
+ }
+
+ @Override
+ BasePlayerGestureListener buildGestureListener() {
+ return new MainPlayerGestureListener(this);
+ }
+
+ @Override
+ protected void initListeners() {
+ super.initListeners();
+
+ binding.queueButton.setOnClickListener(v -> onQueueClicked());
+ binding.segmentsButton.setOnClickListener(v -> onSegmentsClicked());
+
+ binding.addToPlaylistButton.setOnClickListener(v ->
+ getParentActivity().map(FragmentActivity::getSupportFragmentManager)
+ .ifPresent(fragmentManager ->
+ PlaylistDialog.showForPlayQueue(player, fragmentManager)));
+
+ settingsContentObserver = new ContentObserver(new Handler(Looper.getMainLooper())) {
+ @Override
+ public void onChange(final boolean selfChange) {
+ setupScreenRotationButton();
+ }
+ };
+ context.getContentResolver().registerContentObserver(
+ Settings.System.getUriFor(Settings.System.ACCELEROMETER_ROTATION), false,
+ settingsContentObserver);
+
+ binding.getRoot().addOnLayoutChangeListener(this);
+ }
+
+ @Override
+ protected void deinitListeners() {
+ super.deinitListeners();
+
+ binding.queueButton.setOnClickListener(null);
+ binding.segmentsButton.setOnClickListener(null);
+ binding.addToPlaylistButton.setOnClickListener(null);
+
+ context.getContentResolver().unregisterContentObserver(settingsContentObserver);
+
+ binding.getRoot().removeOnLayoutChangeListener(this);
+ }
+
+ @Override
+ public void initPlayback() {
+ super.initPlayback();
+
+ if (playQueueAdapter != null) {
+ playQueueAdapter.dispose();
+ }
+ playQueueAdapter = new PlayQueueAdapter(context,
+ Objects.requireNonNull(player.getPlayQueue()));
+ segmentAdapter = new StreamSegmentAdapter(getStreamSegmentListener());
+ }
+
+ @Override
+ public void removeViewFromParent() {
+ // view was added to fragment
+ final ViewParent parent = binding.getRoot().getParent();
+ if (parent instanceof ViewGroup) {
+ ((ViewGroup) parent).removeView(binding.getRoot());
+ }
+ }
+
+ @Override
+ public void destroy() {
+ super.destroy();
+
+ // Exit from fullscreen when user closes the player via notification
+ if (isFullscreen) {
+ toggleFullscreen();
+ }
+
+ removeViewFromParent();
+ }
+
+ @Override
+ public void destroyPlayer() {
+ super.destroyPlayer();
+
+ if (playQueueAdapter != null) {
+ playQueueAdapter.unsetSelectedListener();
+ playQueueAdapter.dispose();
+ }
+ }
+
+ @Override
+ public void smoothStopForImmediateReusing() {
+ super.smoothStopForImmediateReusing();
+ // Android TV will handle back button in case controls will be visible
+ // (one more additional unneeded click while the player is hidden)
+ hideControls(0, 0);
+ closeItemsList();
+ }
+
+ private void initVideoPlayer() {
+ // restore last resize mode
+ setResizeMode(PlayerHelper.retrieveResizeModeFromPrefs(player));
+ binding.getRoot().setLayoutParams(new FrameLayout.LayoutParams(MATCH_PARENT, MATCH_PARENT));
+ }
+
+ @Override
+ protected void setupElementsVisibility() {
+ super.setupElementsVisibility();
+
+ closeItemsList();
+ showHideKodiButton();
+ binding.fullScreenButton.setVisibility(View.GONE);
+ setupScreenRotationButton();
+ binding.resizeTextView.setVisibility(View.VISIBLE);
+ binding.getRoot().findViewById(R.id.metadataView).setVisibility(View.VISIBLE);
+ binding.moreOptionsButton.setVisibility(View.VISIBLE);
+ binding.topControls.setOrientation(LinearLayout.VERTICAL);
+ binding.primaryControls.getLayoutParams().width = MATCH_PARENT;
+ binding.secondaryControls.setVisibility(View.INVISIBLE);
+ binding.moreOptionsButton.setImageDrawable(AppCompatResources.getDrawable(context,
+ R.drawable.ic_expand_more));
+ binding.share.setVisibility(View.VISIBLE);
+ binding.openInBrowser.setVisibility(View.VISIBLE);
+ binding.switchMute.setVisibility(View.VISIBLE);
+ binding.playerCloseButton.setVisibility(isFullscreen ? View.GONE : View.VISIBLE);
+ // Top controls have a large minHeight which is allows to drag the player
+ // down in fullscreen mode (just larger area to make easy to locate by finger)
+ binding.topControls.setClickable(true);
+ binding.topControls.setFocusable(true);
+
+ binding.titleTextView.setVisibility(isFullscreen ? View.VISIBLE : View.GONE);
+ binding.channelTextView.setVisibility(isFullscreen ? View.VISIBLE : View.GONE);
+ }
+
+ @Override
+ protected void setupElementsSize(final Resources resources) {
+ setupElementsSize(
+ resources.getDimensionPixelSize(R.dimen.player_main_buttons_min_width),
+ resources.getDimensionPixelSize(R.dimen.player_main_top_padding),
+ resources.getDimensionPixelSize(R.dimen.player_main_controls_padding),
+ resources.getDimensionPixelSize(R.dimen.player_main_buttons_padding)
+ );
+ }
+ //endregion
+
+
+ /*//////////////////////////////////////////////////////////////////////////
+ // Broadcast receiver
+ //////////////////////////////////////////////////////////////////////////*/
+ //region Broadcast receiver
+
+ @Override
+ public void onBroadcastReceived(final Intent intent) {
+ super.onBroadcastReceived(intent);
+ if (Intent.ACTION_CONFIGURATION_CHANGED.equals(intent.getAction())) {
+ // Close it because when changing orientation from portrait
+ // (in fullscreen mode) the size of queue layout can be larger than the screen size
+ closeItemsList();
+ } else if (ACTION_PLAY_PAUSE.equals(intent.getAction())) {
+ // Ensure that we have audio-only stream playing when a user
+ // started to play from notification's play button from outside of the app
+ if (!fragmentIsVisible) {
+ onFragmentStopped();
+ }
+ } else if (VideoDetailFragment.ACTION_VIDEO_FRAGMENT_STOPPED.equals(intent.getAction())) {
+ fragmentIsVisible = false;
+ onFragmentStopped();
+ } else if (VideoDetailFragment.ACTION_VIDEO_FRAGMENT_RESUMED.equals(intent.getAction())) {
+ // Restore video source when user returns to the fragment
+ fragmentIsVisible = true;
+ player.useVideoSource(true);
+
+ // When a user returns from background, the system UI will always be shown even if
+ // controls are invisible: hide it in that case
+ if (!isControlsVisible()) {
+ hideSystemUIIfNeeded();
+ }
+ }
+ }
+ //endregion
+
+
+ /*//////////////////////////////////////////////////////////////////////////
+ // Fragment binding
+ //////////////////////////////////////////////////////////////////////////*/
+ //region Fragment binding
+
+ @Override
+ public void onFragmentListenerSet() {
+ super.onFragmentListenerSet();
+ fragmentIsVisible = true;
+ // Apply window insets because Android will not do it when orientation changes
+ // from landscape to portrait
+ if (!isFullscreen) {
+ binding.playbackControlRoot.setPadding(0, 0, 0, 0);
+ }
+ binding.itemsListPanel.setPadding(0, 0, 0, 0);
+ player.getFragmentListener().ifPresent(PlayerServiceEventListener::onViewCreated);
+ }
+
+ /**
+ * This will be called when a user goes to another app/activity, turns off a screen.
+ * We don't want to interrupt playback and don't want to see notification so
+ * next lines of code will enable audio-only playback only if needed
+ */
+ private void onFragmentStopped() {
+ if (player.isPlaying() || player.isLoading()) {
+ switch (getMinimizeOnExitAction(context)) {
+ case MINIMIZE_ON_EXIT_MODE_BACKGROUND:
+ player.useVideoSource(false);
+ break;
+ case MINIMIZE_ON_EXIT_MODE_POPUP:
+ getParentActivity().ifPresent(activity -> {
+ player.setRecovery();
+ NavigationHelper.playOnPopupPlayer(activity, player.getPlayQueue(), true);
+ });
+ break;
+ case MINIMIZE_ON_EXIT_MODE_NONE: default:
+ player.pause();
+ break;
+ }
+ }
+ }
+ //endregion
+
+
+ /*//////////////////////////////////////////////////////////////////////////
+ // Playback states
+ //////////////////////////////////////////////////////////////////////////*/
+ //region Playback states
+
+ @Override
+ public void onUpdateProgress(final int currentProgress,
+ final int duration,
+ final int bufferPercent) {
+ super.onUpdateProgress(currentProgress, duration, bufferPercent);
+
+ if (areSegmentsVisible) {
+ segmentAdapter.selectSegmentAt(getNearestStreamSegmentPosition(currentProgress));
+ }
+ if (isQueueVisible) {
+ updateQueueTime(currentProgress);
+ }
+ }
+
+ @Override
+ public void onPlaying() {
+ super.onPlaying();
+ checkLandscape();
+ }
+
+ @Override
+ public void onCompleted() {
+ super.onCompleted();
+ if (isFullscreen) {
+ toggleFullscreen();
+ }
+ }
+ //endregion
+
+
+ /*//////////////////////////////////////////////////////////////////////////
+ // Controls showing / hiding
+ //////////////////////////////////////////////////////////////////////////*/
+ //region Controls showing / hiding
+
+ @Override
+ protected void showOrHideButtons() {
+ super.showOrHideButtons();
+ @Nullable final PlayQueue playQueue = player.getPlayQueue();
+ if (playQueue == null) {
+ return;
+ }
+
+ final boolean showQueue = playQueue.getStreams().size() > 1;
+ final boolean showSegment = !player.getCurrentStreamInfo()
+ .map(StreamInfo::getStreamSegments)
+ .map(List::isEmpty)
+ .orElse(/*no stream info=*/true);
+
+ binding.queueButton.setVisibility(showQueue ? View.VISIBLE : View.GONE);
+ binding.queueButton.setAlpha(showQueue ? 1.0f : 0.0f);
+ binding.segmentsButton.setVisibility(showSegment ? View.VISIBLE : View.GONE);
+ binding.segmentsButton.setAlpha(showSegment ? 1.0f : 0.0f);
+ }
+
+ @Override
+ public void showSystemUIPartially() {
+ if (isFullscreen) {
+ getParentActivity().map(Activity::getWindow).ifPresent(window -> {
+ window.setStatusBarColor(Color.TRANSPARENT);
+ window.setNavigationBarColor(Color.TRANSPARENT);
+ final int visibility = View.SYSTEM_UI_FLAG_LAYOUT_STABLE
+ | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
+ | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION;
+ window.getDecorView().setSystemUiVisibility(visibility);
+ window.clearFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN);
+ });
+ }
+ }
+
+ @Override
+ public void hideSystemUIIfNeeded() {
+ player.getFragmentListener().ifPresent(PlayerServiceEventListener::hideSystemUiIfNeeded);
+ }
+
+ /**
+ * Calculate the maximum allowed height for the {@link R.id.endScreen}
+ * to prevent it from enlarging the player.
+ *
+ * The calculating follows these rules:
+ *
+ *
+ * Show at least stream title and content creator on TVs and tablets when in landscape
+ * (always the case for TVs) and not in fullscreen mode. This requires to have at least
+ * {@link #DETAIL_ROOT_MINIMUM_HEIGHT} free space for {@link R.id.detail_root} and
+ * additional space for the stream title text size ({@link R.id.detail_title_root_layout}).
+ * The text size is {@link #DETAIL_TITLE_TEXT_SIZE_TABLET} on tablets and
+ * {@link #DETAIL_TITLE_TEXT_SIZE_TV} on TVs, see {@link R.id.titleTextView}.
+ *
+ *
+ * Otherwise, the max thumbnail height is the screen height.
+ *
+ *
+ *
+ * @param bitmap the bitmap that needs to be resized to fit the end screen
+ * @return the maximum height for the end screen thumbnail
+ */
+ @Override
+ protected float calculateMaxEndScreenThumbnailHeight(@NonNull final Bitmap bitmap) {
+ final int screenHeight = context.getResources().getDisplayMetrics().heightPixels;
+
+ if (DeviceUtils.isTv(context) && !isFullscreen()) {
+ final int videoInfoHeight = DeviceUtils.dpToPx(DETAIL_ROOT_MINIMUM_HEIGHT, context)
+ + DeviceUtils.spToPx(DETAIL_TITLE_TEXT_SIZE_TV, context);
+ return Math.min(bitmap.getHeight(), screenHeight - videoInfoHeight);
+ } else if (DeviceUtils.isTablet(context) && isLandscape() && !isFullscreen()) {
+ final int videoInfoHeight = DeviceUtils.dpToPx(DETAIL_ROOT_MINIMUM_HEIGHT, context)
+ + DeviceUtils.spToPx(DETAIL_TITLE_TEXT_SIZE_TABLET, context);
+ return Math.min(bitmap.getHeight(), screenHeight - videoInfoHeight);
+ } else { // fullscreen player: max height is the device height
+ return Math.min(bitmap.getHeight(), screenHeight);
+ }
+ }
+
+ private void showHideKodiButton() {
+ // show kodi button if it supports the current service and it is enabled in settings
+ @Nullable final PlayQueue playQueue = player.getPlayQueue();
+ binding.playWithKodi.setVisibility(playQueue != null && playQueue.getItem() != null
+ && KoreUtils.shouldShowPlayWithKodi(context, playQueue.getItem().getServiceId())
+ ? View.VISIBLE : View.GONE);
+ }
+ //endregion
+
+
+ /*//////////////////////////////////////////////////////////////////////////
+ // Captions (text tracks)
+ //////////////////////////////////////////////////////////////////////////*/
+ //region Captions (text tracks)
+
+ @Override
+ protected void setupSubtitleView(final float captionScale) {
+ final DisplayMetrics metrics = context.getResources().getDisplayMetrics();
+ final int minimumLength = Math.min(metrics.heightPixels, metrics.widthPixels);
+ final float captionRatioInverse = 20f + 4f * (1.0f - captionScale);
+ binding.subtitleView.setFixedTextSize(
+ TypedValue.COMPLEX_UNIT_PX, minimumLength / captionRatioInverse);
+ }
+ //endregion
+
+
+ /*//////////////////////////////////////////////////////////////////////////
+ // Gestures
+ //////////////////////////////////////////////////////////////////////////*/
+ //region Gestures
+
+ @SuppressWarnings("checkstyle:ParameterNumber")
+ @Override
+ public void onLayoutChange(final View view, final int l, final int t, final int r, final int b,
+ final int ol, final int ot, final int or, final int ob) {
+ if (l != ol || t != ot || r != or || b != ob) {
+ // Use a smaller value to be consistent across screen orientations, and to make usage
+ // easier. Multiply by 3/4 to ensure the user does not need to move the finger up to the
+ // screen border, in order to reach the maximum volume/brightness.
+ final int width = r - l;
+ final int height = b - t;
+ final int min = Math.min(width, height);
+ final int maxGestureLength = (int) (min * 0.75);
+
+ if (DEBUG) {
+ Log.d(TAG, "maxGestureLength = " + maxGestureLength);
+ }
+
+ binding.volumeProgressBar.setMax(maxGestureLength);
+ binding.brightnessProgressBar.setMax(maxGestureLength);
+
+ setInitialGestureValues();
+ binding.itemsListPanel.getLayoutParams().height =
+ height - binding.itemsListPanel.getTop();
+ }
+ }
+
+ private void setInitialGestureValues() {
+ if (player.getAudioReactor() != null) {
+ final float currentVolumeNormalized = (float) player.getAudioReactor().getVolume()
+ / player.getAudioReactor().getMaxVolume();
+ binding.volumeProgressBar.setProgress(
+ (int) (binding.volumeProgressBar.getMax() * currentVolumeNormalized));
+ }
+ }
+ //endregion
+
+
+ /*//////////////////////////////////////////////////////////////////////////
+ // Play queue, segments and streams
+ //////////////////////////////////////////////////////////////////////////*/
+ //region Play queue, segments and streams
+
+ @Override
+ public void onMetadataChanged(@NonNull final StreamInfo info) {
+ super.onMetadataChanged(info);
+ showHideKodiButton();
+ if (areSegmentsVisible) {
+ if (segmentAdapter.setItems(info)) {
+ final int adapterPosition = getNearestStreamSegmentPosition(
+ player.getExoPlayer().getCurrentPosition());
+ segmentAdapter.selectSegmentAt(adapterPosition);
+ binding.itemsList.scrollToPosition(adapterPosition);
+ } else {
+ closeItemsList();
+ }
+ }
+ }
+
+ @Override
+ public void onPlayQueueEdited() {
+ super.onPlayQueueEdited();
+ showOrHideButtons();
+ }
+
+ private void onQueueClicked() {
+ isQueueVisible = true;
+
+ hideSystemUIIfNeeded();
+ buildQueue();
+
+ binding.itemsListHeaderTitle.setVisibility(View.GONE);
+ binding.itemsListHeaderDuration.setVisibility(View.VISIBLE);
+ binding.shuffleButton.setVisibility(View.VISIBLE);
+ binding.repeatButton.setVisibility(View.VISIBLE);
+ binding.addToPlaylistButton.setVisibility(View.VISIBLE);
+
+ hideControls(0, 0);
+ binding.itemsListPanel.requestFocus();
+ animate(binding.itemsListPanel, true, DEFAULT_CONTROLS_DURATION,
+ AnimationType.SLIDE_AND_ALPHA);
+
+ @Nullable final PlayQueue playQueue = player.getPlayQueue();
+ if (playQueue != null) {
+ binding.itemsList.scrollToPosition(playQueue.getIndex());
+ }
+
+ updateQueueTime((int) player.getExoPlayer().getCurrentPosition());
+ }
+
+ private void buildQueue() {
+ binding.itemsList.setAdapter(playQueueAdapter);
+ binding.itemsList.setClickable(true);
+ binding.itemsList.setLongClickable(true);
+
+ binding.itemsList.clearOnScrollListeners();
+ binding.itemsList.addOnScrollListener(getQueueScrollListener());
+
+ itemTouchHelper = new ItemTouchHelper(getItemTouchCallback());
+ itemTouchHelper.attachToRecyclerView(binding.itemsList);
+
+ playQueueAdapter.setSelectedListener(getOnSelectedListener());
+
+ binding.itemsListClose.setOnClickListener(view -> closeItemsList());
+ }
+
+ private void onSegmentsClicked() {
+ areSegmentsVisible = true;
+
+ hideSystemUIIfNeeded();
+ buildSegments();
+
+ binding.itemsListHeaderTitle.setVisibility(View.VISIBLE);
+ binding.itemsListHeaderDuration.setVisibility(View.GONE);
+ binding.shuffleButton.setVisibility(View.GONE);
+ binding.repeatButton.setVisibility(View.GONE);
+ binding.addToPlaylistButton.setVisibility(View.GONE);
+
+ hideControls(0, 0);
+ binding.itemsListPanel.requestFocus();
+ animate(binding.itemsListPanel, true, DEFAULT_CONTROLS_DURATION,
+ AnimationType.SLIDE_AND_ALPHA);
+
+ final int adapterPosition = getNearestStreamSegmentPosition(
+ player.getExoPlayer().getCurrentPosition());
+ segmentAdapter.selectSegmentAt(adapterPosition);
+ binding.itemsList.scrollToPosition(adapterPosition);
+ }
+
+ private void buildSegments() {
+ binding.itemsList.setAdapter(segmentAdapter);
+ binding.itemsList.setClickable(true);
+ binding.itemsList.setLongClickable(false);
+
+ binding.itemsList.clearOnScrollListeners();
+ if (itemTouchHelper != null) {
+ itemTouchHelper.attachToRecyclerView(null);
+ }
+
+ player.getCurrentStreamInfo().ifPresent(segmentAdapter::setItems);
+
+ binding.shuffleButton.setVisibility(View.GONE);
+ binding.repeatButton.setVisibility(View.GONE);
+ binding.addToPlaylistButton.setVisibility(View.GONE);
+ binding.itemsListClose.setOnClickListener(view -> closeItemsList());
+ }
+
+ public void closeItemsList() {
+ if (isQueueVisible || areSegmentsVisible) {
+ isQueueVisible = false;
+ areSegmentsVisible = false;
+
+ if (itemTouchHelper != null) {
+ itemTouchHelper.attachToRecyclerView(null);
+ }
+
+ animate(binding.itemsListPanel, false, DEFAULT_CONTROLS_DURATION,
+ AnimationType.SLIDE_AND_ALPHA, 0, () ->
+ // Even when queueLayout is GONE it receives touch events
+ // and ruins normal behavior of the app. This line fixes it
+ binding.itemsListPanel.setTranslationY(
+ -binding.itemsListPanel.getHeight() * 5.0f));
+
+ // clear focus, otherwise a white rectangle remains on top of the player
+ binding.itemsListClose.clearFocus();
+ binding.playPauseButton.requestFocus();
+ }
+ }
+
+ private OnScrollBelowItemsListener getQueueScrollListener() {
+ return new OnScrollBelowItemsListener() {
+ @Override
+ public void onScrolledDown(final RecyclerView recyclerView) {
+ @Nullable final PlayQueue playQueue = player.getPlayQueue();
+ if (playQueue != null && !playQueue.isComplete()) {
+ playQueue.fetch();
+ } else if (binding != null) {
+ binding.itemsList.clearOnScrollListeners();
+ }
+ }
+ };
+ }
+
+ private StreamSegmentAdapter.StreamSegmentListener getStreamSegmentListener() {
+ return (item, seconds) -> {
+ segmentAdapter.selectSegment(item);
+ player.seekTo(seconds * 1000L);
+ player.triggerProgressUpdate();
+ };
+ }
+
+ private int getNearestStreamSegmentPosition(final long playbackPosition) {
+ //noinspection SimplifyOptionalCallChains
+ if (!player.getCurrentStreamInfo().isPresent()) {
+ return 0;
+ }
+
+ int nearestPosition = 0;
+ final List segments = player.getCurrentStreamInfo()
+ .get()
+ .getStreamSegments();
+
+ for (int i = 0; i < segments.size(); i++) {
+ if (segments.get(i).getStartTimeSeconds() * 1000L > playbackPosition) {
+ break;
+ }
+ nearestPosition++;
+ }
+ return Math.max(0, nearestPosition - 1);
+ }
+
+ private ItemTouchHelper.SimpleCallback getItemTouchCallback() {
+ return new PlayQueueItemTouchCallback() {
+ @Override
+ public void onMove(final int sourceIndex, final int targetIndex) {
+ @Nullable final PlayQueue playQueue = player.getPlayQueue();
+ if (playQueue != null) {
+ playQueue.move(sourceIndex, targetIndex);
+ }
+ }
+
+ @Override
+ public void onSwiped(final int index) {
+ @Nullable final PlayQueue playQueue = player.getPlayQueue();
+ if (playQueue != null && index != -1) {
+ playQueue.remove(index);
+ }
+ }
+ };
+ }
+
+ private PlayQueueItemBuilder.OnSelectedListener getOnSelectedListener() {
+ return new PlayQueueItemBuilder.OnSelectedListener() {
+ @Override
+ public void selected(final PlayQueueItem item, final View view) {
+ player.selectQueueItem(item);
+ }
+
+ @Override
+ public void held(final PlayQueueItem item, final View view) {
+ @Nullable final PlayQueue playQueue = player.getPlayQueue();
+ @Nullable final AppCompatActivity parentActivity = getParentActivity().orElse(null);
+ if (playQueue != null && parentActivity != null && playQueue.indexOf(item) != -1) {
+ openPopupMenu(player.getPlayQueue(), item, view, true,
+ parentActivity.getSupportFragmentManager(), context);
+ }
+ }
+
+ @Override
+ public void onStartDrag(final PlayQueueItemHolder viewHolder) {
+ if (itemTouchHelper != null) {
+ itemTouchHelper.startDrag(viewHolder);
+ }
+ }
+ };
+ }
+
+ private void updateQueueTime(final int currentTime) {
+ @Nullable final PlayQueue playQueue = player.getPlayQueue();
+ if (playQueue == null) {
+ return;
+ }
+
+ final int currentStream = playQueue.getIndex();
+ int before = 0;
+ int after = 0;
+
+ final List streams = playQueue.getStreams();
+ final int nStreams = streams.size();
+
+ for (int i = 0; i < nStreams; i++) {
+ if (i < currentStream) {
+ before += streams.get(i).getDuration();
+ } else {
+ after += streams.get(i).getDuration();
+ }
+ }
+
+ before *= 1000;
+ after *= 1000;
+
+ binding.itemsListHeaderDuration.setText(
+ String.format("%s/%s",
+ getTimeString(currentTime + before),
+ getTimeString(before + after)
+ ));
+ }
+
+ @Override
+ protected boolean isAnyListViewOpen() {
+ return isQueueVisible || areSegmentsVisible;
+ }
+
+ @Override
+ public boolean isFullscreen() {
+ return isFullscreen;
+ }
+
+ public boolean isVerticalVideo() {
+ return isVerticalVideo;
+ }
+
+ //endregion
+
+
+ /*//////////////////////////////////////////////////////////////////////////
+ // Click listeners
+ //////////////////////////////////////////////////////////////////////////*/
+ //region Click listeners
+
+ @Override
+ public void onClick(final View v) {
+ if (v.getId() == binding.screenRotationButton.getId()) {
+ // Only if it's not a vertical video or vertical video but in landscape with locked
+ // orientation a screen orientation can be changed automatically
+ if (!isVerticalVideo || (isLandscape() && globalScreenOrientationLocked(context))) {
+ player.getFragmentListener().ifPresent(
+ PlayerServiceEventListener::onScreenRotationButtonClicked);
+ } else {
+ toggleFullscreen();
+ }
+ }
+
+ // call it later since it calls manageControlsAfterOnClick at the end
+ super.onClick(v);
+ }
+
+ @Override
+ protected void onPlaybackSpeedClicked() {
+ final AppCompatActivity activity = getParentActivity().orElse(null);
+ if (activity == null) {
+ return;
+ }
+
+ PlaybackParameterDialog.newInstance(player.getPlaybackSpeed(), player.getPlaybackPitch(),
+ player.getPlaybackSkipSilence(), player::setPlaybackParameters)
+ .show(activity.getSupportFragmentManager(), null);
+ }
+
+ @Override
+ public boolean onLongClick(final View v) {
+ if (v.getId() == binding.moreOptionsButton.getId() && isFullscreen) {
+ player.getFragmentListener().ifPresent(
+ PlayerServiceEventListener::onMoreOptionsLongClicked);
+ hideControls(0, 0);
+ hideSystemUIIfNeeded();
+ return true;
+ }
+ return super.onLongClick(v);
+ }
+
+ @Override
+ public boolean onKeyDown(final int keyCode) {
+ if (keyCode == KeyEvent.KEYCODE_SPACE && isFullscreen) {
+ player.playPause();
+ if (player.isPlaying()) {
+ hideControls(0, 0);
+ }
+ return true;
+ }
+ return super.onKeyDown(keyCode);
+ }
+ //endregion
+
+
+ /*//////////////////////////////////////////////////////////////////////////
+ // Video size, orientation, fullscreen
+ //////////////////////////////////////////////////////////////////////////*/
+ //region Video size, orientation, fullscreen
+
+ private void setupScreenRotationButton() {
+ binding.screenRotationButton.setVisibility(globalScreenOrientationLocked(context)
+ || isVerticalVideo || DeviceUtils.isTablet(context)
+ ? View.VISIBLE : View.GONE);
+ binding.screenRotationButton.setImageDrawable(AppCompatResources.getDrawable(context,
+ isFullscreen ? R.drawable.ic_fullscreen_exit
+ : R.drawable.ic_fullscreen));
+ }
+
+ @Override
+ public void onVideoSizeChanged(@NonNull final VideoSize videoSize) {
+ super.onVideoSizeChanged(videoSize);
+ isVerticalVideo = videoSize.width < videoSize.height;
+
+ if (globalScreenOrientationLocked(context)
+ && isFullscreen
+ && isLandscape() == isVerticalVideo
+ && !DeviceUtils.isTv(context)
+ && !DeviceUtils.isTablet(context)) {
+ // set correct orientation
+ player.getFragmentListener().ifPresent(
+ PlayerServiceEventListener::onScreenRotationButtonClicked);
+ }
+
+ setupScreenRotationButton();
+ }
+
+ public void toggleFullscreen() {
+ if (DEBUG) {
+ Log.d(TAG, "toggleFullscreen() called");
+ }
+ final PlayerServiceEventListener fragmentListener = player.getFragmentListener()
+ .orElse(null);
+ if (fragmentListener == null || player.exoPlayerIsNull()) {
+ return;
+ }
+
+ isFullscreen = !isFullscreen;
+ if (isFullscreen) {
+ // Android needs tens milliseconds to send new insets but a user is able to see
+ // how controls changes it's position from `0` to `nav bar height` padding.
+ // So just hide the controls to hide this visual inconsistency
+ hideControls(0, 0);
+ } else {
+ // Apply window insets because Android will not do it when orientation changes
+ // from landscape to portrait (open vertical video to reproduce)
+ binding.playbackControlRoot.setPadding(0, 0, 0, 0);
+ }
+ fragmentListener.onFullscreenStateChanged(isFullscreen);
+
+ binding.titleTextView.setVisibility(isFullscreen ? View.VISIBLE : View.GONE);
+ binding.channelTextView.setVisibility(isFullscreen ? View.VISIBLE : View.GONE);
+ binding.playerCloseButton.setVisibility(isFullscreen ? View.GONE : View.VISIBLE);
+ setupScreenRotationButton();
+ }
+
+ public void checkLandscape() {
+ // check if landscape is correct
+ final boolean videoInLandscapeButNotInFullscreen = isLandscape()
+ && !isFullscreen
+ && !player.isAudioOnly();
+ final boolean notPaused = player.getCurrentState() != STATE_COMPLETED
+ && player.getCurrentState() != STATE_PAUSED;
+
+ if (videoInLandscapeButNotInFullscreen
+ && notPaused
+ && !DeviceUtils.isTablet(context)) {
+ toggleFullscreen();
+ }
+ }
+ //endregion
+
+
+ /*//////////////////////////////////////////////////////////////////////////
+ // Getters
+ //////////////////////////////////////////////////////////////////////////*/
+ //region Getters
+
+ public Optional getParentActivity() {
+ final ViewParent rootParent = binding.getRoot().getParent();
+ if (rootParent instanceof ViewGroup) {
+ final Context activity = ((ViewGroup) rootParent).getContext();
+ if (activity instanceof AppCompatActivity) {
+ return Optional.of((AppCompatActivity) activity);
+ }
+ }
+ return Optional.empty();
+ }
+
+ public boolean isLandscape() {
+ // DisplayMetrics from activity context knows about MultiWindow feature
+ // while DisplayMetrics from app context doesn't
+ return DeviceUtils.isLandscape(
+ getParentActivity().map(Context.class::cast).orElse(player.getService()));
+ }
+ //endregion
+}
diff --git a/app/src/main/java/org/schabi/newpipe/player/ui/PlayerUi.java b/app/src/main/java/org/schabi/newpipe/player/ui/PlayerUi.java
new file mode 100644
index 00000000000..57e2ec2a2cf
--- /dev/null
+++ b/app/src/main/java/org/schabi/newpipe/player/ui/PlayerUi.java
@@ -0,0 +1,212 @@
+package org.schabi.newpipe.player.ui;
+
+import android.content.Context;
+import android.content.Intent;
+import android.graphics.Bitmap;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import com.google.android.exoplayer2.PlaybackParameters;
+import com.google.android.exoplayer2.Player.RepeatMode;
+import com.google.android.exoplayer2.Tracks;
+import com.google.android.exoplayer2.text.Cue;
+import com.google.android.exoplayer2.video.VideoSize;
+
+import org.schabi.newpipe.extractor.stream.StreamInfo;
+import org.schabi.newpipe.player.Player;
+
+import java.util.List;
+
+/**
+ * A player UI is a component that can seamlessly connect and disconnect from the {@link Player} and
+ * provide a user interface of some sort. Try to extend this class instead of adding more code to
+ * {@link Player}!
+ */
+public abstract class PlayerUi {
+
+ @NonNull protected final Context context;
+ @NonNull protected final Player player;
+
+ /**
+ * @param player the player instance that will be usable throughout the lifetime of this UI; its
+ * context should already have been initialized
+ */
+ protected PlayerUi(@NonNull final Player player) {
+ this.context = player.getContext();
+ this.player = player;
+ }
+
+ /**
+ * @return the player instance this UI was constructed with
+ */
+ @NonNull
+ public Player getPlayer() {
+ return player;
+ }
+
+
+ /**
+ * Called after the player received an intent and processed it.
+ */
+ public void setupAfterIntent() {
+ }
+
+ /**
+ * Called right after the exoplayer instance is constructed, or right after this UI is
+ * constructed if the exoplayer is already available then. Note that the exoplayer instance
+ * could be built and destroyed multiple times during the lifetime of the player, so this method
+ * might be called multiple times.
+ */
+ public void initPlayer() {
+ }
+
+ /**
+ * Called when playback in the exoplayer is about to start, or right after this UI is
+ * constructed if the exoplayer and the play queue are already available then. The play queue
+ * will therefore always be not null.
+ */
+ public void initPlayback() {
+ }
+
+ /**
+ * Called when the exoplayer instance is about to be destroyed. Note that the exoplayer instance
+ * could be built and destroyed multiple times during the lifetime of the player, so this method
+ * might be called multiple times. Be sure to unset any video surface view or play queue
+ * listeners! This will also be called when this UI is being discarded, just before {@link
+ * #destroy()}.
+ */
+ public void destroyPlayer() {
+ }
+
+ /**
+ * Called when this UI is being discarded, either because the player is switching to a different
+ * UI or because the player is shutting down completely.
+ */
+ public void destroy() {
+ }
+
+ /**
+ * Called when the player is smooth-stopping, that is, transitioning smoothly to a new play
+ * queue after the user tapped on a new video stream while a stream was playing in the video
+ * detail fragment.
+ */
+ public void smoothStopForImmediateReusing() {
+ }
+
+ /**
+ * Called when the video detail fragment listener is connected with the player, or right after
+ * this UI is constructed if the listener is already connected then.
+ */
+ public void onFragmentListenerSet() {
+ }
+
+ /**
+ * Broadcasts that the player receives will also be notified to UIs here. If you want to
+ * register new broadcast actions to receive here, add them to {@link
+ * Player#setupBroadcastReceiver()}.
+ * @param intent the broadcast intent received by the player
+ */
+ public void onBroadcastReceived(final Intent intent) {
+ }
+
+ /**
+ * Called when stream progress (i.e. the current time in the seekbar) or stream duration change.
+ * Will surely be called every {@link Player#PROGRESS_LOOP_INTERVAL_MILLIS} while a stream is
+ * playing.
+ * @param currentProgress the current progress in milliseconds
+ * @param duration the duration of the stream being played
+ * @param bufferPercent the percentage of stream already buffered, see {@link
+ * com.google.android.exoplayer2.BasePlayer#getBufferedPercentage()}
+ */
+ public void onUpdateProgress(final int currentProgress,
+ final int duration,
+ final int bufferPercent) {
+ }
+
+ public void onPrepared() {
+ }
+
+ public void onBlocked() {
+ }
+
+ public void onPlaying() {
+ }
+
+ public void onBuffering() {
+ }
+
+ public void onPaused() {
+ }
+
+ public void onPausedSeek() {
+ }
+
+ public void onCompleted() {
+ }
+
+ public void onRepeatModeChanged(@RepeatMode final int repeatMode) {
+ }
+
+ public void onShuffleModeEnabledChanged(final boolean shuffleModeEnabled) {
+ }
+
+ public void onMuteUnmuteChanged(final boolean isMuted) {
+ }
+
+ /**
+ * @see com.google.android.exoplayer2.Player.Listener#onTracksChanged(Tracks)
+ * @param currentTracks the available tracks information
+ */
+ public void onTextTracksChanged(@NonNull final Tracks currentTracks) {
+ }
+
+ /**
+ * @see com.google.android.exoplayer2.Player.Listener#onPlaybackParametersChanged
+ * @param playbackParameters the new playback parameters
+ */
+ public void onPlaybackParametersChanged(@NonNull final PlaybackParameters playbackParameters) {
+ }
+
+ /**
+ * @see com.google.android.exoplayer2.Player.Listener#onRenderedFirstFrame
+ */
+ public void onRenderedFirstFrame() {
+ }
+
+ /**
+ * @see com.google.android.exoplayer2.text.TextOutput#onCues
+ * @param cues the cues to pass to the subtitle view
+ */
+ public void onCues(@NonNull final List cues) {
+ }
+
+ /**
+ * Called when the stream being played changes.
+ * @param info the {@link StreamInfo} metadata object, along with data about the selected and
+ * available video streams (to be used to build the resolution menus, for example)
+ */
+ public void onMetadataChanged(@NonNull final StreamInfo info) {
+ }
+
+ /**
+ * Called when the thumbnail for the current metadata was loaded.
+ * @param bitmap the thumbnail to process, or null if there is no thumbnail or there was an
+ * error when loading the thumbnail
+ */
+ public void onThumbnailLoaded(@Nullable final Bitmap bitmap) {
+ }
+
+ /**
+ * Called when the play queue was edited: a stream was appended, moved or removed.
+ */
+ public void onPlayQueueEdited() {
+ }
+
+ /**
+ * @param videoSize the new video size, useful to set the surface aspect ratio
+ * @see com.google.android.exoplayer2.Player.Listener#onVideoSizeChanged
+ */
+ public void onVideoSizeChanged(@NonNull final VideoSize videoSize) {
+ }
+}
diff --git a/app/src/main/java/org/schabi/newpipe/player/ui/PlayerUiList.java b/app/src/main/java/org/schabi/newpipe/player/ui/PlayerUiList.java
new file mode 100644
index 00000000000..24fec3b8afc
--- /dev/null
+++ b/app/src/main/java/org/schabi/newpipe/player/ui/PlayerUiList.java
@@ -0,0 +1,90 @@
+package org.schabi.newpipe.player.ui;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Optional;
+import java.util.function.Consumer;
+
+public final class PlayerUiList {
+ final List playerUis = new ArrayList<>();
+
+ /**
+ * Creates a {@link PlayerUiList} starting with the provided player uis. The provided player uis
+ * will not be prepared like those passed to {@link #addAndPrepare(PlayerUi)}, because when
+ * the {@link PlayerUiList} constructor is called, the player is still not running and it
+ * wouldn't make sense to initialize uis then. Instead the player will initialize them by doing
+ * proper calls to {@link #call(Consumer)}.
+ *
+ * @param initialPlayerUis the player uis this list should start with; the order will be kept
+ */
+ public PlayerUiList(final PlayerUi... initialPlayerUis) {
+ playerUis.addAll(List.of(initialPlayerUis));
+ }
+
+ /**
+ * Adds the provided player ui to the list and calls on it the initialization functions that
+ * apply based on the current player state. The preparation step needs to be done since when UIs
+ * are removed and re-added, the player will not call e.g. initPlayer again since the exoplayer
+ * is already initialized, but we need to notify the newly built UI that the player is ready
+ * nonetheless.
+ * @param playerUi the player ui to prepare and add to the list; its {@link
+ * PlayerUi#getPlayer()} will be used to query information about the player
+ * state
+ */
+ public void addAndPrepare(final PlayerUi playerUi) {
+ if (playerUi.getPlayer().getFragmentListener().isPresent()) {
+ // make sure UIs know whether a service is connected or not
+ playerUi.onFragmentListenerSet();
+ }
+
+ if (!playerUi.getPlayer().exoPlayerIsNull()) {
+ playerUi.initPlayer();
+ if (playerUi.getPlayer().getPlayQueue() != null) {
+ playerUi.initPlayback();
+ }
+ }
+
+ playerUis.add(playerUi);
+ }
+
+ /**
+ * Destroys all matching player UIs and removes them from the list.
+ * @param playerUiType the class of the player UI to destroy; the {@link
+ * Class#isInstance(Object)} method will be used, so even subclasses will be
+ * destroyed and removed
+ * @param the class type parameter
+ */
+ public void destroyAll(final Class playerUiType) {
+ playerUis.stream()
+ .filter(playerUiType::isInstance)
+ .forEach(playerUi -> {
+ playerUi.destroyPlayer();
+ playerUi.destroy();
+ });
+ playerUis.removeIf(playerUiType::isInstance);
+ }
+
+ /**
+ * @param playerUiType the class of the player UI to return; the {@link
+ * Class#isInstance(Object)} method will be used, so even subclasses could
+ * be returned
+ * @param the class type parameter
+ * @return the first player UI of the required type found in the list, or an empty {@link
+ * Optional} otherwise
+ */
+ public Optional get(final Class playerUiType) {
+ return playerUis.stream()
+ .filter(playerUiType::isInstance)
+ .map(playerUiType::cast)
+ .findFirst();
+ }
+
+ /**
+ * Calls the provided consumer on all player UIs in the list, in order of addition.
+ * @param consumer the consumer to call with player UIs
+ */
+ public void call(final Consumer consumer) {
+ //noinspection SimplifyStreamApiCallChains
+ playerUis.stream().forEachOrdered(consumer);
+ }
+}
diff --git a/app/src/main/java/org/schabi/newpipe/player/ui/PopupPlayerUi.java b/app/src/main/java/org/schabi/newpipe/player/ui/PopupPlayerUi.java
new file mode 100644
index 00000000000..aa36a6a5a97
--- /dev/null
+++ b/app/src/main/java/org/schabi/newpipe/player/ui/PopupPlayerUi.java
@@ -0,0 +1,592 @@
+package org.schabi.newpipe.player.ui;
+
+import static android.view.ViewGroup.LayoutParams.WRAP_CONTENT;
+import static org.schabi.newpipe.MainActivity.DEBUG;
+import static org.schabi.newpipe.player.helper.PlayerHelper.getMinimumVideoHeight;
+
+import android.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
+import android.annotation.SuppressLint;
+import android.content.Context;
+import android.content.Intent;
+import android.content.SharedPreferences;
+import android.content.res.Resources;
+import android.graphics.Bitmap;
+import android.graphics.PixelFormat;
+import android.os.Build;
+import android.util.DisplayMetrics;
+import android.util.Log;
+import android.view.Gravity;
+import android.view.LayoutInflater;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.WindowInsets;
+import android.view.WindowManager;
+import android.view.animation.AnticipateInterpolator;
+import android.widget.LinearLayout;
+
+import androidx.annotation.NonNull;
+import androidx.core.content.ContextCompat;
+import androidx.core.math.MathUtils;
+
+import com.google.android.exoplayer2.ui.AspectRatioFrameLayout;
+import com.google.android.exoplayer2.ui.SubtitleView;
+
+import org.schabi.newpipe.R;
+import org.schabi.newpipe.databinding.PlayerBinding;
+import org.schabi.newpipe.databinding.PlayerPopupCloseOverlayBinding;
+import org.schabi.newpipe.player.Player;
+import org.schabi.newpipe.player.gesture.BasePlayerGestureListener;
+import org.schabi.newpipe.player.gesture.PopupPlayerGestureListener;
+import org.schabi.newpipe.player.helper.PlayerHelper;
+
+public final class PopupPlayerUi extends VideoPlayerUi {
+ private static final String TAG = PopupPlayerUi.class.getSimpleName();
+
+ /**
+ * Maximum opacity allowed for Android 12 and higher to allow touches on other apps when using
+ * NewPipe's popup player.
+ *
+ *
+ * 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.
+ *
+ *
+ * @see WindowManager.LayoutParams#FLAG_NOT_TOUCHABLE
+ */
+ private static final float MAXIMUM_OPACITY_ALLOWED_FOR_S_AND_HIGHER = 0.8f;
+
+ /*//////////////////////////////////////////////////////////////////////////
+ // Popup player
+ //////////////////////////////////////////////////////////////////////////*/
+
+ private PlayerPopupCloseOverlayBinding closeOverlayBinding;
+
+ private boolean isPopupClosing = false;
+
+ private int screenWidth;
+ private int screenHeight;
+
+ /*//////////////////////////////////////////////////////////////////////////
+ // Popup player window manager
+ //////////////////////////////////////////////////////////////////////////*/
+
+ public static final int IDLE_WINDOW_FLAGS = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
+ | WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM;
+ public static final int ONGOING_PLAYBACK_WINDOW_FLAGS = IDLE_WINDOW_FLAGS
+ | WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON;
+
+ private WindowManager.LayoutParams popupLayoutParams; // null if player is not popup
+ private final WindowManager windowManager;
+
+
+ /*//////////////////////////////////////////////////////////////////////////
+ // Constructor, setup, destroy
+ //////////////////////////////////////////////////////////////////////////*/
+ //region Constructor, setup, destroy
+
+ public PopupPlayerUi(@NonNull final Player player,
+ @NonNull final PlayerBinding playerBinding) {
+ super(player, playerBinding);
+ windowManager = ContextCompat.getSystemService(context, WindowManager.class);
+ }
+
+ @Override
+ public void setupAfterIntent() {
+ super.setupAfterIntent();
+ initPopup();
+ initPopupCloseOverlay();
+ }
+
+ @Override
+ BasePlayerGestureListener buildGestureListener() {
+ return new PopupPlayerGestureListener(this);
+ }
+
+ @SuppressLint("RtlHardcoded")
+ private void initPopup() {
+ if (DEBUG) {
+ Log.d(TAG, "initPopup() called");
+ }
+
+ // Popup is already added to windowManager
+ if (popupHasParent()) {
+ return;
+ }
+
+ updateScreenSize();
+
+ popupLayoutParams = retrievePopupLayoutParamsFromPrefs();
+ binding.surfaceView.setHeights(popupLayoutParams.height, popupLayoutParams.height);
+
+ checkPopupPositionBounds();
+
+ binding.loadingPanel.setMinimumWidth(popupLayoutParams.width);
+ binding.loadingPanel.setMinimumHeight(popupLayoutParams.height);
+
+ windowManager.addView(binding.getRoot(), popupLayoutParams);
+ setupVideoSurfaceIfNeeded(); // now there is a parent, we can setup video surface
+
+ // Popup doesn't have aspectRatio selector, using FIT automatically
+ setResizeMode(AspectRatioFrameLayout.RESIZE_MODE_FIT);
+ }
+
+ @SuppressLint("RtlHardcoded")
+ private void initPopupCloseOverlay() {
+ if (DEBUG) {
+ Log.d(TAG, "initPopupCloseOverlay() called");
+ }
+
+ // closeOverlayView is already added to windowManager
+ if (closeOverlayBinding != null) {
+ return;
+ }
+
+ closeOverlayBinding = PlayerPopupCloseOverlayBinding.inflate(LayoutInflater.from(context));
+
+ final WindowManager.LayoutParams closeOverlayLayoutParams = buildCloseOverlayLayoutParams();
+ closeOverlayBinding.closeButton.setVisibility(View.GONE);
+ windowManager.addView(closeOverlayBinding.getRoot(), closeOverlayLayoutParams);
+ }
+
+ @Override
+ protected void setupElementsVisibility() {
+ binding.fullScreenButton.setVisibility(View.VISIBLE);
+ binding.screenRotationButton.setVisibility(View.GONE);
+ binding.resizeTextView.setVisibility(View.GONE);
+ binding.getRoot().findViewById(R.id.metadataView).setVisibility(View.GONE);
+ binding.queueButton.setVisibility(View.GONE);
+ binding.segmentsButton.setVisibility(View.GONE);
+ binding.moreOptionsButton.setVisibility(View.GONE);
+ binding.topControls.setOrientation(LinearLayout.HORIZONTAL);
+ binding.primaryControls.getLayoutParams().width = WRAP_CONTENT;
+ binding.secondaryControls.setAlpha(1.0f);
+ binding.secondaryControls.setVisibility(View.VISIBLE);
+ binding.secondaryControls.setTranslationY(0);
+ binding.share.setVisibility(View.GONE);
+ binding.playWithKodi.setVisibility(View.GONE);
+ binding.openInBrowser.setVisibility(View.GONE);
+ binding.switchMute.setVisibility(View.GONE);
+ binding.playerCloseButton.setVisibility(View.GONE);
+ binding.topControls.bringToFront();
+ binding.topControls.setClickable(false);
+ binding.topControls.setFocusable(false);
+ binding.bottomControls.bringToFront();
+ super.setupElementsVisibility();
+ }
+
+ @Override
+ protected void setupElementsSize(final Resources resources) {
+ setupElementsSize(
+ 0,
+ 0,
+ resources.getDimensionPixelSize(R.dimen.player_popup_controls_padding),
+ resources.getDimensionPixelSize(R.dimen.player_popup_buttons_padding)
+ );
+ }
+
+ @Override
+ public void removeViewFromParent() {
+ // view was added by windowManager for popup player
+ windowManager.removeViewImmediate(binding.getRoot());
+ }
+
+ @Override
+ public void destroy() {
+ super.destroy();
+ removePopupFromView();
+ }
+ //endregion
+
+
+ /*//////////////////////////////////////////////////////////////////////////
+ // Broadcast receiver
+ //////////////////////////////////////////////////////////////////////////*/
+ //region Broadcast receiver
+
+ @Override
+ public void onBroadcastReceived(final Intent intent) {
+ super.onBroadcastReceived(intent);
+ if (Intent.ACTION_CONFIGURATION_CHANGED.equals(intent.getAction())) {
+ updateScreenSize();
+ changePopupSize(popupLayoutParams.width);
+ checkPopupPositionBounds();
+ } else if (player.isPlaying() || player.isLoading()) {
+ if (Intent.ACTION_SCREEN_OFF.equals(intent.getAction())) {
+ // Use only audio source when screen turns off while popup player is playing
+ player.useVideoSource(false);
+ } else if (Intent.ACTION_SCREEN_ON.equals(intent.getAction())) {
+ // Restore video source when screen turns on and user was watching video in popup
+ player.useVideoSource(true);
+ }
+ }
+ }
+ //endregion
+
+
+ /*//////////////////////////////////////////////////////////////////////////
+ // Popup position and size
+ //////////////////////////////////////////////////////////////////////////*/
+ //region Popup position and size
+
+ /**
+ * Check if {@link #popupLayoutParams}' position is within a arbitrary boundary
+ * that goes from (0, 0) to (screenWidth, screenHeight).
+ *
+ * If it's out of these boundaries, {@link #popupLayoutParams}' position is changed
+ * and {@code true} is returned to represent this change.
+ *
+ */
+ public void checkPopupPositionBounds() {
+ if (DEBUG) {
+ Log.d(TAG, "checkPopupPositionBounds() called with: "
+ + "screenWidth = [" + screenWidth + "], "
+ + "screenHeight = [" + screenHeight + "]");
+ }
+ if (popupLayoutParams == null) {
+ return;
+ }
+
+ popupLayoutParams.x = MathUtils.clamp(popupLayoutParams.x, 0, screenWidth
+ - popupLayoutParams.width);
+ popupLayoutParams.y = MathUtils.clamp(popupLayoutParams.y, 0, screenHeight
+ - popupLayoutParams.height);
+ }
+
+ public void updateScreenSize() {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
+ final var windowMetrics = windowManager.getCurrentWindowMetrics();
+ final var bounds = windowMetrics.getBounds();
+ final var windowInsets = windowMetrics.getWindowInsets();
+ final var insets = windowInsets.getInsetsIgnoringVisibility(
+ WindowInsets.Type.navigationBars() | WindowInsets.Type.displayCutout());
+ screenWidth = bounds.width() - (insets.left + insets.right);
+ screenHeight = bounds.height() - (insets.top + insets.bottom);
+ } else {
+ final DisplayMetrics metrics = new DisplayMetrics();
+ windowManager.getDefaultDisplay().getMetrics(metrics);
+ screenWidth = metrics.widthPixels;
+ screenHeight = metrics.heightPixels;
+ }
+ if (DEBUG) {
+ Log.d(TAG, "updateScreenSize() called: screenWidth = ["
+ + screenWidth + "], screenHeight = [" + screenHeight + "]");
+ }
+ }
+
+ /**
+ * Changes the size of the popup based on the width.
+ * @param width the new width, height is calculated with
+ * {@link PlayerHelper#getMinimumVideoHeight(float)}
+ */
+ public void changePopupSize(final int width) {
+ if (DEBUG) {
+ Log.d(TAG, "changePopupSize() called with: width = [" + width + "]");
+ }
+
+ if (anyPopupViewIsNull()) {
+ return;
+ }
+
+ final float minimumWidth = context.getResources().getDimension(R.dimen.popup_minimum_width);
+ final int actualWidth = Math.min((int) Math.max(width, minimumWidth), screenWidth);
+ final int actualHeight = (int) getMinimumVideoHeight(width);
+ if (DEBUG) {
+ Log.d(TAG, "updatePopupSize() updated values:"
+ + " width = [" + actualWidth + "], height = [" + actualHeight + "]");
+ }
+
+ popupLayoutParams.width = actualWidth;
+ popupLayoutParams.height = actualHeight;
+ binding.surfaceView.setHeights(popupLayoutParams.height, popupLayoutParams.height);
+ windowManager.updateViewLayout(binding.getRoot(), popupLayoutParams);
+ }
+
+ @Override
+ protected float calculateMaxEndScreenThumbnailHeight(@NonNull final Bitmap bitmap) {
+ // no need for the end screen thumbnail to be resized on popup player: it's only needed
+ // for the main player so that it is enlarged correctly inside the fragment
+ return bitmap.getHeight();
+ }
+ //endregion
+
+
+ /*//////////////////////////////////////////////////////////////////////////
+ // Popup closing
+ //////////////////////////////////////////////////////////////////////////*/
+ //region Popup closing
+
+ public void closePopup() {
+ if (DEBUG) {
+ Log.d(TAG, "closePopup() called, isPopupClosing = " + isPopupClosing);
+ }
+ if (isPopupClosing) {
+ return;
+ }
+ isPopupClosing = true;
+
+ player.saveStreamProgressState();
+ windowManager.removeView(binding.getRoot());
+
+ animatePopupOverlayAndFinishService();
+ }
+
+ public boolean isPopupClosing() {
+ return isPopupClosing;
+ }
+
+ public void removePopupFromView() {
+ // wrap in try-catch since it could sometimes generate errors randomly
+ try {
+ if (popupHasParent()) {
+ windowManager.removeView(binding.getRoot());
+ }
+ } catch (final IllegalArgumentException e) {
+ Log.w(TAG, "Failed to remove popup from window manager", e);
+ }
+
+ try {
+ final boolean closeOverlayHasParent = closeOverlayBinding != null
+ && closeOverlayBinding.getRoot().getParent() != null;
+ if (closeOverlayHasParent) {
+ windowManager.removeView(closeOverlayBinding.getRoot());
+ }
+ } catch (final IllegalArgumentException e) {
+ Log.w(TAG, "Failed to remove popup overlay from window manager", e);
+ }
+ }
+
+ private void animatePopupOverlayAndFinishService() {
+ final int targetTranslationY =
+ (int) (closeOverlayBinding.closeButton.getRootView().getHeight()
+ - closeOverlayBinding.closeButton.getY());
+
+ closeOverlayBinding.closeButton.animate().setListener(null).cancel();
+ closeOverlayBinding.closeButton.animate()
+ .setInterpolator(new AnticipateInterpolator())
+ .translationY(targetTranslationY)
+ .setDuration(400)
+ .setListener(new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationCancel(final Animator animation) {
+ end();
+ }
+
+ @Override
+ public void onAnimationEnd(final Animator animation) {
+ end();
+ }
+
+ private void end() {
+ windowManager.removeView(closeOverlayBinding.getRoot());
+ closeOverlayBinding = null;
+ player.getService().stopService();
+ }
+ }).start();
+ }
+ //endregion
+
+ /*//////////////////////////////////////////////////////////////////////////
+ // Playback states
+ //////////////////////////////////////////////////////////////////////////*/
+ //region Playback states
+
+ private void changePopupWindowFlags(final int flags) {
+ if (DEBUG) {
+ Log.d(TAG, "changePopupWindowFlags() called with: flags = [" + flags + "]");
+ }
+
+ if (!anyPopupViewIsNull()) {
+ popupLayoutParams.flags = flags;
+ windowManager.updateViewLayout(binding.getRoot(), popupLayoutParams);
+ }
+ }
+
+ @Override
+ public void onPlaying() {
+ super.onPlaying();
+ changePopupWindowFlags(ONGOING_PLAYBACK_WINDOW_FLAGS);
+ }
+
+ @Override
+ public void onPaused() {
+ super.onPaused();
+ changePopupWindowFlags(IDLE_WINDOW_FLAGS);
+ }
+
+ @Override
+ public void onCompleted() {
+ super.onCompleted();
+ changePopupWindowFlags(IDLE_WINDOW_FLAGS);
+ }
+
+ @Override
+ protected void setupSubtitleView(final float captionScale) {
+ final float captionRatio = (captionScale - 1.0f) / 5.0f + 1.0f;
+ binding.subtitleView.setFractionalTextSize(
+ SubtitleView.DEFAULT_TEXT_SIZE_FRACTION * captionRatio);
+ }
+
+ @Override
+ protected void onPlaybackSpeedClicked() {
+ playbackSpeedPopupMenu.show();
+ isSomePopupMenuVisible = true;
+ }
+ //endregion
+
+
+ /*//////////////////////////////////////////////////////////////////////////
+ // Gestures
+ //////////////////////////////////////////////////////////////////////////*/
+ //region Gestures
+
+ private int distanceFromCloseButton(@NonNull final MotionEvent popupMotionEvent) {
+ final int closeOverlayButtonX = closeOverlayBinding.closeButton.getLeft()
+ + closeOverlayBinding.closeButton.getWidth() / 2;
+ final int closeOverlayButtonY = closeOverlayBinding.closeButton.getTop()
+ + closeOverlayBinding.closeButton.getHeight() / 2;
+
+ final float fingerX = popupLayoutParams.x + popupMotionEvent.getX();
+ final float fingerY = popupLayoutParams.y + popupMotionEvent.getY();
+
+ return (int) Math.sqrt(Math.pow(closeOverlayButtonX - fingerX, 2)
+ + Math.pow(closeOverlayButtonY - fingerY, 2));
+ }
+
+ private float getClosingRadius() {
+ final int buttonRadius = closeOverlayBinding.closeButton.getWidth() / 2;
+ // 20% wider than the button itself
+ return buttonRadius * 1.2f;
+ }
+
+ public boolean isInsideClosingRadius(@NonNull final MotionEvent popupMotionEvent) {
+ return distanceFromCloseButton(popupMotionEvent) <= getClosingRadius();
+ }
+ //endregion
+
+
+ /*//////////////////////////////////////////////////////////////////////////
+ // Popup & closing overlay layout params + saving popup position and size
+ //////////////////////////////////////////////////////////////////////////*/
+ //region Popup & closing overlay layout params + saving popup position and size
+
+ /**
+ * {@code screenWidth} and {@code screenHeight} must have been initialized.
+ * @return the popup starting layout params
+ */
+ @SuppressLint("RtlHardcoded")
+ public WindowManager.LayoutParams retrievePopupLayoutParamsFromPrefs() {
+ final SharedPreferences prefs = getPlayer().getPrefs();
+ final Context context = getPlayer().getContext();
+
+ final boolean popupRememberSizeAndPos = prefs.getBoolean(
+ context.getString(R.string.popup_remember_size_pos_key), true);
+ final float defaultSize = context.getResources().getDimension(R.dimen.popup_default_width);
+ final float popupWidth = popupRememberSizeAndPos
+ ? prefs.getFloat(context.getString(R.string.popup_saved_width_key), defaultSize)
+ : defaultSize;
+ final float popupHeight = getMinimumVideoHeight(popupWidth);
+
+ final WindowManager.LayoutParams params = new WindowManager.LayoutParams(
+ (int) popupWidth, (int) popupHeight,
+ popupLayoutParamType(),
+ IDLE_WINDOW_FLAGS,
+ PixelFormat.TRANSLUCENT);
+ params.gravity = Gravity.LEFT | Gravity.TOP;
+ params.softInputMode = WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE;
+
+ final int centerX = (int) (screenWidth / 2f - popupWidth / 2f);
+ final int centerY = (int) (screenHeight / 2f - popupHeight / 2f);
+ params.x = popupRememberSizeAndPos
+ ? prefs.getInt(context.getString(R.string.popup_saved_x_key), centerX) : centerX;
+ params.y = popupRememberSizeAndPos
+ ? prefs.getInt(context.getString(R.string.popup_saved_y_key), centerY) : centerY;
+
+ return params;
+ }
+
+ public void savePopupPositionAndSizeToPrefs() {
+ if (getPopupLayoutParams() != null) {
+ final Context context = getPlayer().getContext();
+ getPlayer().getPrefs().edit()
+ .putFloat(context.getString(R.string.popup_saved_width_key),
+ popupLayoutParams.width)
+ .putInt(context.getString(R.string.popup_saved_x_key),
+ popupLayoutParams.x)
+ .putInt(context.getString(R.string.popup_saved_y_key),
+ popupLayoutParams.y)
+ .apply();
+ }
+ }
+
+ @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;
+ }
+ //endregion
+
+
+ /*//////////////////////////////////////////////////////////////////////////
+ // Getters
+ //////////////////////////////////////////////////////////////////////////*/
+ //region Getters
+
+ private boolean popupHasParent() {
+ return binding != null
+ && binding.getRoot().getLayoutParams() instanceof WindowManager.LayoutParams
+ && binding.getRoot().getParent() != null;
+ }
+
+ private boolean anyPopupViewIsNull() {
+ return popupLayoutParams == null || windowManager == null
+ || binding.getRoot().getParent() == null;
+ }
+
+ public PlayerPopupCloseOverlayBinding getCloseOverlayBinding() {
+ return closeOverlayBinding;
+ }
+
+ public WindowManager.LayoutParams getPopupLayoutParams() {
+ return popupLayoutParams;
+ }
+
+ public WindowManager getWindowManager() {
+ return windowManager;
+ }
+
+ public int getScreenHeight() {
+ return screenHeight;
+ }
+
+ public int getScreenWidth() {
+ return screenWidth;
+ }
+ //endregion
+}
diff --git a/app/src/main/java/org/schabi/newpipe/player/ui/VideoPlayerUi.java b/app/src/main/java/org/schabi/newpipe/player/ui/VideoPlayerUi.java
new file mode 100644
index 00000000000..1709755f246
--- /dev/null
+++ b/app/src/main/java/org/schabi/newpipe/player/ui/VideoPlayerUi.java
@@ -0,0 +1,1578 @@
+package org.schabi.newpipe.player.ui;
+
+import static com.google.android.exoplayer2.Player.REPEAT_MODE_ALL;
+import static com.google.android.exoplayer2.Player.REPEAT_MODE_ONE;
+import static org.schabi.newpipe.MainActivity.DEBUG;
+import static org.schabi.newpipe.ktx.ViewUtils.animate;
+import static org.schabi.newpipe.ktx.ViewUtils.animateRotation;
+import static org.schabi.newpipe.player.Player.RENDERER_UNAVAILABLE;
+import static org.schabi.newpipe.player.Player.STATE_BUFFERING;
+import static org.schabi.newpipe.player.Player.STATE_COMPLETED;
+import static org.schabi.newpipe.player.Player.STATE_PAUSED;
+import static org.schabi.newpipe.player.Player.STATE_PAUSED_SEEK;
+import static org.schabi.newpipe.player.Player.STATE_PLAYING;
+import static org.schabi.newpipe.player.helper.PlayerHelper.formatSpeed;
+import static org.schabi.newpipe.player.helper.PlayerHelper.getTimeString;
+import static org.schabi.newpipe.player.helper.PlayerHelper.nextResizeModeAndSaveToPrefs;
+import static org.schabi.newpipe.player.helper.PlayerHelper.retrieveSeekDurationFromPreferences;
+
+import android.content.Intent;
+import android.content.res.Resources;
+import android.graphics.Bitmap;
+import android.graphics.Color;
+import android.graphics.PorterDuff;
+import android.graphics.PorterDuffColorFilter;
+import android.net.Uri;
+import android.os.Build;
+import android.os.Handler;
+import android.os.Looper;
+import android.util.Log;
+import android.view.GestureDetector;
+import android.view.Gravity;
+import android.view.KeyEvent;
+import android.view.Menu;
+import android.view.MenuItem;
+import android.view.View;
+import android.widget.LinearLayout;
+import android.widget.RelativeLayout;
+import android.widget.SeekBar;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.appcompat.content.res.AppCompatResources;
+import androidx.appcompat.view.ContextThemeWrapper;
+import androidx.appcompat.widget.PopupMenu;
+import androidx.core.graphics.Insets;
+import androidx.core.math.MathUtils;
+import androidx.core.view.ViewCompat;
+import androidx.core.view.WindowInsetsCompat;
+
+import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.ExoPlayer;
+import com.google.android.exoplayer2.Format;
+import com.google.android.exoplayer2.PlaybackParameters;
+import com.google.android.exoplayer2.Player.RepeatMode;
+import com.google.android.exoplayer2.Tracks;
+import com.google.android.exoplayer2.text.Cue;
+import com.google.android.exoplayer2.ui.AspectRatioFrameLayout;
+import com.google.android.exoplayer2.ui.CaptionStyleCompat;
+import com.google.android.exoplayer2.video.VideoSize;
+
+import org.schabi.newpipe.App;
+import org.schabi.newpipe.R;
+import org.schabi.newpipe.databinding.PlayerBinding;
+import org.schabi.newpipe.extractor.MediaFormat;
+import org.schabi.newpipe.extractor.stream.StreamInfo;
+import org.schabi.newpipe.extractor.stream.VideoStream;
+import org.schabi.newpipe.fragments.detail.VideoDetailFragment;
+import org.schabi.newpipe.ktx.AnimationType;
+import org.schabi.newpipe.player.Player;
+import org.schabi.newpipe.player.gesture.BasePlayerGestureListener;
+import org.schabi.newpipe.player.gesture.DisplayPortion;
+import org.schabi.newpipe.player.helper.PlayerHelper;
+import org.schabi.newpipe.player.mediaitem.MediaItemTag;
+import org.schabi.newpipe.player.playback.SurfaceHolderCallback;
+import org.schabi.newpipe.player.playqueue.PlayQueue;
+import org.schabi.newpipe.player.playqueue.PlayQueueItem;
+import org.schabi.newpipe.player.seekbarpreview.SeekbarPreviewThumbnailHelper;
+import org.schabi.newpipe.player.seekbarpreview.SeekbarPreviewThumbnailHolder;
+import org.schabi.newpipe.util.DeviceUtils;
+import org.schabi.newpipe.util.NavigationHelper;
+import org.schabi.newpipe.util.external_communication.KoreUtils;
+import org.schabi.newpipe.util.external_communication.ShareUtils;
+import org.schabi.newpipe.views.player.PlayerFastSeekOverlay;
+
+import java.util.List;
+import java.util.Optional;
+import java.util.stream.Collectors;
+
+public abstract class VideoPlayerUi extends PlayerUi
+ implements SeekBar.OnSeekBarChangeListener, View.OnClickListener, View.OnLongClickListener,
+ PopupMenu.OnMenuItemClickListener, PopupMenu.OnDismissListener {
+ private static final String TAG = VideoPlayerUi.class.getSimpleName();
+
+ // time constants
+ public static final long DEFAULT_CONTROLS_DURATION = 300; // 300 millis
+ public static final long DEFAULT_CONTROLS_HIDE_TIME = 2000; // 2 Seconds
+ public static final long DPAD_CONTROLS_HIDE_TIME = 7000; // 7 Seconds
+ public static final int SEEK_OVERLAY_DURATION = 450; // 450 millis
+
+ // other constants (TODO remove playback speeds and use normal menu for popup, too)
+ private static final float[] PLAYBACK_SPEEDS = {0.5f, 0.75f, 1.0f, 1.25f, 1.5f, 1.75f, 2.0f};
+
+
+ /*//////////////////////////////////////////////////////////////////////////
+ // Views
+ //////////////////////////////////////////////////////////////////////////*/
+
+ protected PlayerBinding binding;
+ private final Handler controlsVisibilityHandler = new Handler(Looper.getMainLooper());
+ @Nullable private SurfaceHolderCallback surfaceHolderCallback;
+ boolean surfaceIsSetup = false;
+
+
+ /*//////////////////////////////////////////////////////////////////////////
+ // Popup menus ("popup" means that they pop up, not that they belong to the popup player)
+ //////////////////////////////////////////////////////////////////////////*/
+
+ private static final int POPUP_MENU_ID_QUALITY = 69;
+ private static final int POPUP_MENU_ID_PLAYBACK_SPEED = 79;
+ private static final int POPUP_MENU_ID_CAPTION = 89;
+
+ protected boolean isSomePopupMenuVisible = false;
+ private PopupMenu qualityPopupMenu;
+ protected PopupMenu playbackSpeedPopupMenu;
+ private PopupMenu captionPopupMenu;
+
+
+ /*//////////////////////////////////////////////////////////////////////////
+ // Gestures
+ //////////////////////////////////////////////////////////////////////////*/
+
+ private GestureDetector gestureDetector;
+ private BasePlayerGestureListener playerGestureListener;
+ @Nullable private View.OnLayoutChangeListener onLayoutChangeListener = null;
+
+ @NonNull private final SeekbarPreviewThumbnailHolder seekbarPreviewThumbnailHolder =
+ new SeekbarPreviewThumbnailHolder();
+
+
+ /*//////////////////////////////////////////////////////////////////////////
+ // Constructor, setup, destroy
+ //////////////////////////////////////////////////////////////////////////*/
+ //region Constructor, setup, destroy
+
+ protected VideoPlayerUi(@NonNull final Player player,
+ @NonNull final PlayerBinding playerBinding) {
+ super(player);
+ binding = playerBinding;
+ setupFromView();
+ }
+
+ public void setupFromView() {
+ initViews();
+ initListeners();
+ setupPlayerSeekOverlay();
+ }
+
+ private void initViews() {
+ setupSubtitleView();
+
+ binding.resizeTextView
+ .setText(PlayerHelper.resizeTypeOf(context, binding.surfaceView.getResizeMode()));
+
+ binding.playbackSeekBar.getThumb()
+ .setColorFilter(new PorterDuffColorFilter(Color.RED, PorterDuff.Mode.SRC_IN));
+ binding.playbackSeekBar.getProgressDrawable()
+ .setColorFilter(new PorterDuffColorFilter(Color.RED, PorterDuff.Mode.MULTIPLY));
+
+ final ContextThemeWrapper themeWrapper = new ContextThemeWrapper(context,
+ R.style.DarkPopupMenu);
+
+ qualityPopupMenu = new PopupMenu(themeWrapper, binding.qualityTextView);
+ playbackSpeedPopupMenu = new PopupMenu(context, binding.playbackSpeed);
+ captionPopupMenu = new PopupMenu(themeWrapper, binding.captionTextView);
+
+ binding.progressBarLoadingPanel.getIndeterminateDrawable()
+ .setColorFilter(new PorterDuffColorFilter(Color.WHITE, PorterDuff.Mode.MULTIPLY));
+
+ binding.titleTextView.setSelected(true);
+ binding.channelTextView.setSelected(true);
+
+ // Prevent hiding of bottom sheet via swipe inside queue
+ binding.itemsList.setNestedScrollingEnabled(false);
+ }
+
+ abstract BasePlayerGestureListener buildGestureListener();
+
+ protected void initListeners() {
+ binding.qualityTextView.setOnClickListener(this);
+ binding.playbackSpeed.setOnClickListener(this);
+
+ binding.playbackSeekBar.setOnSeekBarChangeListener(this);
+ binding.captionTextView.setOnClickListener(this);
+ binding.resizeTextView.setOnClickListener(this);
+ binding.playbackLiveSync.setOnClickListener(this);
+
+ playerGestureListener = buildGestureListener();
+ gestureDetector = new GestureDetector(context, playerGestureListener);
+ binding.getRoot().setOnTouchListener(playerGestureListener);
+
+ binding.repeatButton.setOnClickListener(v -> onRepeatClicked());
+ binding.shuffleButton.setOnClickListener(v -> onShuffleClicked());
+
+ binding.playPauseButton.setOnClickListener(this);
+ binding.playPreviousButton.setOnClickListener(this);
+ binding.playNextButton.setOnClickListener(this);
+
+ binding.moreOptionsButton.setOnClickListener(this);
+ binding.moreOptionsButton.setOnLongClickListener(this);
+ binding.share.setOnClickListener(this);
+ binding.share.setOnLongClickListener(this);
+ binding.fullScreenButton.setOnClickListener(this);
+ binding.screenRotationButton.setOnClickListener(this);
+ binding.playWithKodi.setOnClickListener(this);
+ binding.openInBrowser.setOnClickListener(this);
+ binding.playerCloseButton.setOnClickListener(this);
+ binding.switchMute.setOnClickListener(this);
+
+ ViewCompat.setOnApplyWindowInsetsListener(binding.itemsListPanel, (view, windowInsets) -> {
+ final Insets cutout = windowInsets.getInsets(WindowInsetsCompat.Type.displayCutout());
+ if (!cutout.equals(Insets.NONE)) {
+ view.setPadding(cutout.left, cutout.top, cutout.right, cutout.bottom);
+ }
+ return windowInsets;
+ });
+
+ // PlaybackControlRoot already consumed window insets but we should pass them to
+ // player_overlays and fast_seek_overlay too. Without it they will be off-centered.
+ onLayoutChangeListener =
+ (v, left, top, right, bottom, oldLeft, oldTop, oldRight, oldBottom) -> {
+ binding.playerOverlays.setPadding(
+ v.getPaddingLeft(),
+ v.getPaddingTop(),
+ v.getPaddingRight(),
+ v.getPaddingBottom());
+
+ // If we added padding to the fast seek overlay, too, it would not go under the
+ // system ui. Instead we apply negative margins equal to the window insets of
+ // the opposite side, so that the view covers all of the player (overflowing on
+ // some sides) and its center coincides with the center of other controls.
+ final RelativeLayout.LayoutParams fastSeekParams = (RelativeLayout.LayoutParams)
+ binding.fastSeekOverlay.getLayoutParams();
+ fastSeekParams.leftMargin = -v.getPaddingRight();
+ fastSeekParams.topMargin = -v.getPaddingBottom();
+ fastSeekParams.rightMargin = -v.getPaddingLeft();
+ fastSeekParams.bottomMargin = -v.getPaddingTop();
+ };
+ binding.playbackControlRoot.addOnLayoutChangeListener(onLayoutChangeListener);
+ }
+
+ protected void deinitListeners() {
+ binding.qualityTextView.setOnClickListener(null);
+ binding.playbackSpeed.setOnClickListener(null);
+ binding.playbackSeekBar.setOnSeekBarChangeListener(null);
+ binding.captionTextView.setOnClickListener(null);
+ binding.resizeTextView.setOnClickListener(null);
+ binding.playbackLiveSync.setOnClickListener(null);
+
+ binding.getRoot().setOnTouchListener(null);
+ playerGestureListener = null;
+ gestureDetector = null;
+
+ binding.repeatButton.setOnClickListener(null);
+ binding.shuffleButton.setOnClickListener(null);
+
+ binding.playPauseButton.setOnClickListener(null);
+ binding.playPreviousButton.setOnClickListener(null);
+ binding.playNextButton.setOnClickListener(null);
+
+ binding.moreOptionsButton.setOnClickListener(null);
+ binding.moreOptionsButton.setOnLongClickListener(null);
+ binding.share.setOnClickListener(null);
+ binding.share.setOnLongClickListener(null);
+ binding.fullScreenButton.setOnClickListener(null);
+ binding.screenRotationButton.setOnClickListener(null);
+ binding.playWithKodi.setOnClickListener(null);
+ binding.openInBrowser.setOnClickListener(null);
+ binding.playerCloseButton.setOnClickListener(null);
+ binding.switchMute.setOnClickListener(null);
+
+ ViewCompat.setOnApplyWindowInsetsListener(binding.itemsListPanel, null);
+
+ binding.playbackControlRoot.removeOnLayoutChangeListener(onLayoutChangeListener);
+ }
+
+ /**
+ * Initializes the Fast-For/Backward overlay.
+ */
+ private void setupPlayerSeekOverlay() {
+ binding.fastSeekOverlay
+ .seekSecondsSupplier(() -> retrieveSeekDurationFromPreferences(player) / 1000)
+ .performListener(new PlayerFastSeekOverlay.PerformListener() {
+
+ @Override
+ public void onDoubleTap() {
+ animate(binding.fastSeekOverlay, true, SEEK_OVERLAY_DURATION);
+ }
+
+ @Override
+ public void onDoubleTapEnd() {
+ animate(binding.fastSeekOverlay, false, SEEK_OVERLAY_DURATION);
+ }
+
+ @NonNull
+ @Override
+ public FastSeekDirection getFastSeekDirection(
+ @NonNull final DisplayPortion portion
+ ) {
+ if (player.exoPlayerIsNull()) {
+ // Abort seeking
+ playerGestureListener.endMultiDoubleTap();
+ return FastSeekDirection.NONE;
+ }
+ if (portion == DisplayPortion.LEFT) {
+ // Check if it's possible to rewind
+ // Small puffer to eliminate infinite rewind seeking
+ if (player.getExoPlayer().getCurrentPosition() < 500L) {
+ return FastSeekDirection.NONE;
+ }
+ return FastSeekDirection.BACKWARD;
+ } else if (portion == DisplayPortion.RIGHT) {
+ // Check if it's possible to fast-forward
+ if (player.getCurrentState() == STATE_COMPLETED
+ || player.getExoPlayer().getCurrentPosition()
+ >= player.getExoPlayer().getDuration()) {
+ return FastSeekDirection.NONE;
+ }
+ return FastSeekDirection.FORWARD;
+ }
+ /* portion == DisplayPortion.MIDDLE */
+ return FastSeekDirection.NONE;
+ }
+
+ @Override
+ public void seek(final boolean forward) {
+ playerGestureListener.keepInDoubleTapMode();
+ if (forward) {
+ player.fastForward();
+ } else {
+ player.fastRewind();
+ }
+ }
+ });
+ playerGestureListener.doubleTapControls(binding.fastSeekOverlay);
+ }
+
+ public void deinitPlayerSeekOverlay() {
+ binding.fastSeekOverlay
+ .seekSecondsSupplier(null)
+ .performListener(null);
+ }
+
+ @Override
+ public void setupAfterIntent() {
+ super.setupAfterIntent();
+ setupElementsVisibility();
+ setupElementsSize(context.getResources());
+ binding.getRoot().setVisibility(View.VISIBLE);
+ binding.playPauseButton.requestFocus();
+ }
+
+ @Override
+ public void initPlayer() {
+ super.initPlayer();
+ setupVideoSurfaceIfNeeded();
+ }
+
+ @Override
+ public void initPlayback() {
+ super.initPlayback();
+
+ // #6825 - Ensure that the shuffle-button is in the correct state on the UI
+ setShuffleButton(player.getExoPlayer().getShuffleModeEnabled());
+ }
+
+ public abstract void removeViewFromParent();
+
+ @Override
+ public void destroyPlayer() {
+ super.destroyPlayer();
+ clearVideoSurface();
+ }
+
+ @Override
+ public void destroy() {
+ super.destroy();
+ binding.endScreen.setImageDrawable(null);
+ deinitPlayerSeekOverlay();
+ deinitListeners();
+ }
+
+ protected void setupElementsVisibility() {
+ setMuteButton(player.isMuted());
+ animateRotation(binding.moreOptionsButton, DEFAULT_CONTROLS_DURATION, 0);
+ }
+
+ protected abstract void setupElementsSize(Resources resources);
+
+ protected void setupElementsSize(final int buttonsMinWidth,
+ final int playerTopPad,
+ final int controlsPad,
+ final int buttonsPad) {
+ binding.topControls.setPaddingRelative(controlsPad, playerTopPad, controlsPad, 0);
+ binding.bottomControls.setPaddingRelative(controlsPad, 0, controlsPad, 0);
+ binding.qualityTextView.setPadding(buttonsPad, buttonsPad, buttonsPad, buttonsPad);
+ binding.playbackSpeed.setPadding(buttonsPad, buttonsPad, buttonsPad, buttonsPad);
+ binding.playbackSpeed.setMinimumWidth(buttonsMinWidth);
+ binding.captionTextView.setPadding(buttonsPad, buttonsPad, buttonsPad, buttonsPad);
+ }
+ //endregion
+
+
+ /*//////////////////////////////////////////////////////////////////////////
+ // Broadcast receiver
+ //////////////////////////////////////////////////////////////////////////*/
+ //region Broadcast receiver
+
+ @Override
+ public void onBroadcastReceived(final Intent intent) {
+ super.onBroadcastReceived(intent);
+ if (Intent.ACTION_CONFIGURATION_CHANGED.equals(intent.getAction())) {
+ // When the orientation changes, the screen height might be smaller. If the end screen
+ // thumbnail is not re-scaled, it can be larger than the current screen height and thus
+ // enlarging the whole player. This causes the seekbar to be out of the visible area.
+ updateEndScreenThumbnail(player.getThumbnail());
+ }
+ }
+ //endregion
+
+
+ /*//////////////////////////////////////////////////////////////////////////
+ // Thumbnail
+ //////////////////////////////////////////////////////////////////////////*/
+ //region Thumbnail
+
+ /**
+ * Scale the player audio / end screen thumbnail down if necessary.
+ *
+ * This is necessary when the thumbnail's height is larger than the device's height
+ * and thus is enlarging the player's height
+ * causing the bottom playback controls to be out of the visible screen.
+ *
make links in comments clickable, increase text size
+
seek on clicking timestamp links in comments
+
show preferred tab based on recently selected state
+
add playlist to queue when long clicking on 'Background' in playlist window
+
search for shared text when it is not an URL
+
add "share at current time" button to the main video player
+-
+-
+-
+-
+-
+-
+-
+-
+-
+-
+-
+-
+-
+-
+-
diff --git a/fastlane/metadata/android/cs/changelogs/750.txt b/fastlane/metadata/android/cs/changelogs/750.txt
new file mode 100644
index 00000000000..325be0c281e
--- /dev/null
+++ b/fastlane/metadata/android/cs/changelogs/750.txt
@@ -0,0 +1,14 @@
+New
+Playback resume #2288
+• Resume streams where you stopped last time
+Downloader Enhancements #2149
+• Use Storage Access Framework to store downloads on external SD-cards
+• New mp4 muxer
+• Optionally change the download directory before starting a download
+• Respect metered networks
+
+
+Improved
+• Removed gema strings #2295
+• Handle (auto)rotation changes during activity lifecycle #2444
+• Make long-press menus consistent #2368
diff --git a/fastlane/metadata/android/cs/changelogs/760.txt b/fastlane/metadata/android/cs/changelogs/760.txt
new file mode 100644
index 00000000000..d449ec53a37
--- /dev/null
+++ b/fastlane/metadata/android/cs/changelogs/760.txt
@@ -0,0 +1,14 @@
+Změny ve verzi 0.17.1
+
+Nové stránky
+- Thajská lokalizace
+
+
+Vylepšené stránky
+- Znovu přidána akce "začít přehrávat zde" v nabídkách pro dlouhé stisknutí pro seznamy skladeb #2518
+- Přidání přepínače pro výběr souborů SAF / legacy #2521
+
+Opraveno
+- Oprava mizení tlačítek v zobrazení stahování při přepínání aplikací #2487
+- Oprava pozice přehrávání se ukládá, i když je vypnutá historie sledování
+- Oprava sníženého výkonu způsobeného pozicí přehrávání v zobrazeních seznamu #2517
diff --git a/fastlane/metadata/android/cs/changelogs/770.txt b/fastlane/metadata/android/cs/changelogs/770.txt
new file mode 100644
index 00000000000..0a07f6bdd95
--- /dev/null
+++ b/fastlane/metadata/android/cs/changelogs/770.txt
@@ -0,0 +1,4 @@
+Změny ve verzi 0.17.2
+
+Oprava
+- Oprava nebylo k dispozici žádné video
diff --git a/fastlane/metadata/android/cs/changelogs/780.txt b/fastlane/metadata/android/cs/changelogs/780.txt
new file mode 100644
index 00000000000..32cc03d1f76
--- /dev/null
+++ b/fastlane/metadata/android/cs/changelogs/780.txt
@@ -0,0 +1,11 @@
+Změny ve verzi 0.17.3
+
+Vylepšené stránky
+- Přidána možnost vymazat stavy přehrávání #2550
+- Zobrazení skrytých adresářů ve výběru souborů #2591
+- Podpora otevírání adres URL z instancí `invidio.us` pomocí NewPipe #2488
+- Přidána podpora pro adresy URL `music.youtube.com` TeamNewPipe/NewPipeExtractor#194
+
+Opraveno
+- YouTube] Opraveno 'java.lang.IllegalArgumentException #192
+- YouTube] Opraveno nefunkční živé vysílání TeamNewPipe/NewPipeExtractor#195
diff --git a/fastlane/metadata/android/cs/changelogs/790.txt b/fastlane/metadata/android/cs/changelogs/790.txt
new file mode 100644
index 00000000000..04b5c576324
--- /dev/null
+++ b/fastlane/metadata/android/cs/changelogs/790.txt
@@ -0,0 +1,9 @@
+Vylepšené stránky
+- Přidání více nadpisů pro zlepšení přístupnosti pro nevidomé #2655
+- Udělejte jazyk nastavení složky pro stahování konzistentnější a méně nejednoznačný #2637
+
+Opraveno
+- Kontrola, zda je stažen poslední bajt v bloku #2646
+- Opraveno posouvání ve fragmentu detailu videa #2672
+- Odstranění dvojité animace vymazání vyhledávacího pole na jednu #2695
+- [SoundCloud] Oprava extrakce client_id #2745
diff --git a/fastlane/metadata/android/cs/changelogs/800.txt b/fastlane/metadata/android/cs/changelogs/800.txt
new file mode 100644
index 00000000000..0a22893f19c
--- /dev/null
+++ b/fastlane/metadata/android/cs/changelogs/800.txt
@@ -0,0 +1,10 @@
+Nový
+- Podpora PeerTube bez P2P (#2201) [Beta]:
+ ◦ Sledování a stahování videí z instancí PeerTube
+ ◦ Přidání instancí v nastavení pro přístup ke kompletnímu světu PeerTube
+ ◦ V systémech Android 4.4 a 7.1 mohou být při přístupu k některým instancím problémy s přenosem SSL, což může vést k chybě sítě.
+
+- Downloader (#2679):
+ ◦ Vypočítat předpokládaný čas stahování
+ ◦ Stáhnout opus (soubory webm) jako ogg
+ ◦ Obnovení vypršených odkazů ke stažení pro obnovení stahování po dlouhé pauze
diff --git a/fastlane/metadata/android/cs/changelogs/810.txt b/fastlane/metadata/android/cs/changelogs/810.txt
new file mode 100644
index 00000000000..c04a9cac9af
--- /dev/null
+++ b/fastlane/metadata/android/cs/changelogs/810.txt
@@ -0,0 +1,8 @@
+Nový
+- Zobrazení miniatury videa na zamykací obrazovce při přehrávání na pozadí
+
+Vylepšená stránka
+- Přidání místního seznamu skladeb do fronty při dlouhém stisknutí tlačítka na pozadí / vyskakovacího tlačítka
+- Umožnit posouvání karet hlavní stránky a jejich skrytí, pokud je k dispozici pouze jedna karta
+- Omezit počet aktualizací miniatur oznámení v přehrávači na pozadí
+- Přidání fiktivní miniatury pro prázdné místní seznamy skladeb
diff --git a/fastlane/metadata/android/cs/changelogs/820.txt b/fastlane/metadata/android/cs/changelogs/820.txt
new file mode 100644
index 00000000000..9dc52c6f517
--- /dev/null
+++ b/fastlane/metadata/android/cs/changelogs/820.txt
@@ -0,0 +1 @@
+Opraven regex názvu dešifrovací funkce, který znemožňuje použití služby YouTube.
diff --git a/fastlane/metadata/android/cs/changelogs/830.txt b/fastlane/metadata/android/cs/changelogs/830.txt
new file mode 100644
index 00000000000..1f666691264
--- /dev/null
+++ b/fastlane/metadata/android/cs/changelogs/830.txt
@@ -0,0 +1 @@
+Aktualizováno klient_id služby SoundCloud pro opravu problémů se službou SoundCloud.
diff --git a/fastlane/metadata/android/cs/changelogs/840.txt b/fastlane/metadata/android/cs/changelogs/840.txt
new file mode 100644
index 00000000000..73da3bcc5c8
--- /dev/null
+++ b/fastlane/metadata/android/cs/changelogs/840.txt
@@ -0,0 +1,8 @@
+Nový
+- Přidán volič jazyka pro změnu jazyka aplikace
+- Přidáno tlačítko odeslat do Kodi do skládací nabídky přehrávače
+- Přidána možnost kopírování komentářů při dlouhém stisknutí
+
+Vylepšena stránka
+- Oprava aktivity ReCaptcha a správné ukládání získaných souborů cookie
+- Odstraněna nabídka s tečkami ve prospěch šuplíku a skrytí tlačítka historie, pokud není v nastavení povolena historie sledování
diff --git a/fastlane/metadata/android/cs/changelogs/850.txt b/fastlane/metadata/android/cs/changelogs/850.txt
new file mode 100644
index 00000000000..86fa0fc0f66
--- /dev/null
+++ b/fastlane/metadata/android/cs/changelogs/850.txt
@@ -0,0 +1 @@
+V tomto vydání byla aktualizována verze webových stránek YouTube. Stará verze webových stránek bude v březnu ukončena, a proto je nutné provést aktualizaci NewPipe.
diff --git a/fastlane/metadata/android/cs/changelogs/860.txt b/fastlane/metadata/android/cs/changelogs/860.txt
new file mode 100644
index 00000000000..b1d6765f850
--- /dev/null
+++ b/fastlane/metadata/android/cs/changelogs/860.txt
@@ -0,0 +1,7 @@
+Vylepšené stránky
+- Uložení a obnovení, zda je výška tónu a tempo odpojeno, nebo ne
+- Podpora výřezu displeje v přehrávači
+- Kulaté zobrazení a počet účastníků
+- Optimalizováno pro YouTube tak, aby využívalo méně dat
+
+V této verzi bylo opraveno více než 15 chyb souvisejících s YouTube.
diff --git a/fastlane/metadata/android/cs/changelogs/870.txt b/fastlane/metadata/android/cs/changelogs/870.txt
new file mode 100644
index 00000000000..27cc41b5daa
--- /dev/null
+++ b/fastlane/metadata/android/cs/changelogs/870.txt
@@ -0,0 +1,2 @@
+Jedná se o opravnou verzi, která aktualizuje NewPipe tak, aby opět umožňovala používání služby SoundCloud bez větších potíží.
+V extraktoru se nyní používá rozhraní API SoundCloud v2 a byla vylepšena detekce neplatných ID klientů.
diff --git a/fastlane/metadata/android/cs/changelogs/900.txt b/fastlane/metadata/android/cs/changelogs/900.txt
new file mode 100644
index 00000000000..301ced05932
--- /dev/null
+++ b/fastlane/metadata/android/cs/changelogs/900.txt
@@ -0,0 +1,13 @@
+Nový
+- Skupiny předplatného a tříděné kanály
+- Tlačítko ztlumení zvuku v přehrávačích
+
+Vylepšené stránky
+- Povoleno otevírání odkazů na music.youtube.com a media.ccc.de v aplikaci NewPipe
+- Přemístění dvou nastavení ze vzhledu do obsahu
+- Skrytí možností vyhledávání po 5, 15 a 25 sekundách, pokud je povoleno nepřesné vyhledávání
+
+Opraveno
+- některá videa WebM nelze zobrazit
+- zálohování databáze v systému Android P
+- pád při sdílení staženého souboru
diff --git a/fastlane/metadata/android/cs/changelogs/910.txt b/fastlane/metadata/android/cs/changelogs/910.txt
new file mode 100644
index 00000000000..130b5dc8707
--- /dev/null
+++ b/fastlane/metadata/android/cs/changelogs/910.txt
@@ -0,0 +1 @@
+Opravena migrace databáze, která v některých vzácných případech znemožňovala spuštění aplikace NewPipe.
diff --git a/fastlane/metadata/android/cs/changelogs/920.txt b/fastlane/metadata/android/cs/changelogs/920.txt
new file mode 100644
index 00000000000..b5e3167ee3f
--- /dev/null
+++ b/fastlane/metadata/android/cs/changelogs/920.txt
@@ -0,0 +1,9 @@
+Vylepšeno
+
+- Přidáno datum nahrání a počet zobrazení na položkách mřížky streamu
+- Vylepšení rozvržení záhlaví zásuvky
+
+Opraveno
+
+- Opraveno tlačítko ztlumení zvuku způsobující pády na rozhraní API 19
+- Opraveno stahování dlouhých videí 1080p 60fps
diff --git a/fastlane/metadata/android/cs/changelogs/930.txt b/fastlane/metadata/android/cs/changelogs/930.txt
new file mode 100644
index 00000000000..e72a3a61c47
--- /dev/null
+++ b/fastlane/metadata/android/cs/changelogs/930.txt
@@ -0,0 +1,10 @@
+Nový
+- Vyhledávání na YouTube Music
+- Základní podpora Android TV
+
+Vylepšené stránky
+- Přidána možnost odstranit všechna sledovaná videa z místního seznamu skladeb
+- Zobrazení zprávy, když obsah ještě není podporován, místo pádu.
+- Vylepšena změna velikosti vyskakovacího přehrávače pomocí gest štípnutí
+- Enqueue streamy při dlouhém stisknutí tlačítek na pozadí a vyskakovacích tlačítek v kanálu
+- Vylepšené zpracování velikosti záhlaví zásuvky
diff --git a/fastlane/metadata/android/cs/changelogs/940.txt b/fastlane/metadata/android/cs/changelogs/940.txt
new file mode 100644
index 00000000000..7988003d8d2
--- /dev/null
+++ b/fastlane/metadata/android/cs/changelogs/940.txt
@@ -0,0 +1,9 @@
+Nový
+- Přidání podpory pro komentáře SoundCloud
+- Přidání nastavení omezeného režimu YouTube
+- Zobrazení podrobností o nadřazeném kanálu PeerTube
+
+Vylepšené stránky
+- Zobrazení tlačítka Kore pouze pro podporované služby
+- Blokování gest přehrávače, která začínají na panelu NavigationBar nebo StatusBar
+- Změna barvy pozadí tlačítek opakování a přihlášení k odběru na základě barvy služby
diff --git a/fastlane/metadata/android/cs/changelogs/950.txt b/fastlane/metadata/android/cs/changelogs/950.txt
new file mode 100644
index 00000000000..21e52a36556
--- /dev/null
+++ b/fastlane/metadata/android/cs/changelogs/950.txt
@@ -0,0 +1,4 @@
+Tato verze přináší tři drobné opravy:
+- Oprava přístupu k úložišti v systému Adroid 10+
+- Opraveno otevírání kiosků
+- Opraveno rozbor trvání dlouhých videí
diff --git a/fastlane/metadata/android/cs/changelogs/951.txt b/fastlane/metadata/android/cs/changelogs/951.txt
new file mode 100644
index 00000000000..e5e3b3a6475
--- /dev/null
+++ b/fastlane/metadata/android/cs/changelogs/951.txt
@@ -0,0 +1,6 @@
+Nový
+- Přidání vyhledávání pro výběr odběru v dialogovém okně skupiny kanálů
+- Přidání filtru do dialogového okna skupiny kanálů pro zobrazení pouze neseskupených odběrů
+- Přidání karty seznamu skladeb na hlavní stránku
+- Rychlé převíjení vpřed/vzad ve frontě přehrávačů na pozadí/vyskočení.
+- Zobrazení návrhu vyhledávání: mysleli jste a zobrazení výsledku pro
diff --git a/fastlane/metadata/android/cs/changelogs/953.txt b/fastlane/metadata/android/cs/changelogs/953.txt
new file mode 100644
index 00000000000..cb762686232
--- /dev/null
+++ b/fastlane/metadata/android/cs/changelogs/953.txt
@@ -0,0 +1 @@
+Oprava extrakce dešifrovací funkce YouTube.
diff --git a/fastlane/metadata/android/cs/changelogs/954.txt b/fastlane/metadata/android/cs/changelogs/954.txt
new file mode 100644
index 00000000000..e07b70aa2e9
--- /dev/null
+++ b/fastlane/metadata/android/cs/changelogs/954.txt
@@ -0,0 +1,6 @@
+- nový pracovní postup aplikace: přehrávání videí na stránce s detailem, přejetí prstem dolů pro minimalizaci přehrávače
+- Oznámení MediaStyle: přizpůsobitelné akce v oznámeních, zlepšení výkonu
+- základní změna velikosti při používání aplikace NewPipe jako aplikace pro stolní počítače
+
+- zobrazení dialogu s možnostmi otevření v případě přípitku nepodporované adresy URL
+- Zlepšení zkušeností s návrhy vyhledávání, pokud nelze načíst ty vzdálené
diff --git a/fastlane/metadata/android/cs/changelogs/955.txt b/fastlane/metadata/android/cs/changelogs/955.txt
new file mode 100644
index 00000000000..ed158e34b8e
--- /dev/null
+++ b/fastlane/metadata/android/cs/changelogs/955.txt
@@ -0,0 +1,3 @@
+[YouTube] Oprava vyhledávání pro některé uživatele
+[YouTube] Oprava náhodných výjimek při dešifrování
+[SoundCloud] Adresy URL, které končí lomítkem, jsou nyní zpracovávány správně
diff --git a/fastlane/metadata/android/cs/changelogs/956.txt b/fastlane/metadata/android/cs/changelogs/956.txt
new file mode 100644
index 00000000000..f3188257313
--- /dev/null
+++ b/fastlane/metadata/android/cs/changelogs/956.txt
@@ -0,0 +1 @@
+[YouTube] Opraveno selhání při načítání jakéhokoli videa
diff --git a/fastlane/metadata/android/cs/changelogs/957.txt b/fastlane/metadata/android/cs/changelogs/957.txt
new file mode 100644
index 00000000000..666c5109725
--- /dev/null
+++ b/fastlane/metadata/android/cs/changelogs/957.txt
@@ -0,0 +1,8 @@
+- Sjednocení specifických akcí enqueue do jedné
+- Gesto dvěma prsty pro zavření přehrávače
+- Povolení vymazání souborů cookie reCAPTCHA
+- Možnost nezabarvovat oznámení
+- Vylepšení způsobu otevírání detailů videa s cílem opravit nekonečné vyrovnávací paměti, chybné chování při sdílení do NewPipe a další nesrovnalosti
+- Zrychlení videí na YouTube a oprava videí s věkovým omezením
+- Oprava pádu při rychlém převíjení vpřed/vzad
+- Nepřeuspořádávat seznamy přetahováním miniatur
diff --git a/fastlane/metadata/android/cs/changelogs/958.txt b/fastlane/metadata/android/cs/changelogs/958.txt
new file mode 100644
index 00000000000..989f9ad6125
--- /dev/null
+++ b/fastlane/metadata/android/cs/changelogs/958.txt
@@ -0,0 +1,15 @@
+Nové a vylepšené:
+- Znovu přidána možnost skrýt miniaturu na zamykací obrazovce
+- Tažení pro obnovení kanálu
+- Vylepšený výkon při načítání místních seznamů
+
+Opraveno:
+- Opraven pád při spuštění aplikace NewPipe po jejím vyjmutí z paměti RAM
+- Opraven pád při spuštění, když není připojení k internetu
+- Opraveno: Respektování nastavení jasu a nastavení hlasitosti
+- YouTube] Opraveny dlouhé seznamy skladeb
+
+Ostatní:
+- Vyčištění kódu a několik interních vylepšení
+- Aktualizace závislostí
+-
diff --git a/fastlane/metadata/android/cs/changelogs/959.txt b/fastlane/metadata/android/cs/changelogs/959.txt
new file mode 100644
index 00000000000..18a25645b6d
--- /dev/null
+++ b/fastlane/metadata/android/cs/changelogs/959.txt
@@ -0,0 +1,3 @@
+Opravena nekonečná smyčka pádů po otevření hlášení chyb.
+Aktualizován seznam instancí PeerTube, které lze automaticky otevřít pomocí NewPipe.
+Aktualizovány překlady.
diff --git a/fastlane/metadata/android/cs/changelogs/960.txt b/fastlane/metadata/android/cs/changelogs/960.txt
new file mode 100644
index 00000000000..c25277f6428
--- /dev/null
+++ b/fastlane/metadata/android/cs/changelogs/960.txt
@@ -0,0 +1,4 @@
+- Vylepšený popis možnosti exportu databáze v nastavení.
+- Opraveno zpracování komentářů na YouTube.
+- Opraveno zobrazení názvu služby media.ccc.de.
+- Aktualizovány překlady.
diff --git a/fastlane/metadata/android/cs/changelogs/961.txt b/fastlane/metadata/android/cs/changelogs/961.txt
new file mode 100644
index 00000000000..db711f0d4c2
--- /dev/null
+++ b/fastlane/metadata/android/cs/changelogs/961.txt
@@ -0,0 +1,12 @@
+- [YouTube] Podpora mixu
+- [YouTube] Zobrazení informací o veřejnoprávních vysílatelích a Covid-19
+- [media.ccc.de] Přidána nejnovější videa
+- Přidán somálský překlad
+
+- Mnoho interních vylepšení
+
+- Opraveno sdílení videí z přehrávače
+- Opraveno prázdné webové zobrazení ReCaptcha
+- Opraven pád, ke kterému docházelo při odebírání streamu ze seznamu
+- [PeerTube] Opraveny související streamy
+- YouTube] Opraveno vyhledávání hudby na YouTube
diff --git a/fastlane/metadata/android/cs/changelogs/963.txt b/fastlane/metadata/android/cs/changelogs/963.txt
new file mode 100644
index 00000000000..e971418af19
--- /dev/null
+++ b/fastlane/metadata/android/cs/changelogs/963.txt
@@ -0,0 +1 @@
+- [YouTube] Opraveno pokračování kanálu
diff --git a/fastlane/metadata/android/cs/changelogs/964.txt b/fastlane/metadata/android/cs/changelogs/964.txt
new file mode 100644
index 00000000000..11eacbcd80d
--- /dev/null
+++ b/fastlane/metadata/android/cs/changelogs/964.txt
@@ -0,0 +1,8 @@
+- Přidána podpora kapitol v ovládání hráče
+- [PeerTube] Přidáno vyhledávání v sépiové barvě
+- Znovu přidáno tlačítko pro sdílení v zobrazení detailu videa a popis streamu přesunut do rozložení karet
+- Zakázáno obnovení jasu, pokud je gesto jasu zakázáno
+- Přidána položka seznamu pro přehrávání videa v Kodi
+- Opraven pád v případě, že na některých zařízeních není nastaven výchozí prohlížeč, a vylepšeny dialogy sdílení
+-
+-
diff --git a/fastlane/metadata/android/cs/changelogs/965.txt b/fastlane/metadata/android/cs/changelogs/965.txt
new file mode 100644
index 00000000000..c62dfa65c46
--- /dev/null
+++ b/fastlane/metadata/android/cs/changelogs/965.txt
@@ -0,0 +1,6 @@
+Opraven pád, ke kterému docházelo při změně pořadí skupin kanálů.
+Opraveno získávání dalších videí YouTube z kanálů a seznamů skladeb.
+Opraveno získávání komentářů YouTube.
+Přidána podpora podcest /watch/, /v/ a /w/ v adresách URL YouTube.
+Opraveno získávání id klienta služby SoundCloud a obsahu s geografickým omezením.
+Přidána lokalizace do severní kurdštiny.
diff --git a/fastlane/metadata/android/cs/changelogs/966.txt b/fastlane/metadata/android/cs/changelogs/966.txt
new file mode 100644
index 00000000000..212687b51c4
--- /dev/null
+++ b/fastlane/metadata/android/cs/changelogs/966.txt
@@ -0,0 +1,14 @@
+Novinka:
+- Přidat novou službu: Bandcamp
+
+Vylepšeno:
+- Přidána možnost, aby aplikace následovala motiv zařízení
+- Předcházení některým pádům zobrazením vylepšeného panelu chyb
+- Zobrazení více informací o tom, proč je obsah nedostupný
+- Hardwarové tlačítko mezerníku spouští přehrávání/pauzu
+- Zobrazení přípitku "Stahování zahájeno"
+
+Opraveno:
+- Oprava velmi malé miniatury v detailech videa při přehrávání na pozadí
+- Oprava prázdného názvu v minimalizovaném přehrávači
+-
diff --git a/fastlane/metadata/android/cs/changelogs/967.txt b/fastlane/metadata/android/cs/changelogs/967.txt
new file mode 100644
index 00000000000..ba62e27eb8d
--- /dev/null
+++ b/fastlane/metadata/android/cs/changelogs/967.txt
@@ -0,0 +1 @@
+Opraveno nesprávné fungování služby YouTube v EU. To bylo způsobeno novým systémem souborů cookie a souhlasu s ochranou osobních údajů, který vyžaduje, aby NewPipe nastavil soubor cookie CONSENT.
diff --git a/fastlane/metadata/android/cs/changelogs/968.txt b/fastlane/metadata/android/cs/changelogs/968.txt
new file mode 100644
index 00000000000..5c14d8a5ff1
--- /dev/null
+++ b/fastlane/metadata/android/cs/changelogs/968.txt
@@ -0,0 +1,7 @@
+Do nabídky dlouhého stisknutí tlačítka byla přidána možnost Podrobnosti o kanálu.
+Přidána funkce přejmenování názvu seznamu skladeb z rozhraní seznamu skladeb.
+Umožňuje uživateli pozastavit video během jeho ukládání do vyrovnávací paměti.
+Vyleštěn bílý motiv.
+Opraveno překrývání písem při použití větší velikosti písma.
+Opraveno chybějící video na zařízeních Formuler a Zephier.
+Opraveny různé pády.
diff --git a/fastlane/metadata/android/cs/changelogs/969.txt b/fastlane/metadata/android/cs/changelogs/969.txt
new file mode 100644
index 00000000000..8c581404790
--- /dev/null
+++ b/fastlane/metadata/android/cs/changelogs/969.txt
@@ -0,0 +1,8 @@
+- Povolení instalace na externí úložiště
+- [Bandcamp] Přidána podpora pro zobrazení prvních tří komentářů u streamu
+- Zobrazení přípitku "stahování zahájeno" pouze po zahájení stahování
+- Nenastavovat soubor cookie reCaptcha, pokud není uložen žádný soubor cookie
+- Přehrávač] Zlepšení výkonu mezipaměti
+- Přehrávač] Opraveno automatické nepřehrávání přehrávače
+- Zrušit předchozí Snackbary při mazání stahování
+- Opraven pokus o odstranění objektu, který není v seznamu
diff --git a/fastlane/metadata/android/cs/changelogs/970.txt b/fastlane/metadata/android/cs/changelogs/970.txt
new file mode 100644
index 00000000000..f526adf8c96
--- /dev/null
+++ b/fastlane/metadata/android/cs/changelogs/970.txt
@@ -0,0 +1,11 @@
+Nový
+- Zobrazení metadat obsahu (značky, kategorie, licence, ...) pod popisem
+- Přidána možnost "Zobrazit podrobnosti o kanálu" ve vzdálených (nelokálních) seznamech skladeb
+- Přidána možnost "Otevřít v prohlížeči" do nabídky dlouhého stisknutí tlačítka
+
+Opravena stránka
+- Opraven pád při otáčení na stránce s podrobnostmi o videu
+- Opraveno tlačítko "Přehrát s Kodi" v přehrávači, které vždy vyzve k instalaci aplikace Kore
+- Opraveno a vylepšeno nastavení cest pro import a export
+-
+-
diff --git a/fastlane/metadata/android/cs/changelogs/971.txt b/fastlane/metadata/android/cs/changelogs/971.txt
new file mode 100644
index 00000000000..481b9edf1ec
--- /dev/null
+++ b/fastlane/metadata/android/cs/changelogs/971.txt
@@ -0,0 +1,3 @@
+Hotfix
+- Zvětšení vyrovnávací paměti pro přehrávání po obnovení vyrovnávací paměti
+- Opraven pád na tabletech a televizorech při kliknutí na ikonu přehrávání v přehrávači
diff --git a/fastlane/metadata/android/cs/changelogs/972.txt b/fastlane/metadata/android/cs/changelogs/972.txt
new file mode 100644
index 00000000000..d0989c7b937
--- /dev/null
+++ b/fastlane/metadata/android/cs/changelogs/972.txt
@@ -0,0 +1,14 @@
+Nový
+Rozpoznání časových razítek a hashtagů v popisu
+Přidáno ruční nastavení režimu tabletu
+Přidána možnost skrýt přehrávané položky ve zdroji
+
+Vylepšený
+Správná podpora rozhraní Storage Access Framework
+Lepší zpracování chyb nedostupných a ukončených kanálů
+List sdílení Android pro uživatele Androidu 10+ nyní zobrazuje název obsahu.
+Aktualizované instance Invidious a podpora předávaných odkazů.
+
+Stabilní
+[YouTube] Obsah s věkovým omezením
+-
diff --git a/fastlane/metadata/android/cs/changelogs/973.txt b/fastlane/metadata/android/cs/changelogs/973.txt
new file mode 100644
index 00000000000..2eba9ea7c4c
--- /dev/null
+++ b/fastlane/metadata/android/cs/changelogs/973.txt
@@ -0,0 +1,4 @@
+Hotfix
+- Oprava ořezávání miniatur a názvů v mřížkovém rozložení kvůli špatnému výpočtu, kolik videí se vejde do jednoho řádku.
+- Oprava dialogu stahování, který zmizí, aniž by cokoli provedl, pokud je otevřen z nabídky sdílení
+- Aktualizace knihovny související s otevíráním externích činností, například výběrem souborů v rámci Storage Access Framework
diff --git a/fastlane/metadata/android/cs/changelogs/974.txt b/fastlane/metadata/android/cs/changelogs/974.txt
new file mode 100644
index 00000000000..3149e47375c
--- /dev/null
+++ b/fastlane/metadata/android/cs/changelogs/974.txt
@@ -0,0 +1,5 @@
+Hotfix
+- Oprava problémů s vyrovnávací pamětí způsobených škrcením YouTube
+- Oprava extrakce komentářů YouTube a pádů s vypnutými komentáři
+- Oprava vyhledávání hudby na YouTube
+- Oprava živých přenosů PeerTube
diff --git a/fastlane/metadata/android/cs/changelogs/975.txt b/fastlane/metadata/android/cs/changelogs/975.txt
new file mode 100644
index 00000000000..839bf0e3680
--- /dev/null
+++ b/fastlane/metadata/android/cs/changelogs/975.txt
@@ -0,0 +1,17 @@
+Nový
+- Zobrazení náhledu miniatur při hledání
+- Rozpoznání zakázaných komentářů
+- Umožňuje označit položku kanálu jako sledovanou
+- Zobrazit srdíčka komentářů
+
+Vylepšené stránky
+- Vylepšení rozvržení metadat a značek
+- Použití barvy služby na součásti uživatelského rozhraní
+
+Opraveno
+- Oprava miniatur v mini přehrávači
+- Oprava nekonečného vyrovnávací paměti u duplicitních položek fronty
+- Opravy některých přehrávačů, jako je otáčení a rychlejší zavírání
+-
+-
+-
diff --git a/fastlane/metadata/android/cs/changelogs/976.txt b/fastlane/metadata/android/cs/changelogs/976.txt
new file mode 100644
index 00000000000..cc28400f559
--- /dev/null
+++ b/fastlane/metadata/android/cs/changelogs/976.txt
@@ -0,0 +1,10 @@
+- Přidána možnost přímého otevření přehrávače ve fullscreenu
+- Umožňuje vybrat, které typy návrhů vyhledávání se mají zobrazit
+- Tmavé téma je nyní tmavší + přidána tmavá úvodní obrazovka
+- Vylepšený nástroj pro výběr souborů, který šedě označuje nechtěné soubory
+- Opraven import odběrů YouTube
+- Opakované přehrávání streamu vyžaduje opětovné klepnutí na tlačítko přehrávání
+- Opraveno ukončení zvukové relace
+-
+-
+-.
diff --git a/fastlane/metadata/android/cs/changelogs/977.txt b/fastlane/metadata/android/cs/changelogs/977.txt
new file mode 100644
index 00000000000..3418d9d4356
--- /dev/null
+++ b/fastlane/metadata/android/cs/changelogs/977.txt
@@ -0,0 +1,10 @@
+- Do nabídky dlouhého stisku bylo přidáno tlačítko "přehrát další".
+- Do filtru záměrů byla přidána předpona cesty ke krátkým filmům YouTube
+- Opraven import nastavení
+- Výměna pozice panelu vyhledávání s tlačítky přehrávače na obrazovce Fronta
+- Různé opravy související se správcem MediasessionManager
+- Opraveno nedokončení panelu vyhledávání po skončení videa
+- Zakázáno tunelování médií na RealtekATV
+- Rozšířena klikatelná oblast minimalizovaných tlačítek přehrávače
+
+-.
diff --git a/fastlane/metadata/android/cs/changelogs/978.txt b/fastlane/metadata/android/cs/changelogs/978.txt
new file mode 100644
index 00000000000..caaf1ac5732
--- /dev/null
+++ b/fastlane/metadata/android/cs/changelogs/978.txt
@@ -0,0 +1 @@
+Opraveno provádění kontroly nové verze NewPipe. Tato kontrola se někdy prováděla příliš brzy, a proto vedla k pádu aplikace. To by nyní mělo být opraveno.
diff --git a/fastlane/metadata/android/cs/changelogs/979.txt b/fastlane/metadata/android/cs/changelogs/979.txt
new file mode 100644
index 00000000000..8d3ee14924b
--- /dev/null
+++ b/fastlane/metadata/android/cs/changelogs/979.txt
@@ -0,0 +1,2 @@
+- Opraveno obnovení přehrávání
+- Vylepšení zajišťující, že služba, která určuje, zda má NewPipe kontrolovat nové verze, není spuštěna na pozadí
diff --git a/fastlane/metadata/android/cs/changelogs/980.txt b/fastlane/metadata/android/cs/changelogs/980.txt
new file mode 100644
index 00000000000..59139b3a2bd
--- /dev/null
+++ b/fastlane/metadata/android/cs/changelogs/980.txt
@@ -0,0 +1,13 @@
+Nový
+- Přidání možnosti "Přidat do seznamu skladeb" do nabídky sdílení
+- Přidána podpora pro krátké odkazy na y2u.be a PeerTube
+
+Vylepšené stránky
+- Kompaktnější ovládání rychlosti přehrávání
+- Kanál nyní zvýrazňuje nové položky
+- Možnost "Zobrazit sledované položky" ve feedu je nyní uložena
+
+Opraveno
+- Opravena extrakce lajků a dislajků na YouTube
+- Opraveno automatické přehrávání po návratu z pozadí
+A mnoho dalšího
diff --git a/fastlane/metadata/android/cs/changelogs/981.txt b/fastlane/metadata/android/cs/changelogs/981.txt
new file mode 100644
index 00000000000..6dd389fd720
--- /dev/null
+++ b/fastlane/metadata/android/cs/changelogs/981.txt
@@ -0,0 +1,2 @@
+Odstraněna podpora MediaParser, aby se opravilo selhání obnovení přehrávání po vyrovnávací paměti v systému Android 11+.
+Zakázáno tunelování médií na přehrávači Philips QM16XE, aby se odstranily problémy s přehráváním.
diff --git a/fastlane/metadata/android/cs/changelogs/982.txt b/fastlane/metadata/android/cs/changelogs/982.txt
new file mode 100644
index 00000000000..c666499a41f
--- /dev/null
+++ b/fastlane/metadata/android/cs/changelogs/982.txt
@@ -0,0 +1 @@
+Opraveno nepřehrávání jakéhokoli streamu ve službě YouTube.
diff --git a/fastlane/metadata/android/cs/changelogs/983.txt b/fastlane/metadata/android/cs/changelogs/983.txt
new file mode 100644
index 00000000000..004ccca65c0
--- /dev/null
+++ b/fastlane/metadata/android/cs/changelogs/983.txt
@@ -0,0 +1,9 @@
+Přidání nového uživatelského rozhraní a chování při hledání dvojitým klepnutím
+Možnost vyhledávání v nastavení
+Zvýraznění připnutých komentářů jako takových
+Přidat podporu open-with-app pro instanci FSFE PeerTube
+Přidat oznámení o chybách
+Oprava přehrávání první položky fronty při změně hráče
+Při vyrovnávací paměti během živých přenosů čekat déle, než dojde k selhání
+Oprava pořadí výsledků místního vyhledávání
+Oprava prázdných políček položek ve frontě přehrávání
diff --git a/fastlane/metadata/android/cs/changelogs/984.txt b/fastlane/metadata/android/cs/changelogs/984.txt
new file mode 100644
index 00000000000..3b9eb35a4bf
--- /dev/null
+++ b/fastlane/metadata/android/cs/changelogs/984.txt
@@ -0,0 +1,7 @@
+načtení dostatečného množství počátečních položek v seznamech, aby zaplnily celou obrazovku, a oprava posouvání na tabletech a televizorech.
+Oprava náhodných pádů při procházení seznamů
+Nechat překryvný oblouk rychlého vyhledávání hráče přejít pod uživatelské rozhraní systému
+Vrátit změny výřezů při přehrávání ve více oknech, které způsobovaly regresi chybně umístěného přehrávače na některých telefonech
+Zvýšit compileSdk z 30 na 31
+Aktualizovat knihovnu pro hlášení chyb
+-
diff --git a/fastlane/metadata/android/cs/changelogs/985.txt b/fastlane/metadata/android/cs/changelogs/985.txt
new file mode 100644
index 00000000000..7035a111220
--- /dev/null
+++ b/fastlane/metadata/android/cs/changelogs/985.txt
@@ -0,0 +1 @@
+Opraveno nepřehrávání jakéhokoli streamu ve službě YouTube
diff --git a/fastlane/metadata/android/cs/changelogs/986.txt b/fastlane/metadata/android/cs/changelogs/986.txt
new file mode 100644
index 00000000000..0ffe8dabc51
--- /dev/null
+++ b/fastlane/metadata/android/cs/changelogs/986.txt
@@ -0,0 +1,16 @@
+Nový
+• Oznámení o nových streamech
+• Bezproblémový přechod mezi přehrávači na pozadí a videem
+• Změna výšky tónu podle půltónů
+• Připojení fronty hlavního přehrávače k seznamu skladeb
+
+Vylepšený
+• Zapamatujte si velikost kroku rychlosti / stoupání
+• Zmírnění počátečního dlouhého ukládání do vyrovnávací paměti v přehrávači videa
+• Vylepšete uživatelské rozhraní přehrávače pro Android TV
+• Potvrďte před odstraněním všech stažených souborů
+
+Stabilní
+•
+•
+•
diff --git a/fastlane/metadata/android/cs/changelogs/987.txt b/fastlane/metadata/android/cs/changelogs/987.txt
new file mode 100644
index 00000000000..51cd846c2a1
--- /dev/null
+++ b/fastlane/metadata/android/cs/changelogs/987.txt
@@ -0,0 +1,12 @@
+Nový
+- Podpora jiných způsobů doručování než progresivního HTTP: rychlejší načítání přehrávání, opravy pro PeerTube a SoundCloud, přehrávání nedávno ukončených živých přenosů na YouTube.
+- Tlačítko Přidat pro přidání vzdáleného seznamu skladeb do místního seznamu skladeb
+- Náhled obrázku ve sdíleném listu systému Android 10+
+
+Vylepšená stránka
+- Vylepšení dialogového okna s parametry přehrávání
+- Přesunutí tlačítek pro import/export předplatného do nabídky se třemi tečkami
+
+Opraveno
+-
+-
diff --git a/fastlane/metadata/android/cs/changelogs/988.txt b/fastlane/metadata/android/cs/changelogs/988.txt
new file mode 100644
index 00000000000..76da8121ccb
--- /dev/null
+++ b/fastlane/metadata/android/cs/changelogs/988.txt
@@ -0,0 +1,2 @@
+[YouTube] Oprava chyby "Nelze načíst žádný stream" při pokusu o přehrání jakéhokoli videa
+[YouTube] Oprava zprávy "Následující obsah není v této aplikaci k dispozici." zobrazené místo požadovaného videa
diff --git a/fastlane/metadata/android/cs/changelogs/989.txt b/fastlane/metadata/android/cs/changelogs/989.txt
new file mode 100644
index 00000000000..08ce8dd95ae
--- /dev/null
+++ b/fastlane/metadata/android/cs/changelogs/989.txt
@@ -0,0 +1,3 @@
+• [YouTube] Oprava nekonečného načítání při pokusu přehrát jakékoli video
+• [YouTube] Oprava omezování výkonu u některých videí
+• Aktualizace knihovny jsoup na verzi 1.15.3, která obsahuje bezpečnostní opravu
diff --git a/fastlane/metadata/android/cs/changelogs/990.txt b/fastlane/metadata/android/cs/changelogs/990.txt
new file mode 100644
index 00000000000..f3145df07e1
--- /dev/null
+++ b/fastlane/metadata/android/cs/changelogs/990.txt
@@ -0,0 +1,14 @@
+V této verzi byla zrušena podpora Androidu 4.4 KitKat, nyní je minimální verzí Android 5 Lollipop!
+
+Nové
+• Stahování z nabídky dlouhého stisknutí
+• Skrytí nadcházejících videí ve zdroji
+• Sdílení místních seznamů skladeb
+
+Vylepšení
+• Přepracování kódu přehrávače na malé komponenty: menší využití RAM, méně chyb
+• Vylepšení režimu měřítka miniatur
+• Vektorizace zástupných symbolů obrázků
+
+Opravy
+• Oprava různých problémů s oznámeními: neaktuální/chybějící informace o médiích, zkreslené miniatury
diff --git a/fastlane/metadata/android/de/changelogs/987.txt b/fastlane/metadata/android/de/changelogs/987.txt
index b6870154cb6..a857b1caac3 100644
--- a/fastlane/metadata/android/de/changelogs/987.txt
+++ b/fastlane/metadata/android/de/changelogs/987.txt
@@ -9,4 +9,4 @@ Verbesserte
Behoben
- Fix: Entfernen vollständig angesehener Videos aus der Wiedergabeliste
-- Repariert das Thema des Freigabemenüs und den Eintrag "Zur Wiedergabeliste hinzufügen".
+- Repariert das Thema des Freigabemenüs und den Eintrag "Zur Wiedergabeliste hinzufügen"
diff --git a/fastlane/metadata/android/en-US/changelogs/990.txt b/fastlane/metadata/android/en-US/changelogs/990.txt
new file mode 100644
index 00000000000..e12c20ba589
--- /dev/null
+++ b/fastlane/metadata/android/en-US/changelogs/990.txt
@@ -0,0 +1,15 @@
+This release drops support for Android 4.4 KitKat, now the minimum version is Android 5 Lollipop!
+
+New
+• Download from long-press menu
+• Hide future videos in feed
+• Share local playlists
+
+Improved
+• Refactor the player code into small components: less RAM used, less bugs
+• Improve thumbnails' scale mode
+• Vector-ize image placeholders
+
+Fixed
+• Fix various issues with the player notification: outdated/missing media info, distorted thumbnail
+• Fix fullscreen using 1/4 of screen
diff --git a/fastlane/metadata/android/es/changelogs/69.txt b/fastlane/metadata/android/es/changelogs/69.txt
new file mode 100644
index 00000000000..a2d1c839532
--- /dev/null
+++ b/fastlane/metadata/android/es/changelogs/69.txt
@@ -0,0 +1,19 @@
+### Nuevo
+- Mantén pulsado para borrar y/o compartir en Subscripciones #1516
+- Interfaz de Tablet y listas en forma de cuadrícula #1617
+
+### Mejoras
+- Guarda y usa la última relación de aspecto #1748
+- Activa las listas lineares en las Descargas con los nombres completos #1771
+- Guarda y comparte subscripciones directamente desde la pestaña de subscripciones #1516
+- Poner en cola un video hace que empiece a reproducirse si la cola ya ha acabado #1783
+- Ajustes para gestos separados del el brillo y el volumen #1644
+- Añadido soporte para traducciones #1792
+
+### Arreglos
+- Arreglada la obtención de fecha para el .format, de modo que NewPipe se puede usar en Finlandia.
+- Arreglado el contador de subscriptores
+- Añadido permiso para arrancar en primer plano para dispositivos con API +28 #1830
+
+### Bugs Conocidos
+- El estado de la reproducción no se puede guardar en Android P
diff --git a/fastlane/metadata/android/es/changelogs/70.txt b/fastlane/metadata/android/es/changelogs/70.txt
new file mode 100644
index 00000000000..b5cd027a70d
--- /dev/null
+++ b/fastlane/metadata/android/es/changelogs/70.txt
@@ -0,0 +1,25 @@
+ATENCIÓN: Esta versión quizá sea un festival de errores, como la anterior. Sin embargo, debido al cierre total desde la 17. una versión rota es mejor que ninguna versión. ¿Cierto? ¯\_(ツ)_/¯
+
+### Mejorías
+* los archivos descargados ahora pueden ser abiertos con un solo clic #1879
+* descenso de soporte para android 4.1 - 4.3 #1884
+* eliminar el reproductor antiguo #1884
+* eliminar los flujos de la cola de reproducción actual deslizándolos hacia la derecha #1915
+* eliminar cola de reproducción automática cuando se pone en cola una nueva secuencia manualmente #1878
+* Posprocesamiento para descargas e implementación de características faltantes #1759 por @kapodamy
+ * Infraestructura de posprocesamiento
+ * Infraestructura de manejo de errores adecuada (para el descargador)
+ * Cola en lugar de descargas múltiples
+* Mover las descargas pendientes serializadas (archivos `.giga`) hacia datos de aplicación
+ * Implementar el reintento máximo de descarga
+ * Pausa adecuada de descargas multihilo
+* Detener las descargas cuando se cambia hacia red móvil (nunca funciona, ver 2º punto)
+* Guardar el conteo de hilos para las próximas descargas
+ * Un montón de incoherencias corregidas
+
+### Corregidos
+* Arreglado el fallo con la resolución por defecto ajustada a la mejor y resolución de datos móviles limitada #1835
+* Arreglado el fallo del reproductor emergente #1874
+* NPE al intentar abrir el reproductor en segundo plano #1901
+* Corrección de la inserción de nuevos flujos cuando la cola automática está activada #1878
+* Corregido el problema de descifrado de shuttown
diff --git a/fastlane/metadata/android/es/changelogs/740.txt b/fastlane/metadata/android/es/changelogs/740.txt
new file mode 100644
index 00000000000..6d89a224699
--- /dev/null
+++ b/fastlane/metadata/android/es/changelogs/740.txt
@@ -0,0 +1,23 @@
+
Mejorías
+
+
hacer cliqueables enlaces en comentarios, aumentar el tamaño del texto
+
buscar al hacer clic en enlaces de marca de tiempo en comentarios
+
mostrar pestaña preferida según el estado seleccionado recientemente
+
añadir lista de reproducción a cola cuando se hace un clic largo en 'Fondo' en ventana de lista de reproducción
+
buscar texto compartido cuando no es una URL
+
añadir botón "compartir en el momento actual" al reproductor de vídeo principal
+
añadir botón de cierre a reproductor principal cuando la cola de vídeo haya terminado
+
añadir "Reproducir directamente en segundo plano" a menú de pulsación larga para elementos lista de vídeos
+
mejorar traducciones a inglés de comandos Reproducir/PonerEnCola
+
pequeñas mejorías de rendimiento
+
eliminar archivos no utilizados
+
actualizar ExoPlayer a 2.9.6
+
añadir soporte para enlaces Invidious
+
+
Arreglado
+
+
arreglado desplazamiento con comentarios y flujos relacionados desactivados
+
arreglado que TareaBuscarNuevaVersiónDeApp se ejecute cuando no debería
+
corregida la importación de suscripciones a YouTube: ignorar las que tienen URL inválida y mantener las que tienen el título vacío
+arreglar URL inválida de YouTube: nombre de etiqueta de firma no es siempre "firma", lo que impide cargar flujos
+
diff --git a/fastlane/metadata/android/es/changelogs/810.txt b/fastlane/metadata/android/es/changelogs/810.txt
new file mode 100644
index 00000000000..2f569dc5bf8
--- /dev/null
+++ b/fastlane/metadata/android/es/changelogs/810.txt
@@ -0,0 +1,19 @@
+Nuevo
+- Mostrar la miniatura del vídeo en la pantalla de bloqueo cuando se reproduce en segundo plano
+
+Mejorado
+- Añadir la lista de reproducción local a la cola cuando se hace una pulsación larga en el botón de fondo / emergente
+- Hacer que las pestañas de la página principal se puedan desplazar y ocultar cuando sólo hay una pestaña
+- Limitar la cantidad de actualizaciones de miniaturas de notificación en el reproductor de fondo
+- Añadir una miniatura ficticia para listas de reproducción locales vacías
+- Usar la extensión de archivos *.opus en lugar de *.webm y mostrar "opus" en etiqueta de formato en lugar de "WebM Opus" en menú desplegable de descargas
+- Añadir un botón para eliminar archivos descargados o el historial de descargas en "Descargas"
+- YouTube] Añadir soporte a los enlaces de canal /c/shortened_url
+
+Corregidos
+- Corregidos múltiples problemas al compartir un video a NewPipe y al descargar sus secuencias directamente
+- Corregido el acceso al reproductor fuera de su hilo de creación
+- Corregida la paginación de resultados de búsqueda
+- YouTube] Corregido el cambio a nulo que causaba NPE
+- YouTube] Corregida la visualización de comentarios al abrir una url de invidio.us
+- SoundCloud] Actualizado client_id
diff --git a/fastlane/metadata/android/es/changelogs/840.txt b/fastlane/metadata/android/es/changelogs/840.txt
new file mode 100644
index 00000000000..37c0628a8ad
--- /dev/null
+++ b/fastlane/metadata/android/es/changelogs/840.txt
@@ -0,0 +1,24 @@
+Nuevo
+- Se ha añadido un selector de idioma para cambiar el idioma de la aplicación
+- Se ha añadido el botón de enviar a Kodi al menú desplegable del reproductor
+- Se ha añadido la posibilidad de copiar comentarios con una pulsación larga
+
+Mejorado
+- Se ha corregido la actividad de ReCaptcha y se han guardado correctamente las cookies obtenidas
+- Menú de puntos eliminado en favor de cajón y botón del historial ocultado cuando no está habilitado el historial de reloj en ajustes
+- Pedir permiso de visualización sobre otras aplicaciones en ajustes correctamente en Android 6 y posteriores
+- Cambiar nombre de lista de reproducción local haciendo un clic largo en MarcadorDePáginasFragmentos
+- Varias mejorías en PeerTube
+- Mejoría de varias cadenas de origen en inglés
+
+Corregido
+Corregido que reproductor se reinicie aunque esté en pausa con opción "minimizar al cambiar de app" activada y NewPipe está minimizado
+- Corregido el valor de brillo inicial para el gesto
+- Corregida la descarga de subtítulos .srt que no contienen todos los saltos de línea
+- Corregida descarga a tarjeta SD que falla porque algunos dispositivos Android 5 no son compatibles con CTF
+- Corregida la descarga en Android KitKat
+- Arreglado el archivo de vídeo .mp4 corrupto que era reconocido como archivo de audio
+- Corregidos múltiples problemas de localización, incluyendo códigos de idioma chino erróneos
+- YouTube] Las marcas de tiempo en la descripción vuelven a ser cliqueables
+
+Traducción realizada con la versión gratuita del traductor www.DeepL.com/Translator
diff --git a/fastlane/metadata/android/es/changelogs/900.txt b/fastlane/metadata/android/es/changelogs/900.txt
new file mode 100644
index 00000000000..6ae175a1728
--- /dev/null
+++ b/fastlane/metadata/android/es/changelogs/900.txt
@@ -0,0 +1,14 @@
+Nuevo
+- Grupos de suscriptores y feeds ordenados
+- Botón de silencio en reproductores
+
+Mejora de
+- Permitir la apertura de enlaces music.youtube.com y media.ccc.de en NewPipe
+- Reubicar dos ajustes de Apariencia a Contenido
+- Ocultar opciones de búsqueda de 5, 15 y 25 seg. si activada búsqueda inexacta
+
+Corregido
+- algunos vídeos WebM no pueden ser buscados
+- copia de seguridad de base de datos en Android P
+- caída al compartir un archivo descargado
+- montones de problema de extracción de YouTube y más ...
diff --git a/fastlane/metadata/android/es/changelogs/930.txt b/fastlane/metadata/android/es/changelogs/930.txt
new file mode 100644
index 00000000000..65cd48b98c6
--- /dev/null
+++ b/fastlane/metadata/android/es/changelogs/930.txt
@@ -0,0 +1,19 @@
+Nuevo
+- Búsqueda en YouTube Music
+- Soporte básico de Android TV
+
+Mejorías de
+- Añadida la opción de borrar todos los vídeos vistos de lista de reproducción local
+- Mostrar mensaje cuando el contenido aún no es compatible en lugar de caída
+- Mejora del tamaño del reproductor emergente con gestos de pellizco
+- Puesta en cola de flujos con presión prolongada de botones de fondo y emergentes en el canal
+- Mejora de la gestión del tamaño del título de la cabecera de cajón
+
+Corregido
+- Arreglado ajuste restringido de contenido por edad que no funciona
+- Corregidos ciertos tipos de reCAPTCHAs
+- Fallo corregido al abrir marcadores mientras lista de reproducción es "nula".
+- Corregida la detección de excepciones relacionadas con la red
+- Corregida visibilidad de botón de clasificación de grupos en fragmento de suscripciones
+
+y más
diff --git a/fastlane/metadata/android/es/changelogs/940.txt b/fastlane/metadata/android/es/changelogs/940.txt
new file mode 100644
index 00000000000..41a09c1a0fa
--- /dev/null
+++ b/fastlane/metadata/android/es/changelogs/940.txt
@@ -0,0 +1,16 @@
+Nuevos
+- Añadir soporte para los comentarios de SoundCloud
+- Añadir la configuración del modo restringido de YouTube
+- Mostrar los detalles del canal padre de PeerTube
+
+Mejora de
+- Mostrar el botón Kore sólo para los servicios compatibles
+- Bloquear gestos de reproductor que inician en NavigationBar o StatusBar
+- Cambio color fondo de botones reintento y suscripción según color de servicio
+
+Arreglados
+- Corregido el congelamiento del diálogo de descarga
+- El botón "Abrir en navegador" ahora se abre realmente en navegador
+- Arreglo de colapso al abrir vídeos y "No se pudo reproducir este flujo"
+
+y más
diff --git a/fastlane/metadata/android/es/changelogs/964.txt b/fastlane/metadata/android/es/changelogs/964.txt
new file mode 100644
index 00000000000..8de16d21aad
--- /dev/null
+++ b/fastlane/metadata/android/es/changelogs/964.txt
@@ -0,0 +1,8 @@
+- Soporte añadido para capítulos en controles del reproductor
+- [PeerTube] Añadida la búsqueda de Sepia
+- Botón compartir reañadido en vista detalles de vídeo y descripción de secuencia movida a diseño de pestaña
+- Desactivar restauración de brillo si gesto de brillo desactivado
+- Añadido el elemento de lista para reproducir vídeo en kodi
+- Fallo corregido si no hay navegador por defecto en ciertos dispositivos, diálogos compartir mejorados
+- Alternar reproducción/pausa con botón de espacio de hardware en reproductor de pantalla completa
+- [media.ccc.de] Varias correcciones y mejoras
diff --git a/fastlane/metadata/android/es/changelogs/966.txt b/fastlane/metadata/android/es/changelogs/966.txt
new file mode 100644
index 00000000000..0cb20696cbc
--- /dev/null
+++ b/fastlane/metadata/android/es/changelogs/966.txt
@@ -0,0 +1,14 @@
+Nuevo:
+- Añadir un nuevo servicio: Bandcamp
+
+Mejorados:
+- Añadir una opción para que la app siga el tema del dispositivo
+- Evitar algunos colapsos mostrando un panel de error mejorado
+- Mostrar más información sobre razón de contenido no disponible
+- Botón de espacio de hardware activa reproducción/pausa
+- Mostrar brindis de "Descarga iniciada"
+
+Corregidos:
+- Arreglar miniatura chica en detalles de vídeo durante reproducción de fondo
+- Arreglar título vacío en el reproductor minimizado
+- Arreglar último modo de redimensionamiento no restaurable correctamente
diff --git a/fastlane/metadata/android/es/changelogs/968.txt b/fastlane/metadata/android/es/changelogs/968.txt
new file mode 100644
index 00000000000..6ef82f8d98e
--- /dev/null
+++ b/fastlane/metadata/android/es/changelogs/968.txt
@@ -0,0 +1,7 @@
+Opción de detalles de canal añadida a menú de pulsación larga.
+Función añadida de cambiar Nombre de Lista de Reproducción desde su interfaz.
+Permitir al usuario pausar video almacenando en memoria intermedia.
+Se ha pulido el tema blanco.
+Solapamiento corregido de fuentes al usar un tamaño mayor de fuente.
+Ausencia de video corregida en dispositivos Formuler y Zephier.
+Se han corregido varios fallos.
diff --git a/fastlane/metadata/android/es/changelogs/969.txt b/fastlane/metadata/android/es/changelogs/969.txt
new file mode 100644
index 00000000000..7714b1aef3e
--- /dev/null
+++ b/fastlane/metadata/android/es/changelogs/969.txt
@@ -0,0 +1,8 @@
+- Permitir instalación en almacenamiento externo
+- Bandcamp] Soporte añadido para mostrar 3 primeros comentarios en una secuencia
+- Sólo mostrar brindis de "descarga iniciada" cuando la descarga inicia
+- No establecer la cookie reCaptcha si no hay cookies almacenadas
+- [Reproductor] Mejorar el rendimiento de la caché
+- [Reproductor] Arreglado reproductor sin reproducción automática
+- Descartar Snackbars anteriores al borrar descargas
+- Corregido intento de eliminar objetos fuera de lista
diff --git a/fastlane/metadata/android/es/changelogs/970.txt b/fastlane/metadata/android/es/changelogs/970.txt
new file mode 100644
index 00000000000..39bb2e23e8f
--- /dev/null
+++ b/fastlane/metadata/android/es/changelogs/970.txt
@@ -0,0 +1,11 @@
+Nuevos
+- Mostrar metadatos de contenido (etiquetas, categorías, licencia, ...) bajo descripción
+- Opción añadida "Mostrar detalles de canal" en listas de reproducción remotas (no locales)
+- Opción añadida "Abrir en navegador" en menú de pulsación larga
+
+Corregidos
+- Fallo corregido de rotación en la página de detalles de vídeo
+- Botón corregido "Reproducir con Kodi" en reproductor que siempre pide instalar Kore
+- Corregido y mejorado ajuste de rutas de importación y exportación
+- [YouTube] Corregido el recuento de comentarios preferidos
+Y mucho más
diff --git a/fastlane/metadata/android/es/changelogs/973.txt b/fastlane/metadata/android/es/changelogs/973.txt
new file mode 100644
index 00000000000..09576bc99da
--- /dev/null
+++ b/fastlane/metadata/android/es/changelogs/973.txt
@@ -0,0 +1,4 @@
+Corrección en caliente
+- Corrección de miniaturas y títulos recortados en diseño de cuadrícula, por cálculo erróneo de cuántos vídeos caben en 1 fila
+- Corrección de diálogo de descarga que desaparece sin hacer nada si se abre desde menú compartir
+- Actualización de biblioteca relacionada con apertura de actividades externas, como selector de archivos de Marco de Acceso a Almacenamiento
diff --git a/fastlane/metadata/android/es/changelogs/980.txt b/fastlane/metadata/android/es/changelogs/980.txt
new file mode 100644
index 00000000000..b3381047969
--- /dev/null
+++ b/fastlane/metadata/android/es/changelogs/980.txt
@@ -0,0 +1,13 @@
+Nuevos
+- Opción añadida "Añadir a lista de reproducción" a menú compartir
+- Soporte añadido para enlaces cortos de y2u.be y PeerTube
+
+Mejorados
+- Controles de velocidad de reproducción más compactos
+- Feed destaca ahora nuevos elementos
+- Ahora se guarda la opción "Mostrar elementos vistos" en feed
+
+Corregidos
+- Extracción corregida de "likes" y "dislikes" de YouTube
+- Repetición automática corregida después de volver del fondo
+Y mucho más
diff --git a/fastlane/metadata/android/es/changelogs/982.txt b/fastlane/metadata/android/es/changelogs/982.txt
new file mode 100644
index 00000000000..e38ba90c972
--- /dev/null
+++ b/fastlane/metadata/android/es/changelogs/982.txt
@@ -0,0 +1 @@
+Solución a YouTube no reproduciendo flujos.
diff --git a/fastlane/metadata/android/es/changelogs/984.txt b/fastlane/metadata/android/es/changelogs/984.txt
new file mode 100644
index 00000000000..3460b9754bc
--- /dev/null
+++ b/fastlane/metadata/android/es/changelogs/984.txt
@@ -0,0 +1,7 @@
+Carga suficientes elementos iniciales en listas para llenar pantalla entera y arreglo desplazamiento en tabletas y televisores
+Arreglar fallos aleatorios al desplazarse por las listas
+Hacer que arco de superposición de búsqueda rápida de reproductor vaya bajo la IU de sistema
+Revertir cambios en cortes al reproducir en multiventana, que causan regresión de reproductor desubicado en teléfonos
+Aumentar compileSdk de 30 a 31
+Actualizar la biblioteca de informes de errores
+Refactorizar algunos códigos en reproductor
diff --git a/fastlane/metadata/android/es/changelogs/985.txt b/fastlane/metadata/android/es/changelogs/985.txt
new file mode 100644
index 00000000000..80b4efa553d
--- /dev/null
+++ b/fastlane/metadata/android/es/changelogs/985.txt
@@ -0,0 +1 @@
+Arreglo en YouTube no reproduciendo flujos
diff --git a/fastlane/metadata/android/es/changelogs/986.txt b/fastlane/metadata/android/es/changelogs/986.txt
new file mode 100644
index 00000000000..1631b5d8162
--- /dev/null
+++ b/fastlane/metadata/android/es/changelogs/986.txt
@@ -0,0 +1,16 @@
+Nuevos
+- Notificaciones de nuevos flujos
+- Transición perfecta entre fondo y reproductores de vídeo
+- Cambio de tono por semitonos
+- Añadir cola de reproductor principal a lista de reproducción
+
+Mejorías
+- Recordar el tamaño del paso de velocidad/tono
+- Mitigar el largo buffering inicial en el reproductor de vídeo
+- Mejor interfaz de usuario de reproductor para Android TV
+- Confirmar antes de borrar todos los archivos descargados
+
+Corregidos
+- Arreglar botón multimedia no oculta controles de reproductor
+- Corregir reinicio reproducción al cambiar tipo de reproductor
+- Arreglar rotación de diálogo de lista de reproducción
diff --git a/fastlane/metadata/android/es/changelogs/987.txt b/fastlane/metadata/android/es/changelogs/987.txt
new file mode 100644
index 00000000000..bdad4e10daf
--- /dev/null
+++ b/fastlane/metadata/android/es/changelogs/987.txt
@@ -0,0 +1,12 @@
+Nuevos
+- Soporta métodos de entrega distintos a HTTP progresivo: tiempo más rápido de carga de reproducción, arreglos PeerTube y SoundCloud, reproducción de livestreams YouTube recién terminados
+- Botón para añadir una lista de reproducción remota a una local
+- Vista previa de imagen en hoja de compartir de Android 10+
+
+Mejorías
+- Mejorar diálogo de parámetros de reproducción
+- Mover botones de importación/exportación de suscripciones a menú de 3 puntos
+
+Arreglados
+- Arreglar eliminación de vídeos totalmente vistos de lista de reproducción
+- Tema corregido de menú compartir y entrada "añadir a lista de reproducción
diff --git a/fastlane/metadata/android/es/changelogs/988.txt b/fastlane/metadata/android/es/changelogs/988.txt
new file mode 100644
index 00000000000..0e1a88e6f9b
--- /dev/null
+++ b/fastlane/metadata/android/es/changelogs/988.txt
@@ -0,0 +1,2 @@
+[YouTube] Arreglo error "No se pudo obtener flujo" al intentar reproducir videos
+[YouTube] Arreglo el mensaje "Siguiente contenido no disponible en esta aplicación" en lugar de video solicitado
diff --git a/fastlane/metadata/android/es/changelogs/989.txt b/fastlane/metadata/android/es/changelogs/989.txt
new file mode 100644
index 00000000000..72f3a8098b3
--- /dev/null
+++ b/fastlane/metadata/android/es/changelogs/989.txt
@@ -0,0 +1,3 @@
+- YouTube] Arreglo carga infinita al tratar de reproducir videos
+- YouTube] Arreglo de ralentización de algunos vídeos
+- Actualización de biblioteca jsoup a versión 1.15.3, con un arreglo de seguridad
diff --git a/fastlane/metadata/android/es/changelogs/990.txt b/fastlane/metadata/android/es/changelogs/990.txt
new file mode 100644
index 00000000000..217ceaaa96e
--- /dev/null
+++ b/fastlane/metadata/android/es/changelogs/990.txt
@@ -0,0 +1,15 @@
+Esta versión deja de soportar Android 4.4 KitKat, ¡ahora la versión mínima es Android 5 Lollipop!
+
+Nuevos
+- Descarga desde menú de pulsación larga
+- Ocultar futuros vídeos en feed
+- Compartir listas de reproducción locales
+
+Mejorados
+- Refactorización de código de reproductor en componentes pequeños: menos RAM usada, menos errores
+- Mejorar el modo de escala de miniaturas
+- Vectorizar marcadores de posición de imágenes
+
+Corregidos
+- Arreglos varios con notificación de reproductor: antigua/falta información de medios, miniatura distorsionada
+- Arreglo pantalla completa usa 1/4 de pantalla
diff --git a/fastlane/metadata/android/fr/changelogs/63.txt b/fastlane/metadata/android/fr/changelogs/63.txt
index be078632bfb..b9abcd760aa 100644
--- a/fastlane/metadata/android/fr/changelogs/63.txt
+++ b/fastlane/metadata/android/fr/changelogs/63.txt
@@ -1,8 +1,8 @@
### Améliorations
-- Importation/exportation des paramètres #1333
+- Import/export des paramètres #1333
- Réduction overdraw (amélioration des performances) #1371
- Petites améliorations du code #1375
-- GDPR #1420
+- Ajout d'un popup RGPD #1420
### Corrections
- Téléchargeur : Correction d'un plantage lors du chargement de téléchargements inachevés de fichiers .giga #1407
diff --git a/fastlane/metadata/android/fr/changelogs/65.txt b/fastlane/metadata/android/fr/changelogs/65.txt
new file mode 100644
index 00000000000..bb664a3cb18
--- /dev/null
+++ b/fastlane/metadata/android/fr/changelogs/65.txt
@@ -0,0 +1,26 @@
+### Améliorations
+
+- L'animation de l'icône du burgermenu a été désactivé #1486
+- Annulation de la suppression des téléchargements #1472
+- Option de téléchargement dans le menu de partage #1498
+- Ajout d'une option de partage dans le menu "long tap" #1454
+- Réduction du lecteur principal à la sortie #1354
+- Mise à jour de la version de la bibliothèque et correction de la sauvegarde de la base de données #1510
+- Mise à jour de ExoPlayer 2.8.2 #1392
+ - La boîte de dialogue de contrôle de la vitesse de lecture a été retravaillée pour prendre en charge différentes tailles de pas pour un changement de vitesse plus rapide.
+ - Ajout d'une option d'avance rapide pendant les silences dans le contrôle de la vitesse de lecture. Cela devrait être utile pour les livres audio et certains genres musicaux, et peut apporter une véritable expérience transparente (et peut casser une chanson avec beaucoup de silences =\\).
+ - Refonte de la résolution des sources de médias pour permettre le passage des métadonnées avec les médias en interne dans le lecteur, plutôt que de le faire manuellement. Maintenant, nous avons une seule source de métadonnées et elles sont directement disponibles lorsque la lecture commence.
+ - Correction des métadonnées des listes de lecture distantes qui ne sont pas mises à jour lorsque de nouvelles métadonnées sont disponibles lors de l'ouverture du fragment de liste de lecture.
+ - Diverses corrections de l'interface utilisateur : #1383, les contrôles de notification du lecteur en arrière-plan sont maintenant toujours blancs, il est plus facile de fermer le lecteur popup en le lançant.
+- Utilisation d'un nouvel extracteur avec une architecture remaniée pour le multiservice.
+
+### Corrections
+
+- Correction #1440 Disposition des informations vidéo cassée #1491
+- Correction de l'historique des vues #1497
+ - #1495, en mettant à jour les métadonnées (vignette, titre et nombre de vidéos) dès que l'utilisateur accède à la liste de lecture.
+ - #1475, en enregistrant une vue dans la base de données lorsque l'utilisateur lance une vidéo sur un lecteur externe sur le fragment de détail.
+- Correction du timeout de la fenêtre en cas de mode popup. #1463 (Corrigé #640)
+- Correction du lecteur vidéo principal #1509
+ - Correction du mode répétition entraînant un NPE du lecteur lorsqu'une nouvelle intention est reçue alors que l'activité du lecteur est en arrière-plan.
+ - Correction de la réduction du lecteur en popup ne détruisant pas le lecteur lorsque la permission de popup n'est pas accordée.
diff --git a/fastlane/metadata/android/fr/changelogs/66.txt b/fastlane/metadata/android/fr/changelogs/66.txt
new file mode 100644
index 00000000000..3a94c81e03f
--- /dev/null
+++ b/fastlane/metadata/android/fr/changelogs/66.txt
@@ -0,0 +1,28 @@
+# Journal des modifications de la v0.13.6
+
+### Améliorations
+
+- L'animation de l'icône du menu « hamburger » a été désactivée #1486
+- Annulation de la suppression des téléchargements #1472
+- Option de téléchargement dans le menu de partage #1498
+- Ajout d'une option de partage dans le menu "long tap" #1454
+- Réduction du lecteur principal à la sortie #1354
+- Mise à jour de la version de la bibliothèque et correction de la sauvegarde de la base de données #1510
+- Mise à jour de ExoPlayer 2.8.2 #1392
+ - La boîte de dialogue de contrôle de la vitesse de lecture a été retravaillée pour prendre en charge différentes tailles de pas pour un changement de vitesse plus rapide.
+ - Ajout d'une option d'avance rapide pendant les silences dans le contrôle de la vitesse de lecture. Cela devrait être utile pour les livres audio et certains genres musicaux, et peut apporter une véritable expérience transparente (et peut casser une chanson avec beaucoup de silences =\\).
+ - Refonte de la résolution des sources de médias pour permettre le passage des métadonnées avec les médias en interne dans le lecteur, plutôt que de le faire manuellement. Maintenant, nous avons une seule source de métadonnées et elles sont directement disponibles lorsque la lecture commence.
+ - Correction des métadonnées des listes de lecture distantes qui ne sont pas mises à jour lorsque de nouvelles métadonnées sont disponibles lors de l'ouverture du fragment de liste de lecture.
+ - Diverses corrections de l'interface utilisateur : #1383, les contrôles de notification du lecteur en arrière-plan sont maintenant toujours blancs, il est plus facile de fermer le lecteur popup en le lançant.
+- Utilisation d'un nouvel extracteur avec une architecture remaniée pour le multiservice.
+
+### Corrections
+
+- Correction #1440 Disposition des informations vidéo cassée #1491
+- Correction de l'historique des vues #1497
+ - #1495, en mettant à jour les métadonnées (vignette, titre et nombre de vidéos) dès que l'utilisateur accède à la liste de lecture.
+ - #1475, en enregistrant une vue dans la base de données lorsque l'utilisateur lance une vidéo sur un lecteur externe sur le fragment de détail.
+- Correction du timeout de la fenêtre en cas de mode popup. #1463 (Corrigé #640)
+- Correction du lecteur vidéo principal #1509
+ - Correction du mode répétition entraînant un NPE du lecteur lorsqu'une nouvelle intention est reçue alors que l'activité du lecteur est en arrière-plan.
+ - Correction de la réduction du lecteur en popup ne détruisant pas le lecteur lorsque la permission de popup n'est pas accordée.
diff --git a/fastlane/metadata/android/fr/changelogs/68.txt b/fastlane/metadata/android/fr/changelogs/68.txt
new file mode 100644
index 00000000000..9b2760c35fd
--- /dev/null
+++ b/fastlane/metadata/android/fr/changelogs/68.txt
@@ -0,0 +1,31 @@
+# Modifications v0.14.1
+
+### Corrections
+- Échec du décryptage de l'URL vidéo #1659
+- Lien de description, ne s'extrayait pas bien #1657
+
+# Modifications v0.14.0
+
+### Nouveautés
+- Design du dossier #1461
+- Page d'accueil personnalisable #1461
+
+### Améliorations
+- Contrôles gestuels retravaillés #1604
+- Nouvelle façon de fermer le lecteur popup #1597
+
+### Corrections
+- Erreur lorsque le nombre d'abonnements n'est pas disponible. Ferme #1649.
+ - Affiche "le nombre d'abonnés non disponible" dans ces cas.
+- NPE lorsqu'une liste de lecture YouTube est vide.
+- Kiosques dans SoundCloud
+- Refactor et correction du bug #1623
+- Résultat de recherche cyclique #1562
+- Barre de recherche qui n'est pas mise en page de manière statique
+- Vidéos YT Premium qui ne sont pas bloquées correctement
+- Vidéos qui ne se chargent pas toujours (à cause du parsing DASH)
+- Liens dans la description des vidéos
+- Afficher un avertissement lorsque quelqu'un essaie de télécharger vers une carte SD externe
+- Exception "rien indiqué" qui déclenche un rapport
+- La vignette ne s'affiche pas dans le lecteur de fond pour Android 8.1 [voir ici](https://github.com/TeamNewPipe/NewPipe/issues/943)
+- Enregistrement du récepteur de diffusion. Ferme le dossier #1641.
diff --git a/fastlane/metadata/android/fr/changelogs/69.txt b/fastlane/metadata/android/fr/changelogs/69.txt
new file mode 100644
index 00000000000..c96b390d9f9
--- /dev/null
+++ b/fastlane/metadata/android/fr/changelogs/69.txt
@@ -0,0 +1,19 @@
+### Nouveau
+- Suppression et partage par appui long dans les abonnements #1516
+- Interface utilisateur pour tablettes et disposition de la liste en grille #1617
+
+### Améliorations
+- Stockage/recharge du dernier rapport d'aspect utilisé #1748
+- Activation de la disposition linéaire dans l'activité Téléchargements avec les noms complets des vidéos #1771
+- Suppression et partage des abonnements directement à partir de l'onglet abonnements #1516
+- La mise en file d'attente déclenche désormais la lecture de la vidéo si la file d'attente de lecture est déjà terminée #1783
+- Paramètres distincts pour les gestes de volume et de luminosité #1644
+- Ajout de la prise en charge de la localisation #1792
+
+### Corrections
+- Analyse de l'heure pour le format . , afin que NewPipe puisse être utilisé en Finlande.
+- Compte d'abonnement
+- Ajout permission de service de premier plan pour les appareils API 28+ #1830
+
+### Bugs connus
+- État de lecture ne peut être enregistré sur Android P
diff --git a/fastlane/metadata/android/fr/changelogs/70.txt b/fastlane/metadata/android/fr/changelogs/70.txt
new file mode 100644
index 00000000000..fccfccd2bfe
--- /dev/null
+++ b/fastlane/metadata/android/fr/changelogs/70.txt
@@ -0,0 +1,25 @@
+ATTENTION : Cette version est probablement un festival de bugs, tout comme la dernière. Cependant, en raison de la fermeture complète depuis la 17. une version cassée est mieux que pas de version. N'est-ce pas ? ¯\_(ツ)_/¯
+
+### Améliorations
+* Les fichiers téléchargés peuvent maintenant être ouverts en un seul clic.
+* Suppression du support pour Android 4.1 - 4.3 #1884
+* Suppression de l'ancien lecteur #1884
+* Suppression des flux de la file d'attente de lecture actuelle en les faisant glisser vers la droite #1915
+* Suppression du flux en file d'attente automatique lorsqu'un nouveau flux est mis en file d'attente manuellement #1878
+* Post-traitement pour les téléchargements et implémentation des fonctionnalités manquantes #1759 par @kapodamy
+ * Infrastructure de post-traitement
+ * Infrastructure de gestion des erreurs (pour le téléchargeur)
+ * File d'attente au lieu de téléchargements multiples
+ * Déplacer les téléchargements sérialisés en attente (fichiers `.giga`) vers les données de l'application.
+ * Implémentation de la répétition maximale des téléchargements
+ * Mise en pause des téléchargements multi-threads
+ * Arrêter les téléchargements lors du passage au réseau mobile (ne fonctionne jamais, voir 2ème point)
+ * Sauvegarder le nombre de threads pour les prochains téléchargements
+ * Beaucoup d'incohérences corrigées
+
+### Corrigé
+* Correction d'un crash avec la résolution par défaut réglée sur la meilleure et la résolution limitée des données mobiles #1835
+* Correction du crash du lecteur de pop-up #1874
+* NPE lors de l'ouverture du lecteur de fond #1901
+* Correction de l'insertion de nouveaux flux lorsque la mise en file d'attente automatique est activée #1878
+* Correction du problème de décryptage de Shuttown
diff --git a/fastlane/metadata/android/fr/changelogs/71.txt b/fastlane/metadata/android/fr/changelogs/71.txt
index 0fa046111b4..4d1a5b1f69b 100644
--- a/fastlane/metadata/android/fr/changelogs/71.txt
+++ b/fastlane/metadata/android/fr/changelogs/71.txt
@@ -1,10 +1,10 @@
### Améliorations
-* Notification maj GitHub (#1608 par @krtkush)
-* Améliorations téléchargeur (#1944 par @kapodamy) :
- * icônes blanches manquantes ; utilisation d'une méthode pour changer leurs couleurs
- * vérification si l'itérateur est initialisé (#2031)
- * réessayer les téléchargements post-processing failed dans le nouveau muxer
- * nouveau muxer MPEG-4 corrigeant les flux non synchrones (#2039)
+* Notification maj GitHub #1608
+* Améliorations téléchargeur #1944 :
+ * Ajout des icônes blanches manquantes et utilisation d'une méthode hardcodé pour changer leurs couleurs
+ * Vérification si l'itérateur est initialisé (#2031)
+ * Autoriser le ré-essai de téléchargement après une erreur "post-processing failed" dans le nouveau muxer
+ * Nouveau muxer MPEG-4 corrigeant les flux non synchrones (#2039)
### Corrections
-* Flux YouTube en direct s'arrêtent (#1996 par @yausername)
+* Flux YouTube en direct s'arrêtent #1996
diff --git a/fastlane/metadata/android/fr/changelogs/964.txt b/fastlane/metadata/android/fr/changelogs/964.txt
new file mode 100644
index 00000000000..4d65340fccd
--- /dev/null
+++ b/fastlane/metadata/android/fr/changelogs/964.txt
@@ -0,0 +1,8 @@
+• Ajout des chapitres dans lecteur
+• [PeerTube] Ajout recherche en sépia
+• Ajout bouton de partage en vue détaillée de la vidéo, déplacement description du flux dans l'onglet
+• Désactivation restauration de luminosité si le geste est désactivé
+• Ajout élément de liste pour lire vidéos sur Kodi
+• Correction crash si aucun navigateur par défaut défini, amélioration dialogues de partage
+• Basculer lecture/pause avec bouton d'espace matériel en lecteur plein écran
+• [media.ccc.de] Corrections
diff --git a/fastlane/metadata/android/fr/changelogs/966.txt b/fastlane/metadata/android/fr/changelogs/966.txt
new file mode 100644
index 00000000000..7cbe82fbfc3
--- /dev/null
+++ b/fastlane/metadata/android/fr/changelogs/966.txt
@@ -0,0 +1,14 @@
+Nouveautés
+• Ajout Bandcamp
+
+Améliorations
+• Ajout option pour que application suive thème de l'appareil
+• Prévention plantages par affichage panneau d'erreurs amélioré
+• Plus d'informations sur raison indisponibilité contenu
+• Bouton matériel espace déclenche lecture/pause
+• Affichage toast "Téléchargement commencé"
+
+Corrections
+• Très petite vignette dans détails de vidéo lors de lecture en arrière-plan
+• Titre vide dans lecteur réduit
+• Dernier mode redimensionnement pas restauré correctement
diff --git a/fastlane/metadata/android/fr/changelogs/969.txt b/fastlane/metadata/android/fr/changelogs/969.txt
new file mode 100644
index 00000000000..6ac4a9467d6
--- /dev/null
+++ b/fastlane/metadata/android/fr/changelogs/969.txt
@@ -0,0 +1,8 @@
+• Autoriser installation sur un stockage externe
+• [Bandcamp] Ajout fonction permettant d'afficher les trois premiers commentaires d'un flux
+• Afficher 'download has started' uniquement lorsque téléchargement lancé
+• Ne pas définir cookie reCaptcha lorsqu'aucun n'est stocké
+• [Player] Amélioration performances cache
+• [Player] Correction problème lecture automatique
+• Désactiver barres d'état précédentes lors suppr. des téléchargements
+• Correction suppression objet ne figurant pas dans la liste
diff --git a/fastlane/metadata/android/fr/changelogs/970.txt b/fastlane/metadata/android/fr/changelogs/970.txt
new file mode 100644
index 00000000000..928d3782222
--- /dev/null
+++ b/fastlane/metadata/android/fr/changelogs/970.txt
@@ -0,0 +1,11 @@
+Nouveautés
+• Affichage métadonnées du contenu sous la description
+• Ajout option "Afficher les détails de la chaîne" dans les playlists distantes
+• Ajout option "Ouvrir dans le navigateur" dans le menu de la touche longue
+
+Corrections
+• Correction d'un crash de rotation sur la page de détails de la vidéo
+• Correction du bouton "Jouer avec Kodi" qui demande toujours d'installer Kore
+• Correction chemins d'import/export des paramètres
+• Correction nombre de commentaires aimés
+Et bien plus encore
diff --git a/fastlane/metadata/android/fr/changelogs/971.txt b/fastlane/metadata/android/fr/changelogs/971.txt
new file mode 100644
index 00000000000..3b302a06b1e
--- /dev/null
+++ b/fastlane/metadata/android/fr/changelogs/971.txt
@@ -0,0 +1,3 @@
+Correctifs
+• Augmentation de la mémoire tampon pour la lecture après le re-buffer
+• Correction d'un crash sur les tablettes et les téléviseurs lors d'un clic sur l'icône de la file d'attente dans le lecteur
diff --git a/fastlane/metadata/android/fr/changelogs/973.txt b/fastlane/metadata/android/fr/changelogs/973.txt
new file mode 100644
index 00000000000..667279399e3
--- /dev/null
+++ b/fastlane/metadata/android/fr/changelogs/973.txt
@@ -0,0 +1,4 @@
+Correctifs
+• Correction des vignettes et des titres qui sont coupés dans la mise en page en vue grille, dû à un calcul erroné du nombre de vidéos pouvant tenir dans une rangée.
+• Correction de la boîte de dialogue de téléchargement qui disparaît sans rien faire si elle est ouverte à partir du menu de partage
+• Maj d'une bibliothèque liée à l'ouverture d'activités externes telles que le sélecteur de fichiers du framewok d'accès stockage
diff --git a/fastlane/metadata/android/fr/changelogs/974.txt b/fastlane/metadata/android/fr/changelogs/974.txt
new file mode 100644
index 00000000000..d963abe2410
--- /dev/null
+++ b/fastlane/metadata/android/fr/changelogs/974.txt
@@ -0,0 +1,5 @@
+Correctifs
+• Correction des problèmes de mise en mémoire tampon causés par la restriction de débit de YouTube
+• Correction de l'extraction des commentaires de YouTube et des plantages avec les commentaires désactivés
+• Correction de la recherche de musique sur YouTube
+• Correction des directs PeerTube
diff --git a/fastlane/metadata/android/fr/changelogs/977.txt b/fastlane/metadata/android/fr/changelogs/977.txt
new file mode 100644
index 00000000000..6232ffa756d
--- /dev/null
+++ b/fastlane/metadata/android/fr/changelogs/977.txt
@@ -0,0 +1,8 @@
+• Ajout bouton "lecture suivante" au menu de la pression longue
+• Ajout préfixe du chemin des shorts YouTube au filtre d'intention
+• Correction importation des paramètres
+• Permutation position barre de recherche avec boutons du lecteur dans l'écran de la file d'attente
+• Corrections liées à MediasessionManager
+• Correction barre de progression qui ne se termine pas après fin de vidéo
+• Désactivation tunneling média sur RealtekATV
+• Élargissement zone cliquable des boutons de lecture minimisés
diff --git a/fastlane/metadata/android/fr/changelogs/980.txt b/fastlane/metadata/android/fr/changelogs/980.txt
new file mode 100644
index 00000000000..6835f70c8a3
--- /dev/null
+++ b/fastlane/metadata/android/fr/changelogs/980.txt
@@ -0,0 +1,13 @@
+Nouveautés
+• Ajout option "Ajouter à la liste de lecture" au menu de partage
+• Ajout prise en charge des liens courts y2u.be et PeerTube
+
+Améliorations
+• Commandes de vitesse de lecture plus compactes
+• Le flux met désormais en évidence les nouveaux éléments
+• L'option "Afficher les éléments surveillés" dans le flux est maintenant enregistrée
+
+Corrections
+• Correction extraction des likes/dislikes de YouTube
+• Correction relecture automatique après le retour de l'arrière-plan
+Et bien d'autres
diff --git a/fastlane/metadata/android/fr/changelogs/987.txt b/fastlane/metadata/android/fr/changelogs/987.txt
index e0e1bd7bd15..1641d9a0016 100644
--- a/fastlane/metadata/android/fr/changelogs/987.txt
+++ b/fastlane/metadata/android/fr/changelogs/987.txt
@@ -1,8 +1,8 @@
Nouveautés
-• Prise en charge de d'autres méthodes de diffusion que le HTTP progressif : temps de chargement plus rapide, corrections pour PeerTube et SoundCloud, lecture des nouveaux flux en directs de YouTube
-• Boutton pour ajouter une liste de lecture distante à une locale
+• Prise en charge d'autres méthodes de diffusion que le HTTP progressif : temps de chargement plus rapide, corrections pour PeerTube et SoundCloud, lecture des nouveaux flux en directs de YouTube
+• Bouton pour ajouter une liste de lecture distante à une locale
• Prévisualisation d'images lors d'un partage pour Andoid 10+
Améliorations
• Amélioration de la boîte de dialogue des paramètres de la lecture
-• Déplacement des bouttons importation/exportation vers le menu à trois points
+• Déplacement des boutons importation/exportation vers le menu à trois points
diff --git a/fastlane/metadata/android/fr/changelogs/988.txt b/fastlane/metadata/android/fr/changelogs/988.txt
new file mode 100644
index 00000000000..7ac03bb2ac2
--- /dev/null
+++ b/fastlane/metadata/android/fr/changelogs/988.txt
@@ -0,0 +1,2 @@
+[YouTube] Correction de l'erreur « Impossible d'obtenir un flux » lors de la lecture d'une vidéo
+[YouTube] Correction du message « Le contenu suivant n'est pas disponible sur cette application. » affiché à la place de la vidéo demandée
diff --git a/fastlane/metadata/android/fr/changelogs/989.txt b/fastlane/metadata/android/fr/changelogs/989.txt
new file mode 100644
index 00000000000..6d41c1b7862
--- /dev/null
+++ b/fastlane/metadata/android/fr/changelogs/989.txt
@@ -0,0 +1,3 @@
+• [YouTube] Correction du chargement infini lors de la lecture d'une vidéo
+• [YouTube] Correction de l'accélération de certaines vidéos
+• Mise à jour 1.15.3 de la bibliothèque jsoup contenant une correction de sécurité
diff --git a/fastlane/metadata/android/fr/changelogs/990.txt b/fastlane/metadata/android/fr/changelogs/990.txt
new file mode 100644
index 00000000000..fcab79c3c95
--- /dev/null
+++ b/fastlane/metadata/android/fr/changelogs/990.txt
@@ -0,0 +1,15 @@
+Cette mise à jour abandonne la prise en charge d'Android 4.4 KitKat, la nouvelle version minimum est Android 5 Lollipop !
+
+Nouveautés
+• Télécharger depuis le menu d'appuis long
+• Cacher les futures vidéos dans le flux
+• Partager des listes de lecture locales
+
+Améliorations
+• Réusinage du code du lecteur en petits composants : moins de mémoire vive utilisée, moins de bogues
+• Meilleur redimension des miniatures
+• Vectorisation des emplacements des images
+
+Corrections
+• Notifications
+• Plein écran
diff --git a/fastlane/metadata/android/fr/short_description.txt b/fastlane/metadata/android/fr/short_description.txt
index a593ce32c93..70048c15a31 100644
--- a/fastlane/metadata/android/fr/short_description.txt
+++ b/fastlane/metadata/android/fr/short_description.txt
@@ -1 +1 @@
-Un lecteur multimédia libre et léger pour Android.
+Une interface pour YouTube libre et légère sur Android.
diff --git a/fastlane/metadata/android/he/changelogs/988.txt b/fastlane/metadata/android/he/changelogs/988.txt
new file mode 100644
index 00000000000..fc69457f787
--- /dev/null
+++ b/fastlane/metadata/android/he/changelogs/988.txt
@@ -0,0 +1,2 @@
+[YouTube] תוקנה השגיאה „אי אפשר לקבל שום תזרים” בעת ניסיון לנגן סרטונים
+[YouTube] תוקנה ההודעה „התוכן הבא אינו זמין ביישומון הזה” שמופיעה במקום הסרטון המבוקש
diff --git a/fastlane/metadata/android/hi/short_description.txt b/fastlane/metadata/android/hi/short_description.txt
index 9e2aff56766..3d76fa533a1 100644
--- a/fastlane/metadata/android/hi/short_description.txt
+++ b/fastlane/metadata/android/hi/short_description.txt
@@ -1 +1 @@
-एंड्रॉयड के लिए एक मुफ्त हल्का यूट्यूब फ्रंटएंड।
+एंड्रॉयड के लिए एक मुफ्त लाइट यूट्यूब फ्रंटएंड।
diff --git a/fastlane/metadata/android/hu/changelogs/65.txt b/fastlane/metadata/android/hu/changelogs/65.txt
new file mode 100644
index 00000000000..c3ee63eccdb
--- /dev/null
+++ b/fastlane/metadata/android/hu/changelogs/65.txt
@@ -0,0 +1,26 @@
+### Fejlesztések
+
+- A burgermenu ikon animációjának letiltása #1486
+- a letöltések törlésének visszavonása #1472
+- Letöltési lehetőség a #1498 megosztás menüben
+- Megosztási lehetőség hozzáadva a hosszú érintéssel #1454
+- A fő játékos minimalizálása a 1354-es kijáratnál
+- A könyvtár verziójának frissítése és az adatbázis biztonsági mentésének javítása #1510
+- ExoPlayer 2.8.2 frissítés #1392
+ - Átdolgoztuk a lejátszási sebesség-vezérlő párbeszédpanelt, hogy támogassa a különböző lépésméreteket a gyorsabb sebességváltás érdekében.
+ - Hozzáadott egy kapcsolót a gyors előretekeréshez a lejátszási sebesség szabályozásában a csendek alatt. Ez hasznos lehet hangoskönyvek és bizonyos zenei műfajok esetében, és valódi zökkenőmentes élményt nyújthat (és megszakíthat egy dalt sok csenddel =\\).
+ - Átdolgozott médiaforrás felbontás, amely lehetővé teszi a metaadatok továbbítását a média mellett a lejátszón belül, nem pedig manuálisan. Most már egyetlen metaadatforrásunk van, és közvetlenül elérhető a lejátszás megkezdésekor.
+ - Javítva a távoli lejátszási lista metaadatai, amelyek nem frissülnek, amikor új metaadatok állnak rendelkezésre a lejátszási lista töredékének megnyitásakor.
+ - Különféle felhasználói felület-javítások: #1383, a háttérben lévő lejátszó értesítési vezérlői mostantól mindig fehérek, a felugró lejátszót egyszerűbben le lehet állítani dobással
+- Használjon új kivonatot refaktorált architektúrával a többszolgáltatáshoz
+
+### Javítások
+
+- Javítás: #1440 Sérült videó információs elrendezés #1491
+- Előzmények megtekintése #1497. javítás
+ - #1495, a metaadatok (bélyegkép, cím és videószám) frissítésével, amint a felhasználó hozzáfér a lejátszási listához.
+ - #1475, egy nézet regisztrálásával az adatbázisban, amikor a felhasználó elindít egy videót a külső lejátszón a részletrészleten.
+- Javítsa ki a képernyő időtúllépését felugró mód esetén. #1463 (fix #640)
+- Fő videólejátszó javítás #1509
+ - [#1412] Javítva az ismétlési mód, ami a játékos NPE-jét okozza, ha új szándék érkezik, miközben a játékos tevékenysége a háttérben van.
+ - Javítva, hogy a lejátszó előugró ablakra minimalizálja, nem semmisíti meg a lejátszót ha a popup engedélyt nem adják meg.
diff --git a/fastlane/metadata/android/hu/short_description.txt b/fastlane/metadata/android/hu/short_description.txt
index 0a96f5e90a1..50752eeaf35 100644
--- a/fastlane/metadata/android/hu/short_description.txt
+++ b/fastlane/metadata/android/hu/short_description.txt
@@ -1 +1 @@
-Egy ingyenes és könnyű YouTube előtétprogram Androidra.
+Ingyenes, könnyű YouTube felület Androidra.
diff --git a/fastlane/metadata/android/it/changelogs/63.txt b/fastlane/metadata/android/it/changelogs/63.txt
index dd8f7324dc7..a342392ad99 100644
--- a/fastlane/metadata/android/it/changelogs/63.txt
+++ b/fastlane/metadata/android/it/changelogs/63.txt
@@ -1,8 +1,8 @@
### Miglioramenti
-- Impostazioni di importazione / esportazione # 1333
-- Ridotto l'overdraw (miglioramento delle prestazioni) # 1371
-- Piccoli miglioramenti al codice # 1375
-- Aggiunto tutto ciò che riguarda il GDPR # 1420
+- Impostazioni di importazione / esportazione #1333
+- Ridotto l'overdraw (miglioramento delle prestazioni) #1371
+- Piccoli miglioramenti al codice #1375
+- Aggiunto tutto ciò che riguarda il GDPR #1420
### Risolto
-- Downloader: risolto il crash durante il caricamento di download incompleti dai file .giga # 1407
+- Downloader: risolto il crash durante il caricamento di download incompleti dai file .giga #1407
diff --git a/fastlane/metadata/android/it/changelogs/730.txt b/fastlane/metadata/android/it/changelogs/730.txt
new file mode 100644
index 00000000000..3df41556f08
--- /dev/null
+++ b/fastlane/metadata/android/it/changelogs/730.txt
@@ -0,0 +1,2 @@
+# Risolto
+- Sistemato di nuovo un errore nella funzione di decifrazione.
diff --git a/fastlane/metadata/android/ko/changelogs/63.txt b/fastlane/metadata/android/ko/changelogs/63.txt
new file mode 100644
index 00000000000..69ca21e9733
--- /dev/null
+++ b/fastlane/metadata/android/ko/changelogs/63.txt
@@ -0,0 +1,8 @@
+### 변경점
+- 불러오기/내보내기 세팅 #1333
+- 오버드로우 현상 개선 (성능 개선) #1371
+- 코드 일부분 개선 #1375
+- GDPR에 관한 모든것 업데이트 #1420
+
+### 해결된것
+- 다운로더 : 다운로드가 완료되지 않은 .giga파일을 로딩할때 발생하는 에러 해결#1407
diff --git a/fastlane/metadata/android/ko/changelogs/64.txt b/fastlane/metadata/android/ko/changelogs/64.txt
new file mode 100644
index 00000000000..4cef85a41cd
--- /dev/null
+++ b/fastlane/metadata/android/ko/changelogs/64.txt
@@ -0,0 +1,8 @@
+### 변경점
+- 불러오기/내보내기 세팅 #1333
+- 오버드로우 현상 개선 (성능 개선) #1371
+- 코드 일부분 개선 #1375
+- GDPR에 관한 모든것 업데이트 #1420
+
+### 고친것
+- 다운로더 : 다운로드가 완료되지 않은 .giga파일을 로딩할때 발생하는 문제 해결#1407
diff --git a/fastlane/metadata/android/pl/changelogs/964.txt b/fastlane/metadata/android/pl/changelogs/964.txt
new file mode 100644
index 00000000000..1684d6985e2
--- /dev/null
+++ b/fastlane/metadata/android/pl/changelogs/964.txt
@@ -0,0 +1,8 @@
+• Dodano wsparcie rozdziałów w kontrolkach odtwarzacza
+• [PeerTube] Dodano wyszukiwarkę Sepia
+• Dodano ponownie przycisk udostępniania w sekcji szczegółów i przeniesiono informacje o strumieniu do układu karty
+• Wyłączono przywracanie jasności przy wyłączonych gestach regulacji jasności
+• Dodano element listy umożliwiający odtworzenie wideo w Kodi
+• Naprawiono błąd przy braku domyślnej przeglądarki na niektórych urządzeniach i usprawniono menu udostępniania
+• Przełączanie odtwarzanie/pauza poprzez wciśnięcie spacji na klawiaturze fizycznej w odtwarzaczu pełnoekranowym
+• [media.cc.de] Różne poprawki i usprawnienia
diff --git a/fastlane/metadata/android/pl/changelogs/965.txt b/fastlane/metadata/android/pl/changelogs/965.txt
new file mode 100644
index 00000000000..d336d984d54
--- /dev/null
+++ b/fastlane/metadata/android/pl/changelogs/965.txt
@@ -0,0 +1,5 @@
+Naprawiono błąd przy zmianie kolejności grup kanałów.
+Naprawiono pobieranie kolejnych wideo z kanałów i playlist.
+Naprawiono pobieranie komentarzy w YouTube. Dodano wsparcie dla ścieżek /watch/, /v/ oraz /w/ w URL-ach YouTube.
+Naprawiono pobieranie ID użytkownika SoundCloud i zawartości z ograniczeniami geograficznymi.
+Dodano język północnokurdyjski.
diff --git a/fastlane/metadata/android/pl/changelogs/990.txt b/fastlane/metadata/android/pl/changelogs/990.txt
new file mode 100644
index 00000000000..4c292432b18
--- /dev/null
+++ b/fastlane/metadata/android/pl/changelogs/990.txt
@@ -0,0 +1,15 @@
+To wydanie znosi wsparcie dla Androida 4.4 KitKat, teraz min. wersja to Android 5 Lollipop!
+
+Nowe
+• Pobier. z menu długiego naciśnięcia
+• Ukryw. przyszłych wideo w kanale
+• Udostęp. lokalnych playlist
+
+Ulepszone
+• Refaktor. kodu odtwarzacza: mniejsze zużycie RAM-u, mniej błędów
+• Skalowanie miniatur
+• Wektoryzacja obrazków zastępczych
+
+Naprawione
+• Różne problemy z powiadomieniem odtwarzacza: nieaktualne/brakujące info o multimediach, zniekształcona miniatura
+• Pełny ekran zajmujący 1/4 ekranu
diff --git a/fastlane/metadata/android/pt/changelogs/65.txt b/fastlane/metadata/android/pt/changelogs/65.txt
new file mode 100644
index 00000000000..89a006829d9
--- /dev/null
+++ b/fastlane/metadata/android/pt/changelogs/65.txt
@@ -0,0 +1,26 @@
+### Melhorias
+
+- Desativar a animação do ícone do burgermenu #1486
+- Desfazer a eliminação de descarregamentos #1472
+- Opção de descarregamento no menu de partilha #1498
+- Opção de partilha adicionada ao menu de toque longo #1454
+- Minimize o jogador principal na saída #1354
+- Atualização da versão da biblioteca e correção de cópia de segurança da base de dados #1510
+- ExoPlayer 2.8.2 Atualização #1392
+- Retrabalhado a caixa de diálogo de controlo de velocidade de reprodução para suportar diferentes tamanhos de etapa para uma mudança de velocidade mais rápida.
+- Adicionado uma alternância para avanço rápido durante silêncios no controle de velocidade de reprodução. Isso deve ser útil para audiolivros e certos géneros musicais, e pode trazer uma experiência verdadeiramente perfeita (e pode quebrar uma música com muitos silêncios =\\).
+- Resolução de fonte de média ré fatorada para permitir a passagem de metadados junto com a média internamente no reprodutor, em vez de fazê-lo manualmente. Agora temos uma única fonte de metadados e está disponível diretamente quando a reprodução é iniciada.
+- Correção de metadados de listas de reprodução remotas que não são atualizadas quando novos metadados estão disponíveis quando o fragmento da lista de reprodução é aberta.
+- Várias correções de interface do utilizador : #1383, controles de notificação do reprodutor em segundo plano agora sempre brancos, mais fácil de desligar o reprodutor pop-up por meio de arremesso
+- Use novo extrator com arquitetura ré fatorada para multisserviço
+
+### Conserta
+
+- Correção #1440 Layout de informações de vídeo quebrado #1491
+-Ver correção de histórico #1497
+- #1495, atualizando os metadados (miniatura, título e contagem de vídeos) assim que o usuário acessar a lista de reprodução.
+- #1475, registando uma visualização na base de dados quando o utilizador inicia um vídeo no reprodutor externo no fragmento de detalhes.
+- Correção de tempo limite de criação em caso de modo pop-up. #1463 (Corrigido #640)
+- Correção do reprodutor de vídeo principal #1509
+- [#1412] Corrigido o modo de repetição causando NPE do reprodutor quando uma nova intenção é recebida enquanto a atividade do reprodutor está em segundo plano.
+- Corrigida a minimização de reprodutor para pop-up não destrói o reprodutor quando a permissão de pop-up não é concedida.
diff --git a/fastlane/metadata/android/pt/changelogs/955.txt b/fastlane/metadata/android/pt/changelogs/955.txt
index cd70b41c9ba..98bed58fecd 100644
--- a/fastlane/metadata/android/pt/changelogs/955.txt
+++ b/fastlane/metadata/android/pt/changelogs/955.txt
@@ -1,3 +1,3 @@
-[YouTube] A procura por alguns utilizadores corrigida
-[YouTube] Exceções de desencriptação aleatórias corrigidas
-[SounCloud] URLs que terminam com uma barra são agora analisados corretamente
+[YouTube] O problema com busca que afetava utilizadores foi corrigida
+[YouTube] Exceções de desencriptação aleatórias foram corrigidas
+[SounCloud] URLs que terminam com uma barra são analisadas corretamente
diff --git a/fastlane/metadata/android/ru/changelogs/65.txt b/fastlane/metadata/android/ru/changelogs/65.txt
new file mode 100644
index 00000000000..51993fef336
--- /dev/null
+++ b/fastlane/metadata/android/ru/changelogs/65.txt
@@ -0,0 +1 @@
+эскиз видео
diff --git a/fastlane/metadata/android/ru/changelogs/66.txt b/fastlane/metadata/android/ru/changelogs/66.txt
new file mode 100644
index 00000000000..51993fef336
--- /dev/null
+++ b/fastlane/metadata/android/ru/changelogs/66.txt
@@ -0,0 +1 @@
+эскиз видео
diff --git a/fastlane/metadata/android/ru/changelogs/68.txt b/fastlane/metadata/android/ru/changelogs/68.txt
new file mode 100644
index 00000000000..51993fef336
--- /dev/null
+++ b/fastlane/metadata/android/ru/changelogs/68.txt
@@ -0,0 +1 @@
+эскиз видео
diff --git a/fastlane/metadata/android/ru/changelogs/69.txt b/fastlane/metadata/android/ru/changelogs/69.txt
new file mode 100644
index 00000000000..c081690f841
--- /dev/null
+++ b/fastlane/metadata/android/ru/changelogs/69.txt
@@ -0,0 +1 @@
+настройки
diff --git a/fastlane/metadata/android/ru/changelogs/70.txt b/fastlane/metadata/android/ru/changelogs/70.txt
new file mode 100644
index 00000000000..da96c42fda6
--- /dev/null
+++ b/fastlane/metadata/android/ru/changelogs/70.txt
@@ -0,0 +1 @@
+всплывающий
diff --git a/fastlane/metadata/android/ru/changelogs/780.txt b/fastlane/metadata/android/ru/changelogs/780.txt
new file mode 100644
index 00000000000..24cfa6e7fbe
--- /dev/null
+++ b/fastlane/metadata/android/ru/changelogs/780.txt
@@ -0,0 +1,12 @@
+Изменения в 0.17.3
+
+Улучшено
+• Добавлена возможность очистки состояний воспроизведения #2550
+• Показ скрытых каталогов в средстве выбора файлов #2591
+• Поддержка URL-адресов из экземпляров `invidio.us`, открываемых с помощью NewPipe #2488
+• Добавлена поддержка URL-адресов `music.youtube.com` TeamNewPipe/NewPipeExtractor #194
+
+Исправлено
+• [YouTube] Исправлена ошибка java.lang.IllegalArgumentException #192
+• [YouTube] Исправлены неработающие прямые трансляции TeamNewPipe/NewPipeExtractor#195
+• Исправлена проблема с производительностью в Android Pie при загрузке потока #2592
diff --git a/fastlane/metadata/android/ru/changelogs/790.txt b/fastlane/metadata/android/ru/changelogs/790.txt
new file mode 100644
index 00000000000..24d1c115af0
--- /dev/null
+++ b/fastlane/metadata/android/ru/changelogs/790.txt
@@ -0,0 +1 @@
+папки
diff --git a/fastlane/metadata/android/ru/changelogs/985.txt b/fastlane/metadata/android/ru/changelogs/985.txt
new file mode 100644
index 00000000000..d3978869d59
--- /dev/null
+++ b/fastlane/metadata/android/ru/changelogs/985.txt
@@ -0,0 +1 @@
+Исправлено: YouTube не воспроизводил никакие потоки
diff --git a/fastlane/metadata/android/ru/changelogs/987.txt b/fastlane/metadata/android/ru/changelogs/987.txt
new file mode 100644
index 00000000000..8e97023650f
--- /dev/null
+++ b/fastlane/metadata/android/ru/changelogs/987.txt
@@ -0,0 +1,12 @@
+Новое
+• Поддержка методов доставки, отличных от прогрессивного HTTP: ускорение времени загрузки воспроизведения, исправления PeerTube и SoundCloud, воспроизведение недавно закончившихся трансляций YouTube
+• Кнопка «Добавить», чтобы добавить удаленный плейлист к локальному
+• Предпросмотр изображения на странице общего доступа Android 10+
+
+Улучшено
+• Улучшения окна параметров воспроизведения
+• Перемещение кнопки импорта/экспорта подписки в трехточечное меню
+
+Исправлено
+• Исправлено удаление полностью просмотренных видео из плейлиста
+• Исправлена тема меню «Поделиться» и пункт «Добавить в плейлист»
diff --git a/fastlane/metadata/android/sk/changelogs/987.txt b/fastlane/metadata/android/sk/changelogs/987.txt
new file mode 100644
index 00000000000..45b2d85dde8
--- /dev/null
+++ b/fastlane/metadata/android/sk/changelogs/987.txt
@@ -0,0 +1,12 @@
+Novinky
+• Poskytovanie iné než progresívne HTTP: zrýchlené načítanie prehrávania, opravy PeerTube a SoundCloud, prehrávanie nedávno ukončených livestreamov YouTube
+• Tlačidlo na pridanie vzdialeného zoznamu k lokálnemu
+• Náhľad obrázka v hárku zdieľania v Android 10+
+
+Vylepšenia
+• Dialógové okno parametrov prehrávania
+• Presunuté tlačidlá import/export odberov do ○○○ menu
+
+Opravy
+• Odstraňovanie dokončených videí zo zoznamu videí
+• Téma v menu zdieľania a položky „pridať do zoznamu skladieb“
diff --git a/fastlane/metadata/android/sv/changelogs/65.txt b/fastlane/metadata/android/sv/changelogs/65.txt
index 89c12c7d29f..74d0a2d7013 100644
--- a/fastlane/metadata/android/sv/changelogs/65.txt
+++ b/fastlane/metadata/android/sv/changelogs/65.txt
@@ -2,11 +2,23 @@
- Stängde av burgarmeny ikonens animation #1486
- Ångra radering av nedladdningar #1472
- Nedladdningsalternativ i delningsmenyn #1498
-- La till delningsalternativet i menyn för långa tryckningar #1454
-- Och mer...
+- Lade till delningsalternativet i menyn för långa tryckningar #1454
+- Minimerar huvudsplearen vid avslut #1354
+- Uppdatering av biblioteksversion samt åtgärd av databasbackup #1510
+- Uppdatering av ExoPlayer 2.8.2 #1392
+ - Omarbetad kontroll för uppspelningshastighet för att stödja olika stegstorlekar för snabbare hastighetsändring.
+ - Lade till växelkontroll för att snabbspola vid tystnad i uppspelningens hastighetskontroll. Detta borde vara underlätta vid uppspelning av ljudböcker och vissa musikgenres och kan bidra till en sömlös upplevelse ( och kan pajja en låt med massa tystnad =\\).
+ - Omskrivning av källmedias upplösning för att tillåta samtidig rörelse av metadata internt i spelaren, hellre än att utföra detta manuellt. Nu finns endast en källa för metadata som är omedelbart tillgängig så snart uppspelning sker.
+ - Åtgärdat att fjärrspellistors metadata inte uppdateras när nytt metadata är tillgänligt vid öppning av spelliststdelar.
+ - Diverse åtgärder av användargränssnitt: #1383, aviseringar för bakgrundsspelaren är nu alltid vita, lättare att stänga popup-spelare via "flinging"
+- Nyttja ny extraherare med omskriven arkitektur för stöd av flera tjänster
-### Fixade
-- Fixade #1440 Trasig layout för videoinformation #1491
+### Åtgärdade
+- Åtgärdade #1440 Trasig layout för videoinformation #1491
- Visningshistorik fix #1497
-- #1495, genom att uppdatera metadata (miniatyrbild, titel och videoantal) så snart användaren får tillgång till spellistan. - #1475
-- Och mer...
+- #1495, genom att uppdatera metadata (miniatyrbild, titel och videoantal) så snart användaren får tillgång till spellistan.
+ - #1475, genom att skapa en vy i databasen när användaren startar en video i extern spelare för detaljfragment.
+- Åtgärdade tidsgräns för fönster som är i popup-läge. #1463 (Fixed #640)
+- Åtgärd av primär videospelare #1509
+ - [#1412] Åtgärdade upprepningsläge vilket orsakade "null-pointer-exception" i spelaren när ny avsikt mottas för spelare som arbetar i bakgrunden.
+ - Åtgärdade att spelare utan popup-behörighet inte kraschar vid minimering till popupstorlek av fönstret.
diff --git a/fastlane/metadata/android/sv/changelogs/969.txt b/fastlane/metadata/android/sv/changelogs/969.txt
index a9ecf6b67a1..32725cc53c6 100644
--- a/fastlane/metadata/android/sv/changelogs/969.txt
+++ b/fastlane/metadata/android/sv/changelogs/969.txt
@@ -1,13 +1,8 @@
-• Tillåt installation på extern lagring
-
+• Tillåt installation på extern lagringsenhet
• [Bandcamp] Stöd för att visa de tre första kommentarerna i en stream har lagts till.
-
• Visa endast "nedladdning har börjat" när nedladdningen har påbörjats.
-
• Ställ inte in reCaptcha-cookie när det inte finns någon cookie lagrad.
-
-• Player] Förbättra prestanda för cache
-
+• [Player] Förbättra prestanda för cache
+• [Player] Åtgärdat ej automatisk uppspelning
• Avskaffa tidigare Snackbars när nedladdningar raderas
-
-• Fixat att försöka radera objekt som inte finns i listan
+• Åtgärdat försök att radera objekt som inte finns i listan
diff --git a/fastlane/metadata/android/sv/changelogs/985.txt b/fastlane/metadata/android/sv/changelogs/985.txt
index 4c434af5448..35f298dbff8 100644
--- a/fastlane/metadata/android/sv/changelogs/985.txt
+++ b/fastlane/metadata/android/sv/changelogs/985.txt
@@ -1 +1 @@
-Fixade att YouTube inte spelade någon stream.
+Åtgärdat att YouTube inte spelar någon stream
diff --git a/fastlane/metadata/android/tr/changelogs/63.txt b/fastlane/metadata/android/tr/changelogs/63.txt
index b4ccdf68a82..9370c537a89 100644
--- a/fastlane/metadata/android/tr/changelogs/63.txt
+++ b/fastlane/metadata/android/tr/changelogs/63.txt
@@ -1,8 +1,8 @@
### Geliştirmeler
- İçe/Dışa aktarma ayarları #1333
-- Aşmalar azaltıldı(performance iyileştirmeleri) #1371
+- Aşmalar azaltıldı(performans iyileştirmeleri) #1371
- Küçük kod iyileştirmeleri #1375
- GPDR hakkında her şey eklendi #1420
### Düzeltildi
-- İndirici: .giga dosyalarından bitmeyen indirmeler yüklenirken çökmeler düzeltildi #1407
+- İndirici: .giga dosyalarından bitmemiş indirmeler yüklenirken çökmeler düzeltildi #1407
diff --git a/fastlane/metadata/android/tr/full_description.txt b/fastlane/metadata/android/tr/full_description.txt
index e6ca6f1b468..11daef85b63 100644
--- a/fastlane/metadata/android/tr/full_description.txt
+++ b/fastlane/metadata/android/tr/full_description.txt
@@ -1 +1,2 @@
-NewPipe herhangi bir Google çerçeve kütüphanesi veya YouTube API'si kullanmaz. Gereksindiği bilgileri edinirken yalnızca web sitesini ayrıştırır. Bu nedenle Google hizmetlerinin kurulmadığı aygıtlarda kullanılabilir. Ayrıca, NewPipe'ı kullanırken YouTube hesabına gereksinmezsiniz, özgür ve açık kaynaklı yazılımdır.
+NewPipe herhangi bir Google çerçeve kütüphanesi veya YouTube API'ı kullanmaz. Sadece, ihtiyaç duyduğu bilgiyi edinmek için web sitesini ayrıştırır.
+Bu nedenle Google hizmetlerinin kurulmadığı aygıtlarda kullanılabilir. Ayrıca, NewPipe'ı kullanırken YouTube hesabına ihtiyacınız yok, ve bu özgür ve açık kaynaklı bir yazılımdır.
diff --git a/fastlane/metadata/android/uk/changelogs/988.txt b/fastlane/metadata/android/uk/changelogs/988.txt
new file mode 100644
index 00000000000..d882d430ab9
--- /dev/null
+++ b/fastlane/metadata/android/uk/changelogs/988.txt
@@ -0,0 +1,2 @@
+[YouTube] Виправлено помилку «Не вдалося отримати жодного потоку» під час спроби відтворити будь-яке відео
+[YouTube] Виправлено «Цей вміст недоступний у цьому застосунку.» замість запитаного відео
diff --git a/fastlane/metadata/android/uk/changelogs/989.txt b/fastlane/metadata/android/uk/changelogs/989.txt
new file mode 100644
index 00000000000..2ccd1653ae3
--- /dev/null
+++ b/fastlane/metadata/android/uk/changelogs/989.txt
@@ -0,0 +1,3 @@
+• [YouTube] Виправлено нескінченне завантаження за спроби відтворити будь-яке відео
+• [YouTube] Виправлено тротлінг на деяких відео
+• Оновлено бібліотеку jsoup до версії 1.15.3, яка включає виправлення безпеки
diff --git a/fastlane/metadata/android/uk/changelogs/990.txt b/fastlane/metadata/android/uk/changelogs/990.txt
new file mode 100644
index 00000000000..bc1d0a12c84
--- /dev/null
+++ b/fastlane/metadata/android/uk/changelogs/990.txt
@@ -0,0 +1,15 @@
+Припинено підтримку Android 4.4 KitKat, тепер найнижча версія — Android 5 Lollipop!
+
+Нове
+• Завантаження з меню при затисненні
+• Ховання майбутніх відео в стрічці
+• Поширення локальних добірок
+
+Поліпшено
+• Код поділено на компоненти: менше використання пам'яті, менше вад
+• Удосконалено режим масштабування мініатюр
+• Замінено картинки-заглушки на векторні
+
+Виправлено
+• Сповіщення: застарілі/відсутні дані про медіафайл, викривлену мініатюру
+• Використання повноекранним режимом лише чверті екрана
diff --git a/fastlane/metadata/android/zh-Hans/changelogs/988.txt b/fastlane/metadata/android/zh-Hans/changelogs/988.txt
new file mode 100644
index 00000000000..67f53fc8fc3
--- /dev/null
+++ b/fastlane/metadata/android/zh-Hans/changelogs/988.txt
@@ -0,0 +1,2 @@
+[YouTube] 修复 试图播放任何视频时,显示"无法获得任何流 "
+[YouTube] 修复 显示"以下内容在此应用中不可用 ",而不是所需要的视频
diff --git a/fastlane/metadata/android/zh-Hans/changelogs/989.txt b/fastlane/metadata/android/zh-Hans/changelogs/989.txt
new file mode 100644
index 00000000000..0c89cb98657
--- /dev/null
+++ b/fastlane/metadata/android/zh-Hans/changelogs/989.txt
@@ -0,0 +1,3 @@
+- [YouTube] 修复 尝试播放任何视频时无限加载
+- [YouTube] 修复 某些视频的节流问题
+- 将jsoup库升级到1.15.3,其中包括一个安全修复
diff --git a/fastlane/metadata/android/zh-Hans/changelogs/990.txt b/fastlane/metadata/android/zh-Hans/changelogs/990.txt
new file mode 100644
index 00000000000..2b3886a0a09
--- /dev/null
+++ b/fastlane/metadata/android/zh-Hans/changelogs/990.txt
@@ -0,0 +1,15 @@
+此版本移除了对 Android 4.4 KitKat 的支持,现在支持的最低版本是 Android 5 Lollipop!
+
+新增
+• 在长按菜单中进行下载
+• 隐藏 Feed 中的未来视频
+• 分享本地播放列表
+
+改进
+• 重构播放器代码成多个小组件:使用的内存更少了,BUG 更少了
+• 改进了缩略图的缩放方式
+• 矢量化了占位图片
+
+修复
+• 修复播放器通知相关的几个问题:媒体信息过期或缺失,缩略图扭曲变形
+• 修复全屏模式仅使用了 1/4 屏幕
diff --git a/fastlane/metadata/android/zh-Hant/changelogs/988.txt b/fastlane/metadata/android/zh-Hant/changelogs/988.txt
new file mode 100644
index 00000000000..684c8844471
--- /dev/null
+++ b/fastlane/metadata/android/zh-Hant/changelogs/988.txt
@@ -0,0 +1,2 @@
+[YouTube] 修正嘗試播放任何影片時「無法取得任何串流」的錯誤
+[YouTube] 修正請求影片時顯示「以下內容不在此應用程式中可用」的訊息
diff --git a/fastlane/metadata/android/zh-Hant/changelogs/989.txt b/fastlane/metadata/android/zh-Hant/changelogs/989.txt
new file mode 100644
index 00000000000..fdf05181a2a
--- /dev/null
+++ b/fastlane/metadata/android/zh-Hant/changelogs/989.txt
@@ -0,0 +1,3 @@
+• [YouTube] 修正嘗試播放任何影片時無盡載入
+• [YouTube] 修正部分影片限速
+• 升級 jsoup 程式庫至 1.15.3,當中包含一項安全修正
diff --git a/fastlane/metadata/android/zh-Hant/changelogs/990.txt b/fastlane/metadata/android/zh-Hant/changelogs/990.txt
new file mode 100644
index 00000000000..3260f395e6a
--- /dev/null
+++ b/fastlane/metadata/android/zh-Hant/changelogs/990.txt
@@ -0,0 +1,15 @@
+是次發布終止支援 Android 4.4 KitKat,最低版本現為 Android 5 Lollipop!
+
+新增
+• 長按功能表中的下載選項
+• 摘要隱藏未到時候的影片
+• 分享本機播放清單
+
+改進
+• 重構播放器程式碼為若干小元件:佔用較少 RAM,出現較少錯誤
+• 改進縮圖的縮放模式
+• 向量化預留位置影像
+
+修正
+• 修正播放器通知的若干問題:媒體資訊過時/欠奉、縮圖失真
+• 修正全螢幕僅佔用 1/4 畫面
diff --git a/fastlane/metadata/android/zh_Hant_HK/changelogs/988.txt b/fastlane/metadata/android/zh_Hant_HK/changelogs/988.txt
new file mode 100644
index 00000000000..bcc006d34c9
--- /dev/null
+++ b/fastlane/metadata/android/zh_Hant_HK/changelogs/988.txt
@@ -0,0 +1,2 @@
+[YouTube] 修正播咩片都「攞唔到任何串流」嘅問題
+[YouTube] 修正出現「呢部內容喺呢個 app 欠奉」嘅訊息,睇唔到請求嘅影片
diff --git a/fastlane/metadata/android/zh_Hant_HK/changelogs/989.txt b/fastlane/metadata/android/zh_Hant_HK/changelogs/989.txt
new file mode 100644
index 00000000000..296c2ada402
--- /dev/null
+++ b/fastlane/metadata/android/zh_Hant_HK/changelogs/989.txt
@@ -0,0 +1,3 @@
+• [YouTube] 執返好播咩片都係噉轉 lo 唔到
+• [YouTube] 執返好有啲片窒下窒下
+• 將 jsoup 程式庫升級做 1.15.3,包括修正一個保安問題
diff --git a/fastlane/metadata/android/zh_Hant_HK/changelogs/990.txt b/fastlane/metadata/android/zh_Hant_HK/changelogs/990.txt
new file mode 100644
index 00000000000..a91ea0e6793
--- /dev/null
+++ b/fastlane/metadata/android/zh_Hant_HK/changelogs/990.txt
@@ -0,0 +1,15 @@
+今次版本要扔低 Android 4.4 KitKat 啦,而家起最起碼要 Android 5 Lollipop 至裝到呢個 app!
+
+新嘢
+• 撳實有得揀下載
+• 摘要飛起未夠鐘上畫嘅片
+• 分享本機嘅播放清單
+
+進步
+• 翻新播放器程式碼劏做幾部細件:用少啲 RAM、冇咁多 bug
+• 錶起啲縮圖嘅時候擺得靚仔啲
+• 啲楔位公仔轉做向量圖
+
+執漏
+• 修正播放器通知嘅問題:多媒體資訊過時/留空、縮圖鬆郁矇
+• 修正全螢幕用得 1/4 個螢幕
diff --git a/gradle.properties b/gradle.properties
index 76b51ef0bb0..032d70cee99 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -1,4 +1,4 @@
-android.enableJetifier=true
+android.enableJetifier=false
android.useAndroidX=true
org.gradle.jvmargs=-Xmx2048M
systemProp.file.encoding=utf-8
diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar
index 41d9927a4d4..249e5832f09 100644
Binary files a/gradle/wrapper/gradle-wrapper.jar and b/gradle/wrapper/gradle-wrapper.jar differ
diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties
index 4ed3bdea443..5116c5b1869 100644
--- a/gradle/wrapper/gradle-wrapper.properties
+++ b/gradle/wrapper/gradle-wrapper.properties
@@ -1,6 +1,6 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions/gradle-7.4.2-all.zip
-distributionSha256Sum=e6d864e3b5bc05cc62041842b306383fc1fefcec359e70cebb1d470a6094ca82
+distributionUrl=https\://services.gradle.org/distributions/gradle-7.5.1-bin.zip
+distributionSha256Sum=f6b8596b10cce501591e92f229816aa4046424f3b24d771751b06779d58c8ec4
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
diff --git a/gradlew b/gradlew
index 1b6c787337f..a69d9cb6c20 100755
--- a/gradlew
+++ b/gradlew
@@ -205,6 +205,12 @@ set -- \
org.gradle.wrapper.GradleWrapperMain \
"$@"
+# Stop when "xargs" is not available.
+if ! command -v xargs >/dev/null 2>&1
+then
+ die "xargs is not available"
+fi
+
# Use "xargs" to parse quoted args.
#
# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
diff --git a/gradlew.bat b/gradlew.bat
index ac1b06f9382..53a6b238d41 100644
--- a/gradlew.bat
+++ b/gradlew.bat
@@ -14,7 +14,7 @@
@rem limitations under the License.
@rem
-@if "%DEBUG%" == "" @echo off
+@if "%DEBUG%"=="" @echo off
@rem ##########################################################################
@rem
@rem Gradle startup script for Windows
@@ -25,7 +25,7 @@
if "%OS%"=="Windows_NT" setlocal
set DIRNAME=%~dp0
-if "%DIRNAME%" == "" set DIRNAME=.
+if "%DIRNAME%"=="" set DIRNAME=.
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
@@ -40,7 +40,7 @@ if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
-if "%ERRORLEVEL%" == "0" goto execute
+if %ERRORLEVEL% equ 0 goto execute
echo.
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
@@ -75,13 +75,15 @@ set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
:end
@rem End local scope for the variables with windows NT shell
-if "%ERRORLEVEL%"=="0" goto mainEnd
+if %ERRORLEVEL% equ 0 goto mainEnd
:fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code!
-if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
-exit /b 1
+set EXIT_CODE=%ERRORLEVEL%
+if %EXIT_CODE% equ 0 set EXIT_CODE=1
+if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
+exit /b %EXIT_CODE%
:mainEnd
if "%OS%"=="Windows_NT" endlocal